// 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/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) 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()

	for cs, newL7Rules := range filterToMerge.PerSelectorPolicies {
		newPriority := newL7Rules.GetPriority()

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

		if l7Rules, ok := existingFilter.PerSelectorPolicies[cs]; ok {
			// existing filter already has 'cs', release and merge L7 rules
			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
			}

			if l7Rules.Equal(newL7Rules) {
				continue // identical rules need no merging
			}

			priority := l7Rules.GetPriority()
			// Check if either rule takes precedence due to precedence level or deny.
			if priority < newPriority || (priority == newPriority && l7Rules.IsDeny()) {
				// Later level newL7Rules has no effect.
				// Same level deny takes takes precedence over any other rule.
				continue
			} else if priority > newPriority || (priority == newPriority && newL7Rules.IsDeny()) {
				// Earlier level (or same level deny) newL7Rules takes precedence.
				// Overwrite existing filter.
				existingFilter.PerSelectorPolicies[cs] = newL7Rules
				continue
			}

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

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

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

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

			if l7Rules.TerminatingTLS == nil || newL7Rules.TerminatingTLS == nil {
				if newL7Rules.TerminatingTLS != nil {
					l7Rules.TerminatingTLS = newL7Rules.TerminatingTLS
				}
			} else if !newL7Rules.TerminatingTLS.Equal(l7Rules.TerminatingTLS) {
				policyCtx.PolicyTrace("   Merge conflict: mismatching terminating TLS contexts %s/%s\n", newL7Rules.TerminatingTLS, l7Rules.TerminatingTLS)
				return fmt.Errorf("cannot merge conflicting terminating TLS contexts for cached selector %s: (%s/%s)", cs.String(), newL7Rules.TerminatingTLS, l7Rules.TerminatingTLS)
			}
			if l7Rules.OriginatingTLS == nil || newL7Rules.OriginatingTLS == nil {
				if newL7Rules.OriginatingTLS != nil {
					l7Rules.OriginatingTLS = newL7Rules.OriginatingTLS
				}
			} else if !newL7Rules.OriginatingTLS.Equal(l7Rules.OriginatingTLS) {
				policyCtx.PolicyTrace("   Merge conflict: mismatching originating TLS contexts %s/%s\n", newL7Rules.OriginatingTLS, l7Rules.OriginatingTLS)
				return fmt.Errorf("cannot merge conflicting originating TLS contexts for cached selector %s: (%s/%s)", cs.String(), newL7Rules.OriginatingTLS, l7Rules.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.
			l7Rules.ServerNames = l7Rules.ServerNames.Merge(newL7Rules.ServerNames)

			// L7 rules can be applied with SNI filtering only if the TLS is also
			// terminated
			if len(l7Rules.ServerNames) > 0 && !l7Rules.L7Rules.IsEmpty() && l7Rules.TerminatingTLS == nil {
				policyCtx.PolicyTrace("   Merge conflict: cannot use SNI filtering with L7 rules without TLS termination: %v\n", l7Rules.ServerNames)
				return fmt.Errorf("cannot merge L7 rules for cached selector %s with SNI filtering without TLS termination: %v", cs.String(), l7Rules.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 !l7Rules.HasL7Rules() && newL7Rules.HasL7Rules() {
				l7Rules.L7Rules = newL7Rules.appendL7WildcardRule(policyCtx)
				existingFilter.PerSelectorPolicies[cs] = l7Rules
				continue
			}
			if l7Rules.HasL7Rules() && !newL7Rules.HasL7Rules() {
				l7Rules.appendL7WildcardRule(policyCtx)
				existingFilter.PerSelectorPolicies[cs] = l7Rules
				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 newL7Rules.HTTP {
				if !newRule.Exists(l7Rules.L7Rules) {
					l7Rules.HTTP = append(l7Rules.HTTP, newRule)
				}
			}
			for _, newRule := range newL7Rules.Kafka {
				if !newRule.Exists(l7Rules.L7Rules.Kafka) {
					l7Rules.Kafka = append(l7Rules.Kafka, newRule)
				}
			}
			if l7Rules.L7Proto == "" && newL7Rules.L7Proto != "" {
				l7Rules.L7Proto = newL7Rules.L7Proto
			}
			for _, newRule := range newL7Rules.L7 {
				if !newRule.Exists(l7Rules.L7Rules) {
					l7Rules.L7 = append(l7Rules.L7, newRule)
				}
			}
			for _, newRule := range newL7Rules.DNS {
				if !newRule.Exists(l7Rules.L7Rules) {
					l7Rules.DNS = append(l7Rules.DNS, newRule)
				}
			}
			// Update the pointer in the map in case it was newly allocated
			existingFilter.PerSelectorPolicies[cs] = l7Rules
		} else { // '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] = newL7Rules

			if cs.IsWildcard() {
				existingFilter.wildcard = cs
			}
		}
	}

	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()
}
