// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0

package ruleengine

import (
	"fmt"
	"strings"

	api "github.com/projectcapsule/capsule/pkg/api/rules"
)

type Value struct {
	Value string
	Path  string
}

type Match struct {
	Matched      bool
	MatchedValue any

	// Detail is optional human-readable matcher context.
	// Example: "10.0.171.239 is contained in 10.0.0.0/16".
	Detail string
}

type Decision struct {
	SetName     string
	EventReason string
	Action      api.ActionType
	Value       Value

	MatchedValue any

	// MatchedRule is the human-readable rule description returned by Set.RuleDescription.
	MatchedRule string

	// MatchDetail is the human-readable detail returned by Match.Detail.
	MatchDetail string

	Message string
}

type DecisionError struct {
	Decision *Decision
}

func (e *DecisionError) Error() string {
	if e == nil || e.Decision == nil {
		return "namespace rule decision denied request"
	}

	return e.Decision.Message
}

type Evaluation struct {
	// Final is the last matching allow/deny decision.
	Final *Decision

	// Blocking is set when the final result blocks admission.
	Blocking *Decision

	// Audits contains all matching audit decisions.
	Audits []*Decision
}

func (e *Evaluation) BlockingError() error {
	if e == nil || e.Blocking == nil {
		return nil
	}

	return &DecisionError{
		Decision: e.Blocking,
	}
}

func (e *Evaluation) Append(other *Evaluation) {
	if e == nil || other == nil {
		return
	}

	e.Audits = append(e.Audits, other.Audits...)

	if other.Final != nil {
		e.Final = other.Final
	}

	if other.Blocking != nil {
		e.Blocking = other.Blocking
	}
}

type Set[R any, O any] struct {
	Name        string
	EventReason string

	Values func(O) []Value
	// Rules extracts rules when extraction cannot fail.
	Rules func(*api.NamespaceRuleEnforceBody) []R
	// RulesWithError extracts rules that require runtime parsing or matching.
	// When set, it takes precedence over Rules.
	RulesWithError func(*api.NamespaceRuleEnforceBody) ([]R, error)
	Matches        func(R, Value) (Match, error)

	// Message can fully override the default message.
	// Prefer leaving this nil unless a rule requires very specific wording.
	Message func(api.ActionType, Value, any) string

	// RuleDescription returns a human-readable representation of one rule.
	// It is used only for admission/audit messages.
	RuleDescription func(R) string

	// AllowedDescription optionally overrides the "Allowed values" label.
	// Example: "Allowed CIDRs", "Allowed ranges", "Allowed hostnames".
	AllowedDescription string
}

func EvaluateEnforce[R any, T any](
	obj T,
	enforceBodies []*api.NamespaceRuleEnforceBody,
	set Set[R, T],
) (*Evaluation, error) {
	if set.Name == "" {
		return nil, fmt.Errorf("rule set name is empty")
	}

	if set.Values == nil {
		return nil, fmt.Errorf("%s: values extractor is nil", set.Name)
	}

	if set.Rules == nil && set.RulesWithError == nil {
		return nil, fmt.Errorf("%s: rules extractor is nil", set.Name)
	}

	if set.Matches == nil {
		return nil, fmt.Errorf("%s: matcher is nil", set.Name)
	}

	evaluation := &Evaluation{}

	values := set.Values(obj)
	if len(values) == 0 {
		return evaluation, nil
	}

	for _, value := range values {
		if value.Value == "" {
			continue
		}

		hasAllowRule := false
		allowRules := make([]R, 0)

		var lastDecision *Decision

		for _, enforce := range enforceBodies {
			if enforce == nil {
				continue
			}

			var items []R

			if set.RulesWithError != nil {
				var err error

				items, err = set.RulesWithError(enforce)
				if err != nil {
					return evaluation, fmt.Errorf("%s: invalid rules: %w", set.Name, err)
				}
			} else {
				items = set.Rules(enforce)
			}

			if len(items) == 0 {
				continue
			}

			action := enforce.Action.OrDefault()

			switch action {
			case api.ActionTypeAllow:
				hasAllowRule = true

				allowRules = append(allowRules, items...)

			case api.ActionTypeDeny, api.ActionTypeAudit:
				// Supported actions.

			default:
				return evaluation, fmt.Errorf(
					"%s: unsupported rule action %q",
					set.Name,
					action,
				)
			}

			for _, item := range items {
				match, err := set.Matches(item, value)
				if err != nil {
					return evaluation, fmt.Errorf("%s: invalid rule: %w", set.Name, err)
				}

				if !match.Matched {
					continue
				}

				matchedRule := describeRule(set, item)

				decision := &Decision{
					SetName:      set.Name,
					EventReason:  set.EventReason,
					Action:       action,
					Value:        value,
					MatchedValue: match.MatchedValue,
					MatchedRule:  matchedRule,
					MatchDetail:  strings.TrimSpace(match.Detail),
					Message: decisionMessage(
						set,
						action,
						value,
						match.MatchedValue,
						matchedRule,
						match.Detail,
					),
				}

				switch action {
				case api.ActionTypeAudit:
					// Audit is purely observational. It must not influence
					// allow/deny evaluation.
					evaluation.Audits = append(evaluation.Audits, decision)

				case api.ActionTypeAllow, api.ActionTypeDeny:
					// Last matching allow/deny wins.
					lastDecision = decision
				}
			}
		}

		if lastDecision != nil {
			evaluation.Final = lastDecision

			if lastDecision.Action == api.ActionTypeDeny {
				evaluation.Blocking = lastDecision

				return evaluation, nil
			}

			continue
		}

		if hasAllowRule {
			evaluation.Blocking = &Decision{
				SetName:     set.Name,
				EventReason: set.EventReason,
				Action:      api.ActionTypeDeny,
				Value:       value,
				Message:     allowMissMessage(set, value, allowRules),
			}

			return evaluation, nil
		}
	}

	return evaluation, nil
}

const maxRuleDescriptions = 10

func describeRule[R any, O any](set Set[R, O], rule R) string {
	if set.RuleDescription == nil {
		return ""
	}

	return strings.TrimSpace(set.RuleDescription(rule))
}

func describeRules[R any, O any](set Set[R, O], rules []R) string {
	if len(rules) == 0 || set.RuleDescription == nil {
		return ""
	}

	limit := min(len(rules), maxRuleDescriptions)

	parts := make([]string, 0, limit)

	for i := range limit {
		description := describeRule(set, rules[i])
		if description == "" {
			continue
		}

		parts = append(parts, description)
	}

	if len(parts) == 0 {
		return ""
	}

	if len(rules) > maxRuleDescriptions {
		parts = append(parts, fmt.Sprintf("and %d more", len(rules)-maxRuleDescriptions))
	}

	return strings.Join(parts, ", ")
}

func allowedLabel[R any, O any](set Set[R, O]) string {
	if set.AllowedDescription != "" {
		return set.AllowedDescription
	}

	return "Allowed values"
}

func allowMissMessage[R any, T any](
	set Set[R, T],
	value Value,
	allowRules []R,
) string {
	message := fmt.Sprintf(
		"%s %q at %s is not allowed by namespace rule",
		set.Name,
		value.Value,
		value.Path,
	)

	descriptions := describeRules(set, allowRules)
	if descriptions == "" {
		return message
	}

	return fmt.Sprintf(
		"%s: value did not match any allowed rule. %s: %s",
		message,
		allowedLabel(set),
		descriptions,
	)
}

func decisionMessage[R any, T any](
	set Set[R, T],
	action api.ActionType,
	value Value,
	matchedValue any,
	matchedRule string,
	matchDetail string,
) string {
	if set.Message != nil {
		return set.Message(action, value, matchedValue)
	}

	matchDetail = strings.TrimSpace(matchDetail)

	switch action {
	case api.ActionTypeAudit:
		message := fmt.Sprintf(
			"%s %q at %s matched audit namespace rule",
			set.Name,
			value.Value,
			value.Path,
		)

		return appendMatchContext(message, matchedRule, matchDetail, "matched audit rule")

	case api.ActionTypeDeny:
		message := fmt.Sprintf(
			"%s %q at %s is denied by namespace rule",
			set.Name,
			value.Value,
			value.Path,
		)

		return appendMatchContext(message, matchedRule, matchDetail, "matched denied rule")

	case api.ActionTypeAllow:
		message := fmt.Sprintf(
			"%s %q at %s is allowed by namespace rule",
			set.Name,
			value.Value,
			value.Path,
		)

		return appendMatchContext(message, matchedRule, matchDetail, "matched allowed rule")

	default:
		return fmt.Sprintf(
			"%s %q at %s matched namespace rule action %q",
			set.Name,
			value.Value,
			value.Path,
			action,
		)
	}
}

func appendMatchContext(
	message string,
	matchedRule string,
	matchDetail string,
	rulePrefix string,
) string {
	if matchDetail != "" {
		return fmt.Sprintf("%s: %s", message, matchDetail)
	}

	if matchedRule != "" {
		return fmt.Sprintf("%s: %s %s", message, rulePrefix, matchedRule)
	}

	return message
}
