package structs

import (
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/hashicorp/consul/acl"
)

type ServiceIntentionsConfigEntry struct {
	Kind string
	Name string // formerly DestinationName

	Sources []*SourceIntention

	Meta map[string]string `json:",omitempty"` // formerly Intention.Meta

	EnterpriseMeta `hcl:",squash" mapstructure:",squash"` // formerly DestinationNS
	RaftIndex
}

var _ UpdatableConfigEntry = (*ServiceIntentionsConfigEntry)(nil)

func (e *ServiceIntentionsConfigEntry) GetKind() string {
	return ServiceIntentions
}

func (e *ServiceIntentionsConfigEntry) GetName() string {
	if e == nil {
		return ""
	}

	return e.Name
}

func (e *ServiceIntentionsConfigEntry) GetMeta() map[string]string {
	if e == nil {
		return nil
	}
	return e.Meta
}

func (e *ServiceIntentionsConfigEntry) Clone() *ServiceIntentionsConfigEntry {
	e2 := *e

	e2.Meta = cloneStringStringMap(e.Meta)

	e2.Sources = make([]*SourceIntention, len(e.Sources))
	for i, src := range e.Sources {
		e2.Sources[i] = src.Clone()
	}

	return &e2
}

func (e *ServiceIntentionsConfigEntry) DestinationServiceName() ServiceName {
	return NewServiceName(e.Name, &e.EnterpriseMeta)
}

func (e *ServiceIntentionsConfigEntry) UpdateSourceByLegacyID(legacyID string, update *SourceIntention) bool {
	for i, src := range e.Sources {
		if src.LegacyID == legacyID {
			e.Sources[i] = update
			return true
		}
	}
	return false
}

func (e *ServiceIntentionsConfigEntry) UpsertSourceByName(sn ServiceName, upsert *SourceIntention) {
	for i, src := range e.Sources {
		if src.SourceServiceName() == sn {
			e.Sources[i] = upsert
			return
		}
	}

	e.Sources = append(e.Sources, upsert)
}

func (e *ServiceIntentionsConfigEntry) DeleteSourceByLegacyID(legacyID string) bool {
	for i, src := range e.Sources {
		if src.LegacyID == legacyID {
			// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
			//    a = append(a[:i], a[i+1:]...)
			e.Sources = append(e.Sources[:i], e.Sources[i+1:]...)

			if len(e.Sources) == 0 {
				e.Sources = nil
			}
			return true
		}
	}
	return false
}

func (e *ServiceIntentionsConfigEntry) DeleteSourceByName(sn ServiceName) bool {
	for i, src := range e.Sources {
		if src.SourceServiceName() == sn {
			// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
			//    a = append(a[:i], a[i+1:]...)
			e.Sources = append(e.Sources[:i], e.Sources[i+1:]...)

			if len(e.Sources) == 0 {
				e.Sources = nil
			}
			return true
		}
	}
	return false
}

func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intention {
	meta := e.Meta
	if src.LegacyID != "" {
		meta = src.LegacyMeta
	}

	ixn := &Intention{
		ID:              src.LegacyID,
		Description:     src.Description,
		SourceNS:        src.NamespaceOrDefault(),
		SourceName:      src.Name,
		SourceType:      src.Type,
		Action:          src.Action,
		Permissions:     src.Permissions,
		Meta:            meta,
		Precedence:      src.Precedence,
		DestinationNS:   e.NamespaceOrDefault(),
		DestinationName: e.Name,
		RaftIndex:       e.RaftIndex,
	}
	if src.LegacyCreateTime != nil {
		ixn.CreatedAt = *src.LegacyCreateTime
	}
	if src.LegacyUpdateTime != nil {
		ixn.UpdatedAt = *src.LegacyUpdateTime
	}

	if src.LegacyID != "" {
		// Ensure that pre-1.9.0 secondaries can still replicate legacy
		// intentions via the APIs. These require the Hash field to be
		// populated.
		//
		//nolint:staticcheck
		ixn.SetHash()
	}
	return ixn
}

func (e *ServiceIntentionsConfigEntry) LegacyIDFieldsAreAllEmpty() bool {
	for _, src := range e.Sources {
		if src.LegacyID != "" {
			return false
		}
	}
	return true
}

func (e *ServiceIntentionsConfigEntry) LegacyIDFieldsAreAllSet() bool {
	for _, src := range e.Sources {
		if src.LegacyID == "" {
			return false
		}
	}
	return true
}

func (e *ServiceIntentionsConfigEntry) ToIntentions() Intentions {
	out := make(Intentions, 0, len(e.Sources))
	for _, src := range e.Sources {
		out = append(out, e.ToIntention(src))
	}
	return out
}

type SourceIntention struct {
	// Name is the name of the source service. This can be a wildcard "*", but
	// only the full value can be a wildcard. Partial wildcards are not
	// allowed.
	//
	// The source may also be a non-Consul service, as specified by SourceType.
	//
	// formerly Intention.SourceName
	Name string

	// Action is whether this is an allowlist or denylist intention.
	//
	// formerly Intention.Action
	//
	// NOTE: this is mutually exclusive with the Permissions field.
	Action IntentionAction `json:",omitempty"`

	// Permissions is the list of additional L7 attributes that extend the
	// intention definition.
	//
	// Permissions are interpreted in the order represented in the slice. In
	// default-deny mode, deny permissions are logically subtracted from all
	// following allow permissions. Multiple allow permissions are then ORed
	// together.
	//
	// For example:
	//   ["deny /v2/admin", "allow /v2/*", "allow GET /healthz"]
	//
	// Is logically interpreted as:
	//   allow: [
	//     "(/v2/*) AND NOT (/v2/admin)",
	//     "(GET /healthz) AND NOT (/v2/admin)"
	//   ]
	Permissions []*IntentionPermission `json:",omitempty"`

	// Precedence is the order that the intention will be applied, with
	// larger numbers being applied first. This is a read-only field, on
	// any intention update it is updated.
	//
	// Note we will technically decode this over the wire during a write, but
	// we always recompute it on save.
	//
	// formerly Intention.Precedence
	Precedence int

	// LegacyID is manipulated just by the bridging code
	// used as part of backwards compatibility.
	//
	// formerly Intention.ID
	LegacyID string `json:",omitempty" alias:"legacy_id"`

	// Type is the type of the value for the source.
	//
	// formerly Intention.SourceType
	Type IntentionSourceType

	// Description is a human-friendly description of this intention.
	// It is opaque to Consul and is only stored and transferred in API
	// requests.
	//
	// formerly Intention.Description
	Description string `json:",omitempty"`

	// LegacyMeta is arbitrary metadata associated with the intention. This is
	// opaque to Consul but is served in API responses.
	//
	// formerly Intention.Meta
	LegacyMeta map[string]string `json:",omitempty" alias:"legacy_meta"`

	// LegacyCreateTime is formerly Intention.CreatedAt
	LegacyCreateTime *time.Time `json:",omitempty" alias:"legacy_create_time"`
	// LegacyUpdateTime is formerly Intention.UpdatedAt
	LegacyUpdateTime *time.Time `json:",omitempty" alias:"legacy_update_time"`

	// Things like L7 rules or Sentinel rules could go here later.

	// formerly Intention.SourceNS
	EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
}

type IntentionPermission struct {
	Action IntentionAction // required: allow|deny

	HTTP *IntentionHTTPPermission `json:",omitempty"`

	// If we have non-http match criteria for other protocols
	// in the future (gRPC, redis, etc) they can go here.

	// Support for edge-decoded JWTs would likely be configured
	// in a new top level section here.

	// If we ever add Sentinel support, this is one place we may
	// wish to add it.
}

func (p *IntentionPermission) Clone() *IntentionPermission {
	p2 := *p
	if p.HTTP != nil {
		p2.HTTP = p.HTTP.Clone()
	}
	return &p2
}

type IntentionHTTPPermission struct {
	// PathExact, PathPrefix, and PathRegex are mutually exclusive.
	PathExact  string `json:",omitempty" alias:"path_exact"`
	PathPrefix string `json:",omitempty" alias:"path_prefix"`
	PathRegex  string `json:",omitempty" alias:"path_regex"`

	Header []IntentionHTTPHeaderPermission `json:",omitempty"`

	Methods []string `json:",omitempty"`
}

func (p *IntentionHTTPPermission) Clone() *IntentionHTTPPermission {
	p2 := *p

	if len(p.Header) > 0 {
		p2.Header = make([]IntentionHTTPHeaderPermission, 0, len(p.Header))
		for _, hdr := range p.Header {
			p2.Header = append(p2.Header, hdr)
		}
	}

	p2.Methods = CloneStringSlice(p.Methods)

	return &p2
}

type IntentionHTTPHeaderPermission struct {
	Name    string
	Present bool   `json:",omitempty"`
	Exact   string `json:",omitempty"`
	Prefix  string `json:",omitempty"`
	Suffix  string `json:",omitempty"`
	Regex   string `json:",omitempty"`
	Invert  bool   `json:",omitempty"`
}

func cloneStringStringMap(m map[string]string) map[string]string {
	if m == nil {
		return nil
	}
	m2 := make(map[string]string)
	for k, v := range m {
		m2[k] = v
	}
	return m2
}

func (x *SourceIntention) SourceServiceName() ServiceName {
	return NewServiceName(x.Name, &x.EnterpriseMeta)
}

func (x *SourceIntention) Clone() *SourceIntention {
	x2 := *x

	x2.LegacyMeta = cloneStringStringMap(x.LegacyMeta)

	if len(x.Permissions) > 0 {
		x2.Permissions = make([]*IntentionPermission, 0, len(x.Permissions))
		for _, perm := range x.Permissions {
			x2.Permissions = append(x2.Permissions, perm.Clone())
		}
	}

	return &x2
}

func (e *ServiceIntentionsConfigEntry) UpdateOver(rawPrev ConfigEntry) error {
	if rawPrev == nil {
		return nil
	}

	prev, ok := rawPrev.(*ServiceIntentionsConfigEntry)
	if !ok {
		return fmt.Errorf("previous config entry is not of type %T: %T", e, rawPrev)
	}

	var (
		prevSourceByName     = make(map[ServiceName]*SourceIntention)
		prevSourceByLegacyID = make(map[string]*SourceIntention)
	)
	for _, src := range prev.Sources {
		prevSourceByName[src.SourceServiceName()] = src
		if src.LegacyID != "" {
			prevSourceByLegacyID[src.LegacyID] = src
		}
	}

	for i, src := range e.Sources {
		if src.LegacyID == "" {
			continue
		}

		// Check that the LegacyID fields are handled correctly during updates.
		if prevSrc, ok := prevSourceByName[src.SourceServiceName()]; ok {
			if prevSrc.LegacyID == "" {
				return fmt.Errorf("Sources[%d].LegacyID: cannot set this field", i)
			} else if src.LegacyID != prevSrc.LegacyID {
				return fmt.Errorf("Sources[%d].LegacyID: cannot set this field to a different value", i)
			}
		}

		// Now ensure legacy timestamps carry over properly. We always retain the LegacyCreateTime.
		if prevSrc, ok := prevSourceByLegacyID[src.LegacyID]; ok {
			if prevSrc.LegacyCreateTime != nil {
				// NOTE: we don't want to share the memory here
				src.LegacyCreateTime = timePointer(*prevSrc.LegacyCreateTime)
			}
		}
	}

	return nil
}

func (e *ServiceIntentionsConfigEntry) Normalize() error {
	return e.normalize(false)
}

func (e *ServiceIntentionsConfigEntry) LegacyNormalize() error {
	return e.normalize(true)
}

func (e *ServiceIntentionsConfigEntry) normalize(legacyWrite bool) error {
	if e == nil {
		return fmt.Errorf("config entry is nil")
	}

	// NOTE: this function must be deterministic so that the raft log doesn't
	// diverge. This means no ID assignments or time.Now() usage!

	e.Kind = ServiceIntentions

	e.EnterpriseMeta.Normalize()

	for _, src := range e.Sources {
		// Default source type
		if src.Type == "" {
			src.Type = IntentionSourceConsul
		}

		// If the source namespace is omitted it inherits that of the
		// destination.
		src.EnterpriseMeta.MergeNoWildcard(&e.EnterpriseMeta)
		src.EnterpriseMeta.Normalize()

		// Compute the precedence only AFTER normalizing namespaces since the
		// namespaces are factored into the calculation.
		src.Precedence = computeIntentionPrecedence(e, src)

		if legacyWrite {
			// We always force meta to be non-nil so that it's an empty map. This
			// makes it easy for API responses to not nil-check this everywhere.
			if src.LegacyMeta == nil {
				src.LegacyMeta = make(map[string]string)
			}
		} else {
			// Legacy fields are cleared, except LegacyMeta which we leave
			// populated so that we can later fail the write in Validate() and
			// give the user a warning about possible data loss.
			src.LegacyID = ""
			src.LegacyCreateTime = nil
			src.LegacyUpdateTime = nil
		}

		for _, perm := range src.Permissions {
			if perm.HTTP == nil {
				continue
			}

			for j := 0; j < len(perm.HTTP.Methods); j++ {
				perm.HTTP.Methods[j] = strings.ToUpper(perm.HTTP.Methods[j])
			}
		}
	}

	// The source intentions closer to the head of the list have higher
	// precedence. i.e. index 0 has the highest precedence.
	sort.SliceStable(e.Sources, func(i, j int) bool {
		return e.Sources[i].Precedence > e.Sources[j].Precedence
	})

	return nil
}

func timePointer(t time.Time) *time.Time {
	if t.IsZero() {
		return nil
	}
	return &t
}

// NOTE: this assumes that the namespaces have been fully normalized.
func computeIntentionPrecedence(entry *ServiceIntentionsConfigEntry, src *SourceIntention) int {
	// Max maintains the maximum value that the precedence can be depending
	// on the number of exact values in the destination.
	var max int
	switch intentionCountExact(entry.Name, &entry.EnterpriseMeta) {
	case 2:
		max = 9
	case 1:
		max = 6
	case 0:
		max = 3
	default:
		// This shouldn't be possible, just set it to zero
		return 0
	}
	// Given the maximum, the exact value is determined based on the
	// number of source exact values.
	countSrc := intentionCountExact(src.Name, &src.EnterpriseMeta)
	return max - (2 - countSrc)
}

// intentionCountExact counts the number of exact values (not wildcards) in
// the given namespace and name.
func intentionCountExact(name string, entMeta *EnterpriseMeta) int {
	ns := entMeta.NamespaceOrDefault()

	// If NS is wildcard, pair must be */* since an exact service cannot follow a wildcard NS
	// */* is allowed, but */foo is not
	if ns == WildcardSpecifier {
		return 0
	}

	// only the namespace must be exact, since the */* case already returned.
	if name == WildcardSpecifier {
		return 1
	}

	return 2
}

func (e *ServiceIntentionsConfigEntry) Validate() error {
	return e.validate(false)
}

func (e *ServiceIntentionsConfigEntry) LegacyValidate() error {
	return e.validate(true)
}

func (e *ServiceIntentionsConfigEntry) HasWildcardDestination() bool {
	dstNS := e.EnterpriseMeta.NamespaceOrDefault()
	return dstNS == WildcardSpecifier || e.Name == WildcardSpecifier
}

func (e *ServiceIntentionsConfigEntry) HasAnyPermissions() bool {
	for _, src := range e.Sources {
		if len(src.Permissions) > 0 {
			return true
		}
	}
	return false
}

func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
	if e.Name == "" {
		return fmt.Errorf("Name is required")
	}

	if err := validateIntentionWildcards(e.Name, &e.EnterpriseMeta); err != nil {
		return err
	}

	destIsWild := e.HasWildcardDestination()

	if legacyWrite {
		if len(e.Meta) > 0 {
			return fmt.Errorf("Meta must be omitted for legacy intention writes")
		}
	} else {
		if err := validateConfigEntryMeta(e.Meta); err != nil {
			return err
		}
	}

	if len(e.Sources) == 0 {
		return fmt.Errorf("At least one source is required")
	}

	seenSources := make(map[ServiceName]struct{})
	for i, src := range e.Sources {
		if src.Name == "" {
			return fmt.Errorf("Sources[%d].Name is required", i)
		}

		if err := validateIntentionWildcards(src.Name, &src.EnterpriseMeta); err != nil {
			return fmt.Errorf("Sources[%d].%v", i, err)
		}

		if err := validateSourceIntentionEnterpriseMeta(&src.EnterpriseMeta); err != nil {
			return fmt.Errorf("Sources[%d].%v", i, err)
		}

		// Length of opaque values
		if len(src.Description) > metaValueMaxLength {
			return fmt.Errorf(
				"Sources[%d].Description exceeds maximum length %d", i, metaValueMaxLength)
		}

		if legacyWrite {
			if len(src.LegacyMeta) > metaMaxKeyPairs {
				return fmt.Errorf(
					"Sources[%d].Meta exceeds maximum element count %d", i, metaMaxKeyPairs)
			}
			for k, v := range src.LegacyMeta {
				if len(k) > metaKeyMaxLength {
					return fmt.Errorf(
						"Sources[%d].Meta key %q exceeds maximum length %d",
						i, k, metaKeyMaxLength,
					)
				}
				if len(v) > metaValueMaxLength {
					return fmt.Errorf(
						"Sources[%d].Meta value for key %q exceeds maximum length %d",
						i, k, metaValueMaxLength,
					)
				}
			}

			if src.LegacyCreateTime == nil {
				return fmt.Errorf("Sources[%d].LegacyCreateTime must be set", i)
			}
			if src.LegacyUpdateTime == nil {
				return fmt.Errorf("Sources[%d].LegacyUpdateTime must be set", i)
			}
		} else {
			if len(src.LegacyMeta) > 0 {
				return fmt.Errorf("Sources[%d].LegacyMeta must be omitted", i)
			}
			src.LegacyMeta = nil // ensure it's completely unset

			if src.LegacyCreateTime != nil {
				return fmt.Errorf("Sources[%d].LegacyCreateTime must be omitted", i)
			}
			if src.LegacyUpdateTime != nil {
				return fmt.Errorf("Sources[%d].LegacyUpdateTime must be omitted", i)
			}
		}

		if legacyWrite {
			if src.LegacyID == "" {
				return fmt.Errorf("Sources[%d].LegacyID must be set", i)
			}
		} else {
			if src.LegacyID != "" {
				return fmt.Errorf("Sources[%d].LegacyID must be omitted", i)
			}
		}

		if legacyWrite || len(src.Permissions) == 0 {
			switch src.Action {
			case IntentionActionAllow, IntentionActionDeny:
			default:
				return fmt.Errorf("Sources[%d].Action must be set to 'allow' or 'deny'", i)
			}
		}

		if len(src.Permissions) > 0 && src.Action != "" {
			return fmt.Errorf("Sources[%d].Action must be omitted if Permissions are specified", i)
		}

		if destIsWild && len(src.Permissions) > 0 {
			return fmt.Errorf("Sources[%d].Permissions cannot be specified on intentions with wildcarded destinations", i)
		}

		switch src.Type {
		case IntentionSourceConsul:
		default:
			return fmt.Errorf("Sources[%d].Type must be set to 'consul'", i)
		}

		for j, perm := range src.Permissions {
			switch perm.Action {
			case IntentionActionAllow, IntentionActionDeny:
			default:
				return fmt.Errorf("Sources[%d].Permissions[%d].Action must be set to 'allow' or 'deny'", i, j)
			}

			errorPrefix := "Sources[%d].Permissions[%d].HTTP"
			if perm.HTTP == nil {
				return fmt.Errorf(errorPrefix+" is required", i, j)
			}

			pathParts := 0
			if perm.HTTP.PathExact != "" {
				pathParts++
				if !strings.HasPrefix(perm.HTTP.PathExact, "/") {
					return fmt.Errorf(
						errorPrefix+".PathExact doesn't start with '/': %q",
						i, j, perm.HTTP.PathExact,
					)
				}
			}
			if perm.HTTP.PathPrefix != "" {
				pathParts++
				if !strings.HasPrefix(perm.HTTP.PathPrefix, "/") {
					return fmt.Errorf(
						errorPrefix+".PathPrefix doesn't start with '/': %q",
						i, j, perm.HTTP.PathPrefix,
					)
				}
			}
			if perm.HTTP.PathRegex != "" {
				pathParts++
			}
			if pathParts > 1 {
				return fmt.Errorf(
					errorPrefix+" should only contain at most one of PathExact, PathPrefix, or PathRegex",
					i, j,
				)
			}

			permParts := pathParts

			for k, hdr := range perm.HTTP.Header {
				if hdr.Name == "" {
					return fmt.Errorf(errorPrefix+".Header[%d] missing required Name field", i, j, k)
				}
				hdrParts := 0
				if hdr.Present {
					hdrParts++
				}
				if hdr.Exact != "" {
					hdrParts++
				}
				if hdr.Regex != "" {
					hdrParts++
				}
				if hdr.Prefix != "" {
					hdrParts++
				}
				if hdr.Suffix != "" {
					hdrParts++
				}
				if hdrParts != 1 {
					return fmt.Errorf(errorPrefix+".Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex", i, j, k)
				}
				permParts++
			}

			if len(perm.HTTP.Methods) > 0 {
				found := make(map[string]struct{})
				for _, m := range perm.HTTP.Methods {
					if !isValidHTTPMethod(m) {
						return fmt.Errorf(errorPrefix+".Methods contains an invalid method %q", i, j, m)
					}
					if _, ok := found[m]; ok {
						return fmt.Errorf(errorPrefix+".Methods contains %q more than once", i, j, m)
					}
					found[m] = struct{}{}
				}
				permParts++
			}

			if permParts == 0 {
				return fmt.Errorf(errorPrefix+" should not be empty", i, j)
			}
		}

		serviceName := src.SourceServiceName()
		if _, exists := seenSources[serviceName]; exists {
			return fmt.Errorf("Sources[%d] defines %q more than once", i, serviceName.String())
		}
		seenSources[serviceName] = struct{}{}
	}

	return nil
}

// Wildcard usage verification
func validateIntentionWildcards(name string, entMeta *EnterpriseMeta) error {
	ns := entMeta.NamespaceOrDefault()
	if ns != WildcardSpecifier {
		if strings.Contains(ns, WildcardSpecifier) {
			return fmt.Errorf("Namespace: wildcard character '*' cannot be used with partial values")
		}
	}
	if name != WildcardSpecifier {
		if strings.Contains(name, WildcardSpecifier) {
			return fmt.Errorf("Name: wildcard character '*' cannot be used with partial values")
		}

		if ns == WildcardSpecifier {
			return fmt.Errorf("Name: exact value cannot follow wildcard namespace")
		}
	}
	return nil
}

func (e *ServiceIntentionsConfigEntry) GetRaftIndex() *RaftIndex {
	if e == nil {
		return &RaftIndex{}
	}

	return &e.RaftIndex
}

func (e *ServiceIntentionsConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
	if e == nil {
		return nil
	}

	return &e.EnterpriseMeta
}

func (e *ServiceIntentionsConfigEntry) CanRead(authz acl.Authorizer) bool {
	var authzContext acl.AuthorizerContext
	e.FillAuthzContext(&authzContext)
	return authz.IntentionRead(e.GetName(), &authzContext) == acl.Allow
}

func (e *ServiceIntentionsConfigEntry) CanWrite(authz acl.Authorizer) bool {
	var authzContext acl.AuthorizerContext
	e.FillAuthzContext(&authzContext)
	return authz.IntentionWrite(e.GetName(), &authzContext) == acl.Allow
}

func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry {
	if len(ixns) == 0 {
		return nil
	}
	collated := make(map[ServiceName]*ServiceIntentionsConfigEntry)
	for _, ixn := range ixns {
		thisEntry := ixn.ToConfigEntry(true)
		sn := thisEntry.DestinationServiceName()

		if entry, ok := collated[sn]; ok {
			entry.Sources = append(entry.Sources, thisEntry.Sources...)
		} else {
			collated[sn] = thisEntry
		}
	}

	out := make([]*ServiceIntentionsConfigEntry, 0, len(collated))
	for _, entry := range collated {
		out = append(out, entry)
	}
	return out
}
