// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package policy

import (
	"fmt"
	"log/slog"

	"github.com/cilium/proxy/pkg/policy/api/kafka"

	"github.com/cilium/cilium/pkg/identity"
	ipcachetypes "github.com/cilium/cilium/pkg/ipcache/types"
	"github.com/cilium/cilium/pkg/labels"
	"github.com/cilium/cilium/pkg/policy/api"
	"github.com/cilium/cilium/pkg/policy/types"
)

// ruleKey is a synthetic unique identifier for a Rule
type ruleKey struct {
	// resource is the owning resource of this rule
	resource ipcachetypes.ResourceID

	// idx is an arbitrary unique index, as resources can own multiple rules
	idx uint
}

type rule struct {
	types.PolicyEntry
	key ruleKey

	// subjectSelector is the entry in the SelectorCache that selects subjects (endpoints or nodes).
	subjectSelector CachedSelector
}

// IdentitySelectionUpdated is called by the SelectorCache when a new identity is added;
// We can ignore it because the endpoint will be regenerated by the nature of
// identities being updated.
func (r *rule) IdentitySelectionUpdated(logger *slog.Logger, selector types.CachedSelector, added, deleted []identity.NumericIdentity) {
}

func (d *rule) IdentitySelectionCommit(*slog.Logger, SelectorSnapshot) {
}

func (r *rule) IsPeerSelector() bool {
	return false
}

func (r *rule) GetRuleLabels(cs CachedSelector) labels.LabelArrayList {
	if cs == r.subjectSelector {
		ro := r.origin()
		if ro != NilRuleOrigin {
			return ro.GetLabelArrayList()
		}
	}
	return nil
}

func (r *rule) String() string {
	return r.Subject.Key()
}

func (r *rule) origin() ruleOrigin {
	return makeSingleRuleOrigin(r.Labels.Sort(), r.Log.Value)
}

func (epd *PerSelectorPolicy) appendL7WildcardRule(policyContext PolicyContext) api.L7Rules {
	// Wildcard rule only needs to be appended if some rules already exist
	switch {
	case len(epd.L7Rules.HTTP) > 0:
		rule := api.PortRuleHTTP{}
		if !rule.Exists(epd.L7Rules) {
			policyContext.PolicyTrace("   Merging HTTP wildcard rule: %+v\n", rule)
			epd.L7Rules.HTTP = append(epd.L7Rules.HTTP, rule)
		} else {
			policyContext.PolicyTrace("   Merging HTTP wildcard rule, equal rule already exists: %+v\n", rule)
		}
	case len(epd.L7Rules.Kafka) > 0:
		rule := kafka.PortRule{}
		rule.Sanitize()
		if !rule.Exists(epd.L7Rules.Kafka) {
			policyContext.PolicyTrace("   Merging Kafka wildcard rule: %+v\n", rule)
			epd.L7Rules.Kafka = append(epd.L7Rules.Kafka, rule)
		} else {
			policyContext.PolicyTrace("   Merging Kafka wildcard rule, equal rule already exists: %+v\n", rule)
		}
	case len(epd.L7Rules.DNS) > 0:
		// Wildcarding at L7 for DNS is specified via allowing all via
		// MatchPattern!
		rule := api.PortRuleDNS{MatchPattern: "*"}
		rule.Sanitize()
		if !rule.Exists(epd.L7Rules) {
			policyContext.PolicyTrace("   Merging DNS wildcard rule: %+v\n", rule)
			epd.L7Rules.DNS = append(epd.L7Rules.DNS, rule)
		} else {
			policyContext.PolicyTrace("   Merging DNS wildcard rule, equal rule already exists: %+v\n", rule)
		}
	case epd.L7Rules.L7Proto != "" && len(epd.L7Rules.L7) > 0:
		rule := api.PortRuleL7{}
		if !rule.Exists(epd.L7Rules) {
			policyContext.PolicyTrace("   Merging L7 wildcard rule: %+v\n", rule)
			epd.L7Rules.L7 = append(epd.L7Rules.L7, rule)
		} else {
			policyContext.PolicyTrace("   Merging L7 wildcard rule, equal rule already exists: %+v\n", rule)
		}
	}
	return epd.L7Rules
}

// takesListenerPrecedenceOver returns true if the listener reference in 'l7Rules' takes precedence
// over the listener reference in 'other'.
func (l7Rules *PerSelectorPolicy) takesListenerPrecedenceOver(other *PerSelectorPolicy) bool {
	var priority, otherPriority ListenerPriority

	// decrement by one to wrap the undefined value (0) to be the highest numerical
	// value of the uint16, which is the lowest possible priority
	priority = l7Rules.ListenerPriority - 1
	otherPriority = other.ListenerPriority - 1

	return priority < otherPriority
}

// mergeRedirect merges listener reference from 'newL7Rules' to 'l7Rules', giving
// precedence to listener with the lowest priority, if any.
func (l7Rules *PerSelectorPolicy) mergeRedirect(newL7Rules *PerSelectorPolicy) error {
	// Merge L7ParserType, if possible
	l7Parser, err := l7Rules.L7Parser.Merge(newL7Rules.L7Parser)
	if err != nil {
		return err
	}

	if l7Parser != l7Rules.L7Parser {
		// Also copy over the listener priority
		l7Rules.L7Parser = l7Parser
		l7Rules.ListenerPriority = newL7Rules.ListenerPriority
	}

	// Nothing to do if 'newL7Rules' has no listener reference
	if newL7Rules.Listener == "" {
		return nil
	}

	// Nothing to do if the listeners are already the same and have the same priority
	if newL7Rules.Listener == l7Rules.Listener && l7Rules.ListenerPriority == newL7Rules.ListenerPriority {
		return nil
	}

	// Nothing to do if 'l7Rules' takes precedence
	if l7Rules.takesListenerPrecedenceOver(newL7Rules) {
		return nil
	}

	// override if 'l7Rules' has no listener or 'newL7Rules' takes precedence
	if l7Rules.Listener == "" || newL7Rules.takesListenerPrecedenceOver(l7Rules) {
		l7Rules.Listener = newL7Rules.Listener
		l7Rules.ListenerPriority = newL7Rules.ListenerPriority
		return nil
	}

	// otherwise error on conflict
	return fmt.Errorf("cannot merge conflicting CiliumEnvoyConfig Listeners (%v/%v) with the same priority (%d)", newL7Rules.Listener, l7Rules.Listener, l7Rules.ListenerPriority)
}

// mergePortProto merges the L7-related data from the filter to merge
// with the L7-related data already in the existing filter.
func (existingFilter *L4Filter) mergePortProto(policyCtx PolicyContext, filterToMerge *L4Filter) (err error) {
	selectorCache := policyCtx.GetSelectorCache()

	// Iterate through each PerSelectorPolicy for each existing CachedSelector.
	// Note that 'newPolicy' can be 'nil' for a zero-valued PerSelectorPolicy
	// (== allow of highest precedence without any L7 rules nor TLS contexts).
	for cs, newPolicy := range filterToMerge.PerSelectorPolicies {
		newRuleOrigin := filterToMerge.RuleOrigin[cs]
		newPriority := newPolicy.GetPriority()

		// 'cs' will be merged or moved (see below), either way it needs
		// to be removed from the maps it is in now.
		delete(filterToMerge.PerSelectorPolicies, cs)
		delete(filterToMerge.RuleOrigin, cs)

		// Get the existing PerSelectorPolicies for this CachedSelector, if it exists
		if existingPolicy, ok := existingFilter.PerSelectorPolicies[cs]; !ok {
			// 'cs' is not in the existing filter yet

			// Update selector owner to the existing filter
			selectorCache.ChangeUser(cs, filterToMerge, existingFilter)

			// Move L7 rules over.
			existingFilter.PerSelectorPolicies[cs] = newPolicy

			// add rule origin for 'cs' to the existing filter
			existingFilter.RuleOrigin[cs] = newRuleOrigin

			if cs.IsWildcard() {
				existingFilter.wildcard = cs
			}
		} else {
			// existing filter already has 'cs', release duplicate cached selector and
			// merge per-selector policies
			selectorCache.RemoveSelector(cs, filterToMerge)

			// skip merging for reserved:none, as it is never
			// selected, and toFQDN rules currently translate to
			// reserved:none as an endpoint selector, causing a
			// merge conflict for different toFQDN destinations
			// with different TLS contexts.
			if cs.IsNone() {
				continue
			}

			existingRuleOrigin := existingFilter.RuleOrigin[cs]

			if existingPolicy.Equal(newPolicy) {
				// identical rules for the same selector need no merging, but could
				// still have different rule origin so merge them
				existingFilter.RuleOrigin[cs] = existingRuleOrigin.Merge(newRuleOrigin)
				continue
			}

			priority := existingPolicy.GetPriority()
			// Check if either rule takes precedence due to precedence level or deny.
			if priority < newPriority || (priority == newPriority && existingPolicy.HasPrecedenceOver(newPolicy)) {
				// Lower precedence newPolicy has no effect.
				// Same level deny takes takes precedence over any other rule.
				// Rule origins are not merged, existingRuleOrigin is retained.
				continue
			}
			if priority > newPriority || (priority == newPriority && newPolicy.HasPrecedenceOver(existingPolicy)) {
				// Earlier level (or same level deny) newPolicy takes precedence.
				// Overwrite existing filter.
				existingFilter.PerSelectorPolicies[cs] = newPolicy
				existingFilter.RuleOrigin[cs] = newRuleOrigin
				continue
			}

			// Merge two non-identical sets of allow rules on the same precedence level

			// Merge the rule origin metadata
			existingFilter.RuleOrigin[cs] = existingRuleOrigin.Merge(newRuleOrigin)

			// One of the rules may be a nil rule, expand it to an empty non-nil rule
			if existingPolicy == nil {
				existingPolicy = &PerSelectorPolicy{Verdict: types.Allow}
			}
			if newPolicy == nil {
				newPolicy = &PerSelectorPolicy{Verdict: types.Allow}
			}

			// Merge Redirect
			if err := existingPolicy.mergeRedirect(newPolicy); err != nil {
				policyCtx.PolicyTrace("   Merge conflict: %s\n", err.Error())
				return err
			}

			if existingPolicy.Authentication == nil || newPolicy.Authentication == nil {
				if newPolicy.Authentication != nil {
					existingPolicy.Authentication = newPolicy.Authentication
				}
			} else if !newPolicy.Authentication.DeepEqual(existingPolicy.Authentication) {
				policyCtx.PolicyTrace("   Merge conflict: mismatching auth types %s/%s\n", newPolicy.Authentication.Mode, existingPolicy.Authentication.Mode)
				return fmt.Errorf("cannot merge conflicting authentication types (%s/%s)", newPolicy.Authentication.Mode, existingPolicy.Authentication.Mode)
			}

			if existingPolicy.TerminatingTLS == nil || newPolicy.TerminatingTLS == nil {
				if newPolicy.TerminatingTLS != nil {
					existingPolicy.TerminatingTLS = newPolicy.TerminatingTLS
				}
			} else if !newPolicy.TerminatingTLS.Equal(existingPolicy.TerminatingTLS) {
				policyCtx.PolicyTrace("   Merge conflict: mismatching terminating TLS contexts %s/%s\n", newPolicy.TerminatingTLS, existingPolicy.TerminatingTLS)
				return fmt.Errorf("cannot merge conflicting terminating TLS contexts for cached selector %s: (%s/%s)", cs.String(), newPolicy.TerminatingTLS, existingPolicy.TerminatingTLS)
			}
			if existingPolicy.OriginatingTLS == nil || newPolicy.OriginatingTLS == nil {
				if newPolicy.OriginatingTLS != nil {
					existingPolicy.OriginatingTLS = newPolicy.OriginatingTLS
				}
			} else if !newPolicy.OriginatingTLS.Equal(existingPolicy.OriginatingTLS) {
				policyCtx.PolicyTrace("   Merge conflict: mismatching originating TLS contexts %s/%s\n", newPolicy.OriginatingTLS, existingPolicy.OriginatingTLS)
				return fmt.Errorf("cannot merge conflicting originating TLS contexts for cached selector %s: (%s/%s)", cs.String(), newPolicy.OriginatingTLS, existingPolicy.OriginatingTLS)
			}

			// For now we simply merge the set of allowed SNIs from different rules
			// to/from the *same remote*, port, and protocol. This means that if any
			// rule requires SNI, then all traffic to that remote/port requires TLS,
			// even if other merged rules would be fine without TLS. Any SNI from all
			// applicable rules is allowed.
			//
			// Preferably we could allow different rules for each SNI, but for now the
			// combination of all L7 rules is allowed for all the SNIs. For example, if
			// SNI and TLS termination are used together so that L7 filtering is
			// possible, in this example:
			//
			// - existing: SNI: public.example.com
			// - new:      SNI: private.example.com HTTP: path="/public"
			//
			// Separately, these rule allow access to all paths at SNI
			// public.example.com and path private.example.com/public, but currently we
			// allow all paths also at private.example.com. This may be clamped down if
			// there is sufficient demand for SNI and TLS termination together.
			//
			// Note however that SNI rules are typically used with `toFQDNs`, each of
			// which defines a separate destination, so that SNIs for different
			// `toFQDNs` will not be merged together.
			existingPolicy.ServerNames = existingPolicy.ServerNames.Merge(newPolicy.ServerNames)

			// L7 rules can be applied with SNI filtering only if the TLS is also
			// terminated
			if len(existingPolicy.ServerNames) > 0 && !existingPolicy.L7Rules.IsEmpty() && existingPolicy.TerminatingTLS == nil {
				policyCtx.PolicyTrace("   Merge conflict: cannot use SNI filtering with L7 rules without TLS termination: %v\n", existingPolicy.ServerNames)
				return fmt.Errorf("cannot merge L7 rules for cached selector %s with SNI filtering without TLS termination: %v", cs.String(), existingPolicy.ServerNames)
			}

			// empty L7 rules effectively wildcard L7. When merging with a non-empty
			// rule, the empty must be expanded to an actual wildcard rule for the
			// specific L7
			if !existingPolicy.HasL7Rules() && newPolicy.HasL7Rules() {
				existingPolicy.L7Rules = newPolicy.appendL7WildcardRule(policyCtx)
				existingFilter.PerSelectorPolicies[cs] = existingPolicy
				continue
			}
			if existingPolicy.HasL7Rules() && !newPolicy.HasL7Rules() {
				existingPolicy.appendL7WildcardRule(policyCtx)
				existingFilter.PerSelectorPolicies[cs] = existingPolicy
				continue
			}

			// We already know from the L7Parser.Merge() above that there are no
			// conflicting parser types, and rule validation only allows one type of L7
			// rules in a rule, so we can just merge the rules here.
			for _, newRule := range newPolicy.HTTP {
				if !newRule.Exists(existingPolicy.L7Rules) {
					existingPolicy.HTTP = append(existingPolicy.HTTP, newRule)
				}
			}
			for _, newRule := range newPolicy.Kafka {
				if !newRule.Exists(existingPolicy.L7Rules.Kafka) {
					existingPolicy.Kafka = append(existingPolicy.Kafka, newRule)
				}
			}
			if existingPolicy.L7Proto == "" && newPolicy.L7Proto != "" {
				existingPolicy.L7Proto = newPolicy.L7Proto
			}
			for _, newRule := range newPolicy.L7 {
				if !newRule.Exists(existingPolicy.L7Rules) {
					existingPolicy.L7 = append(existingPolicy.L7, newRule)
				}
			}
			for _, newRule := range newPolicy.DNS {
				if !newRule.Exists(existingPolicy.L7Rules) {
					existingPolicy.DNS = append(existingPolicy.DNS, newRule)
				}
			}
			// Update the pointer in the map in case it was newly allocated
			existingFilter.PerSelectorPolicies[cs] = existingPolicy
		}
	}

	return nil
}

// addFilter merges all rules which share the same port & protocol that
// select a given set of endpoints. It updates the L4Filter mapped to by the specified
// port and protocol with the contents of the provided PortRule. If the rule
// being merged has conflicting L7 rules with those already in the provided
// L4PolicyMap for the specified port-protocol tuple, it returns an error.
func (resMap *L4PolicyMap) addFilter(policyCtx PolicyContext, entry *types.PolicyEntry, portRule api.Ports, p api.PortProtocol) (int, error) {
	// Create a new L4Filter
	filterToMerge, err := createL4Filter(policyCtx, entry, portRule, p)
	if err != nil {
		return 0, err
	}

	err = resMap.addL4Filter(policyCtx, p, filterToMerge)
	if err != nil {
		return 0, err
	}

	// Currently we return '1' here even if all of the filterToMerge was skipped due to priority
	// in mergePortProto called by addL4Filter.
	return 1, err
}

func (resMap *L4PolicyMap) mergeL4Filter(policyCtx PolicyContext, rule *rule) (int, error) {
	found := 0

	// short-circuit if no endpoint is selected
	if rule.L3 == nil {
		return found, nil
	}

	var (
		cnt int
		err error
	)

	// L3-only rule.
	if rule.L4.Len() == 0 && len(rule.L3) > 0 {
		cnt, err = resMap.addFilter(policyCtx, &rule.PolicyEntry, &api.PortRule{}, api.PortProtocol{Port: "0", Protocol: api.ProtoAny})
		if err != nil {
			return found, err
		}
	}

	found += cnt

	err = rule.L4.Iterate(func(ports api.Ports) error {
		if !rule.IsDeny() {
			policyCtx.PolicyTrace("      Allows port %v\n", ports.GetPortProtocols())
		} else {
			policyCtx.PolicyTrace("      Denies port %v\n", ports.GetPortProtocols())
		}

		pr := ports.GetPortRule()
		if pr != nil {
			if pr.Rules != nil && pr.Rules.L7Proto != "" {
				policyCtx.PolicyTrace("        l7proto: \"%s\"\n", pr.Rules.L7Proto)
			}
			if !pr.Rules.IsEmpty() {
				for _, l7 := range pr.Rules.HTTP {
					policyCtx.PolicyTrace("          %+v\n", l7)
				}
				for _, l7 := range pr.Rules.Kafka {
					policyCtx.PolicyTrace("          %+v\n", l7)
				}
				for _, l7 := range pr.Rules.L7 {
					policyCtx.PolicyTrace("          %+v\n", l7)
				}
			}
		}

		for _, p := range ports.GetPortProtocols() {
			protocols := []api.L4Proto{p.Protocol}
			if p.Protocol.IsAny() {
				protocols = []api.L4Proto{
					api.ProtoTCP,
					api.ProtoUDP,
					api.ProtoSCTP,
				}
			}
			for _, protocol := range protocols {
				p.Protocol = protocol
				cnt, err := resMap.addFilter(policyCtx, &rule.PolicyEntry, ports, p)
				if err != nil {
					return err
				}
				found += cnt
			}
		}
		return nil
	})

	return found, err
}

func (pms *L4PolicyMaps) ensureTier(tier types.Tier) {
	for len(*pms) <= int(tier) {
		*pms = append(*pms, makeL4PolicyMap())
	}
}

// resolveL4Policy analyzes the rule against the given SearchContext, and
// merges it with any prior-generated policy within the provided L4Policy.
//
// If policyCtx.IsIngress() returns true, an ingress policy isresolved,
// otherwise an egress policy is resolved.
func (result *L4PolicyMaps) resolveL4Policy(
	policyCtx PolicyContext,
	state *traceState,
	r *rule,
) error {
	state.selectRule(policyCtx, r)
	found, foundDeny := 0, 0

	policyCtx.SetOrigin(r.origin())
	result.ensureTier(r.Tier)
	cnt, err := (*result)[r.Tier].mergeL4Filter(policyCtx, r)
	if err != nil {
		return err
	}
	if cnt > 0 {
		if r.IsDeny() {
			foundDeny += cnt
		} else {
			found += cnt
		}
	}

	if found != 0 {
		state.matchedRules++
	}
	if foundDeny != 0 {
		state.matchedDenyRules++
	}

	return nil
}

func (r *rule) matchesSubject(securityIdentity *identity.Identity) bool {
	subjectIsNode := securityIdentity.ID == identity.ReservedIdentityHost
	ruleSelectsNode := r.Node

	// Short-circuit if the rule's selector type (node vs. endpoint) does not match the
	// identity's type
	if ruleSelectsNode != subjectIsNode {
		return false
	}
	// Fall back to explicit label matching for the local node
	// because local node has mutable labels, which are applied asynchronously to the SelectorCache.
	if r.subjectSelector == nil || ruleSelectsNode {
		return r.Subject.Matches(securityIdentity.LabelArray)
	}

	return r.subjectSelector.Selects(securityIdentity.ID)
}

func (r *rule) getSubjects() []identity.NumericIdentity {
	if r.Node {
		return []identity.NumericIdentity{identity.ReservedIdentityHost}
	}

	return r.subjectSelector.GetSelections()
}
