package structs

import (
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-multierror"
	"github.com/mitchellh/hashstructure"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/cache"
	"github.com/hashicorp/consul/lib"

	"golang.org/x/crypto/blake2b"
)

const (
	// IntentionDefaultNamespace is the default namespace value.
	// NOTE(mitchellh): This is only meant to be a temporary constant.
	// When namespaces are introduced, we should delete this constant and
	// fix up all the places where this was used with the proper namespace
	// value.
	IntentionDefaultNamespace = "default"
)

// Intention defines an intention for the Connect Service Graph. This defines
// the allowed or denied behavior of a connection between two services using
// Connect.
type Intention struct {
	// ID is the UUID-based ID for the intention, always generated by Consul.
	ID string `json:",omitempty"`

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

	// SourceNS, SourceName are the namespace and name, respectively, of
	// the source service. Either of these may be the 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.
	//
	// DestinationNS, DestinationName is the same, but for the destination
	// service. The same rules apply. The destination is always a Consul
	// service.
	SourceNS, SourceName           string
	DestinationNS, DestinationName string

	// SourceType is the type of the value for the source.
	SourceType IntentionSourceType

	// Action is whether this is an allowlist or denylist intention.
	Action IntentionAction `json:",omitempty"`

	// Permissions is the list of additional L7 attributes that extend the
	// intention definition.
	//
	// NOTE: This field is not editable unless editing the underlying
	// service-intentions config entry directly.
	Permissions []*IntentionPermission `bexpr:"-" json:",omitempty"`

	// DefaultAddr is not used.
	// Deprecated: DefaultAddr is not used and may be removed in a future version.
	DefaultAddr string `bexpr:"-" codec:",omitempty" json:",omitempty"`
	// DefaultPort is not used.
	// Deprecated: DefaultPort is not used and may be removed in a future version.
	DefaultPort int `bexpr:"-" codec:",omitempty" json:",omitempty"`

	// Meta is arbitrary metadata associated with the intention. This is
	// opaque to Consul but is served in API responses.
	Meta map[string]string `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.
	Precedence int

	// CreatedAt and UpdatedAt keep track of when this record was created
	// or modified.
	CreatedAt, UpdatedAt time.Time `mapstructure:"-" bexpr:"-"`

	// Hash of the contents of the intention. This is only necessary for legacy
	// intention replication purposes.
	//
	// This is needed mainly for legacy replication purposes. When replicating
	// from one DC to another keeping the content Hash will allow us to detect
	// content changes more efficiently than checking every single field
	Hash []byte `bexpr:"-" json:",omitempty"`

	RaftIndex `bexpr:"-"`
}

func (t *Intention) Clone() *Intention {
	t2 := *t
	if len(t.Permissions) > 0 {
		t2.Permissions = make([]*IntentionPermission, 0, len(t.Permissions))
		for _, perm := range t.Permissions {
			t2.Permissions = append(t2.Permissions, perm.Clone())
		}
	}
	t2.Meta = cloneStringStringMap(t.Meta)
	t2.Hash = nil
	return &t2
}

func (t *Intention) ToExact() *IntentionQueryExact {
	return &IntentionQueryExact{
		SourceNS:        t.SourceNS,
		SourceName:      t.SourceName,
		DestinationNS:   t.DestinationNS,
		DestinationName: t.DestinationName,
	}
}

func (t *Intention) MarshalJSON() ([]byte, error) {
	type Alias Intention
	exported := &struct {
		CreatedAt, UpdatedAt *time.Time `json:",omitempty"`
		*Alias
	}{
		Alias: (*Alias)(t),
	}
	if !t.CreatedAt.IsZero() {
		exported.CreatedAt = &t.CreatedAt
	}
	if !t.UpdatedAt.IsZero() {
		exported.UpdatedAt = &t.UpdatedAt
	}
	return json.Marshal(exported)
}

func (t *Intention) UnmarshalJSON(data []byte) (err error) {
	type Alias Intention
	aux := &struct {
		Hash                 string
		CreatedAt, UpdatedAt string // effectively `json:"-"` on CreatedAt and UpdatedAt

		*Alias
	}{
		Alias: (*Alias)(t),
	}
	if err = lib.UnmarshalJSON(data, &aux); err != nil {
		return err
	}

	if aux.Hash != "" {
		t.Hash = []byte(aux.Hash)
	}
	return nil
}

// SetHash calculates Intention.Hash from any mutable "content" fields.
//
// The Hash is primarily used for legacy intention replication to determine if
// an intention has changed and should be updated locally.
//
// Deprecated: this is only used for legacy intention CRUD and replication
func (x *Intention) SetHash() {
	hash, err := blake2b.New256(nil)
	if err != nil {
		panic(err)
	}

	// Write all the user set fields
	hash.Write([]byte(x.ID))
	hash.Write([]byte(x.Description))
	hash.Write([]byte(x.SourceNS))
	hash.Write([]byte(x.SourceName))
	hash.Write([]byte(x.DestinationNS))
	hash.Write([]byte(x.DestinationName))
	hash.Write([]byte(x.SourceType))
	hash.Write([]byte(x.Action))
	// hash.Write can not return an error, so the only way for binary.Write to
	// error is to pass it data with an invalid data type. Doing so would be a
	// programming error, so panic in that case.
	if err := binary.Write(hash, binary.LittleEndian, uint64(x.Precedence)); err != nil {
		panic(err)
	}

	// sort keys to ensure hash stability when meta is stored later
	var keys []string
	for k := range x.Meta {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		hash.Write([]byte(k))
		hash.Write([]byte(x.Meta[k]))
	}

	x.Hash = hash.Sum(nil)
}

// Validate returns an error if the intention is invalid for inserting
// or updating via the legacy APIs.
//
// Deprecated: this is only used for legacy intention CRUD
func (x *Intention) Validate() error {
	var result error

	// Empty values
	if x.SourceNS == "" {
		result = multierror.Append(result, fmt.Errorf("SourceNS must be set"))
	}
	if x.SourceName == "" {
		result = multierror.Append(result, fmt.Errorf("SourceName must be set"))
	}
	if x.DestinationNS == "" {
		result = multierror.Append(result, fmt.Errorf("DestinationNS must be set"))
	}
	if x.DestinationName == "" {
		result = multierror.Append(result, fmt.Errorf("DestinationName must be set"))
	}

	// Wildcard usage verification
	if x.SourceNS != WildcardSpecifier {
		if strings.Contains(x.SourceNS, WildcardSpecifier) {
			result = multierror.Append(result, fmt.Errorf(
				"SourceNS: wildcard character '*' cannot be used with partial values"))
		}
	}
	if x.SourceName != WildcardSpecifier {
		if strings.Contains(x.SourceName, WildcardSpecifier) {
			result = multierror.Append(result, fmt.Errorf(
				"SourceName: wildcard character '*' cannot be used with partial values"))
		}

		if x.SourceNS == WildcardSpecifier {
			result = multierror.Append(result, fmt.Errorf(
				"SourceName: exact value cannot follow wildcard namespace"))
		}
	}
	if x.DestinationNS != WildcardSpecifier {
		if strings.Contains(x.DestinationNS, WildcardSpecifier) {
			result = multierror.Append(result, fmt.Errorf(
				"DestinationNS: wildcard character '*' cannot be used with partial values"))
		}
	}
	if x.DestinationName != WildcardSpecifier {
		if strings.Contains(x.DestinationName, WildcardSpecifier) {
			result = multierror.Append(result, fmt.Errorf(
				"DestinationName: wildcard character '*' cannot be used with partial values"))
		}

		if x.DestinationNS == WildcardSpecifier {
			result = multierror.Append(result, fmt.Errorf(
				"DestinationName: exact value cannot follow wildcard namespace"))
		}
	}

	// Length of opaque values
	if len(x.Description) > metaValueMaxLength {
		result = multierror.Append(result, fmt.Errorf(
			"Description exceeds maximum length %d", metaValueMaxLength))
	}
	if len(x.Meta) > metaMaxKeyPairs {
		result = multierror.Append(result, fmt.Errorf(
			"Meta exceeds maximum element count %d", metaMaxKeyPairs))
	}
	for k, v := range x.Meta {
		if len(k) > metaKeyMaxLength {
			result = multierror.Append(result, fmt.Errorf(
				"Meta key %q exceeds maximum length %d", k, metaKeyMaxLength))
		}
		if len(v) > metaValueMaxLength {
			result = multierror.Append(result, fmt.Errorf(
				"Meta value for key %q exceeds maximum length %d", k, metaValueMaxLength))
		}
	}

	switch x.Action {
	case IntentionActionAllow, IntentionActionDeny:
	default:
		result = multierror.Append(result, fmt.Errorf(
			"Action must be set to 'allow' or 'deny'"))
	}

	if len(x.Permissions) > 0 {
		result = multierror.Append(result, fmt.Errorf(
			"Permissions must not be set when using the legacy APIs"))
	}

	switch x.SourceType {
	case IntentionSourceConsul:
	default:
		result = multierror.Append(result, fmt.Errorf(
			"SourceType must be set to 'consul'"))
	}

	return result
}

func (ixn *Intention) CanRead(authz acl.Authorizer) bool {
	if authz == nil {
		return true
	}
	var authzContext acl.AuthorizerContext

	// Read access on either end of the intention allows you to read the
	// complete intention. This is so that both ends can be aware of why
	// something does or does not work.

	if ixn.SourceName != "" {
		ixn.FillAuthzContext(&authzContext, false)
		if authz.IntentionRead(ixn.SourceName, &authzContext) == acl.Allow {
			return true
		}
	}

	if ixn.DestinationName != "" {
		ixn.FillAuthzContext(&authzContext, true)
		if authz.IntentionRead(ixn.DestinationName, &authzContext) == acl.Allow {
			return true
		}
	}

	return false
}

func (ixn *Intention) CanWrite(authz acl.Authorizer) bool {
	if authz == nil {
		return true
	}
	var authzContext acl.AuthorizerContext

	if ixn.DestinationName == "" {
		return false
	}

	ixn.FillAuthzContext(&authzContext, true)
	return authz.IntentionWrite(ixn.DestinationName, &authzContext) == acl.Allow
}

// UpdatePrecedence sets the Precedence value based on the fields of this
// structure.
//
// Deprecated: this is only used for legacy intention CRUD.
func (x *Intention) UpdatePrecedence() {
	// Max maintains the maximum value that the precedence can be depending
	// on the number of exact values in the destination.
	var max int
	switch x.countExact(x.DestinationNS, x.DestinationName) {
	case 2:
		max = 9
	case 1:
		max = 6
	case 0:
		max = 3
	default:
		// This shouldn't be possible, just set it to zero
		x.Precedence = 0
		return
	}

	// Given the maximum, the exact value is determined based on the
	// number of source exact values.
	countSrc := x.countExact(x.SourceNS, x.SourceName)
	x.Precedence = max - (2 - countSrc)
}

// countExact counts the number of exact values (not wildcards) in
// the given namespace and name.
func (x *Intention) countExact(ns, n string) int {
	// If NS is wildcard, it must be zero since wildcards only follow exact
	if ns == WildcardSpecifier {
		return 0
	}

	// Same reasoning as above, a wildcard can only follow an exact value
	// and an exact value cannot follow a wildcard, so if name is a wildcard
	// we must have exactly one.
	if n == WildcardSpecifier {
		return 1
	}

	return 2
}

// String returns a human-friendly string for this intention.
func (x *Intention) String() string {
	var idPart string
	if x.ID != "" {
		idPart = "ID: " + x.ID + ", "
	}

	var detailPart string
	if len(x.Permissions) > 0 {
		detailPart = fmt.Sprintf("Permissions: %d", len(x.Permissions))
	} else {
		detailPart = "Action: " + strings.ToUpper(string(x.Action))
	}

	return fmt.Sprintf("%s/%s => %s/%s (%sPrecedence: %d, %s)",
		x.SourceNS, x.SourceName,
		x.DestinationNS, x.DestinationName,
		idPart,
		x.Precedence,
		detailPart,
	)
}

// LegacyEstimateSize returns an estimate (in bytes) of the size of this structure when encoded.
//
// Deprecated: only exists for legacy intention replication during migration to 1.9.0+ cluster.
func (x *Intention) LegacyEstimateSize() int {
	// 56 = 36 (uuid) + 16 (RaftIndex) + 4 (Precedence)
	size := 56 + len(x.Description) + len(x.SourceNS) + len(x.SourceName) + len(x.DestinationNS) +
		len(x.DestinationName) + len(x.SourceType) + len(x.Action)

	for k, v := range x.Meta {
		size += len(k) + len(v)
	}

	return size
}

func (x *Intention) SourceServiceName() ServiceName {
	return NewServiceName(x.SourceName, x.SourceEnterpriseMeta())
}

func (x *Intention) DestinationServiceName() ServiceName {
	return NewServiceName(x.DestinationName, x.DestinationEnterpriseMeta())
}

// NOTE this is just used to manipulate user-provided data before an insert
// The RPC execution will do Normalize + Validate for us.
func (x *Intention) ToConfigEntry(legacy bool) *ServiceIntentionsConfigEntry {
	return &ServiceIntentionsConfigEntry{
		Kind:           ServiceIntentions,
		Name:           x.DestinationName,
		EnterpriseMeta: *x.DestinationEnterpriseMeta(),
		Sources:        []*SourceIntention{x.ToSourceIntention(legacy)},
	}
}

func (x *Intention) ToSourceIntention(legacy bool) *SourceIntention {
	ct := x.CreatedAt // copy
	ut := x.UpdatedAt

	src := &SourceIntention{
		Name:             x.SourceName,
		EnterpriseMeta:   *x.SourceEnterpriseMeta(),
		Action:           x.Action,
		Permissions:      nil, // explicitly not symmetric with the old APIs
		Precedence:       0,   // Ignore, let it be computed.
		LegacyID:         x.ID,
		Type:             x.SourceType,
		Description:      x.Description,
		LegacyMeta:       x.Meta,
		LegacyCreateTime: &ct,
		LegacyUpdateTime: &ut,
	}
	if !legacy {
		src.Permissions = x.Permissions
	}
	return src
}

// IntentionAction is the action that the intention represents. This
// can be "allow" or "deny".
type IntentionAction string

const (
	IntentionActionAllow IntentionAction = "allow"
	IntentionActionDeny  IntentionAction = "deny"
)

// IntentionSourceType is the type of the source within an intention.
type IntentionSourceType string

const (
	// IntentionSourceConsul is a service within the Consul catalog.
	IntentionSourceConsul IntentionSourceType = "consul"
)

// Intentions is a list of intentions.
type Intentions []*Intention

// IndexedIntentions represents a list of intentions for RPC responses.
type IndexedIntentions struct {
	Intentions Intentions

	// DataOrigin is used to indicate if this query was satisfied against the
	// old legacy intentions ("legacy") memdb table or via config entries
	// ("config"). This is really only of value for the legacy intention
	// replication routine to correctly detect that it should exit.
	DataOrigin string `json:"-"`
	QueryMeta
}

const (
	IntentionDataOriginLegacy        = "legacy"
	IntentionDataOriginConfigEntries = "config"
)

// IndexedIntentionMatches represents the list of matches for a match query.
type IndexedIntentionMatches struct {
	Matches []Intentions
	QueryMeta
}

// IntentionOp is the operation for a request related to intentions.
type IntentionOp string

const (
	IntentionOpCreate    IntentionOp = "create"
	IntentionOpUpdate    IntentionOp = "update"
	IntentionOpDelete    IntentionOp = "delete"
	IntentionOpDeleteAll IntentionOp = "delete-all" // NOTE: this is only accepted when it comes from the leader, RPCs will reject this
	IntentionOpUpsert    IntentionOp = "upsert"     // config-entry only
)

// IntentionRequest is used to create, update, and delete intentions.
type IntentionRequest struct {
	// Datacenter is the target for this request.
	Datacenter string

	// Op is the type of operation being requested.
	Op IntentionOp

	// Intention is the intention.
	//
	// This is mutually exclusive with the Mutation field.
	Intention *Intention

	// Mutation is a change to make to an Intention.
	//
	// This is mutually exclusive with the Intention field.
	//
	// This field is only set by the leader before writing to the raft log and
	// is not settable via the API or an RPC.
	Mutation *IntentionMutation

	// WriteRequest is a common struct containing ACL tokens and other
	// write-related common elements for requests.
	WriteRequest
}

type IntentionMutation struct {
	ID          string
	Destination ServiceName
	Source      ServiceName
	Value       *SourceIntention
}

// RequestDatacenter returns the datacenter for a given request.
func (q *IntentionRequest) RequestDatacenter() string {
	return q.Datacenter
}

// IntentionMatchType is the target for a match request. For example,
// matching by source will look for all intentions that match the given
// source value.
type IntentionMatchType string

const (
	IntentionMatchSource      IntentionMatchType = "source"
	IntentionMatchDestination IntentionMatchType = "destination"
)

// IntentionQueryRequest is used to query intentions.
type IntentionQueryRequest struct {
	// Datacenter is the target this request is intended for.
	Datacenter string

	// IntentionID is the ID of a specific intention.
	IntentionID string

	// Match is non-nil if we're performing a match query. A match will
	// find intentions that "match" the given parameters. A match includes
	// resolving wildcards.
	Match *IntentionQueryMatch

	// Check is non-nil if we're performing a test query. A test will
	// return allowed/deny based on an exact match.
	Check *IntentionQueryCheck

	// Exact is non-nil if we're performing a lookup of an intention by its
	// unique name instead of its ID.
	Exact *IntentionQueryExact

	// Options for queries
	QueryOptions
}

// RequestDatacenter returns the datacenter for a given request.
func (q *IntentionQueryRequest) RequestDatacenter() string {
	return q.Datacenter
}

// CacheInfo implements cache.Request
func (q *IntentionQueryRequest) CacheInfo() cache.RequestInfo {
	info := cache.RequestInfo{
		Token:      q.Token,
		Datacenter: q.Datacenter,
		MinIndex:   q.MinQueryIndex,
		Timeout:    q.MaxQueryTime,
	}

	v, err := hashstructure.Hash(struct {
		IntentionID string
		Match       *IntentionQueryMatch
		Check       *IntentionQueryCheck
		Exact       *IntentionQueryExact
		Filter      string
	}{
		IntentionID: q.IntentionID,
		Check:       q.Check,
		Match:       q.Match,
		Exact:       q.Exact,
		Filter:      q.QueryOptions.Filter,
	}, nil)
	if err == nil {
		// If there is an error, we don't set the key. A blank key forces
		// no cache for this request so the request is forwarded directly
		// to the server.
		info.Key = strconv.FormatUint(v, 16)
	}

	return info
}

// IntentionQueryMatch are the parameters for performing a match request
// against the state store.
type IntentionQueryMatch struct {
	Type    IntentionMatchType
	Entries []IntentionMatchEntry
}

// IntentionMatchEntry is a single entry for matching an intention.
type IntentionMatchEntry struct {
	Namespace string
	Name      string
}

// IntentionQueryCheck are the parameters for performing a test request.
type IntentionQueryCheck struct {
	// SourceNS, SourceName, DestinationNS, and DestinationName are the
	// source and namespace, respectively, for the test. These must be
	// exact values.
	SourceNS, SourceName           string
	DestinationNS, DestinationName string

	// SourceType is the type of the value for the source.
	SourceType IntentionSourceType
}

// GetACLPrefix returns the prefix to look up the ACL policy for this
// request, and a boolean noting whether the prefix is valid to check
// or not. You must check the ok value before using the prefix.
func (q *IntentionQueryCheck) GetACLPrefix() (string, bool) {
	return q.DestinationName, q.DestinationName != ""
}

// IntentionQueryCheckResponse is the response for a test request.
type IntentionQueryCheckResponse struct {
	Allowed bool
}

// IntentionDecisionSummary contains a summary of a set of intentions between two services
// Currently contains:
// - Whether all actions are allowed
// - Whether the matching intention has L7 permissions attached
// - Whether the intention is managed by an external source like k8s
// - Whether there is an exact, or wildcard, intention referencing the two services
// - Whether ACLs are in DefaultAllow mode
type IntentionDecisionSummary struct {
	Allowed        bool
	HasPermissions bool
	ExternalSource string
	HasExact       bool
	DefaultAllow   bool
}

// IntentionQueryExact holds the parameters for performing a lookup of an
// intention by its unique name instead of its ID.
type IntentionQueryExact struct {
	SourceNS, SourceName           string
	DestinationNS, DestinationName string
}

// Validate is used to ensure all 4 parameters are specified.
func (q *IntentionQueryExact) Validate() error {
	var err error
	if q.SourceNS == "" {
		err = multierror.Append(err, errors.New("SourceNS is missing"))
	}
	if q.SourceName == "" {
		err = multierror.Append(err, errors.New("SourceName is missing"))
	}
	if q.DestinationNS == "" {
		err = multierror.Append(err, errors.New("DestinationNS is missing"))
	}
	if q.DestinationName == "" {
		err = multierror.Append(err, errors.New("DestinationName is missing"))
	}
	return err
}

type IntentionListRequest struct {
	Datacenter     string
	Legacy         bool `json:"-"`
	EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
	QueryOptions
}

func (r *IntentionListRequest) RequestDatacenter() string {
	return r.Datacenter
}

// IntentionPrecedenceSorter takes a list of intentions and sorts them
// based on the match precedence rules for intentions. The intentions
// closer to the head of the list have higher precedence. i.e. index 0 has
// the highest precedence.
type IntentionPrecedenceSorter Intentions

func (s IntentionPrecedenceSorter) Len() int { return len(s) }
func (s IntentionPrecedenceSorter) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

func (s IntentionPrecedenceSorter) Less(i, j int) bool {
	a, b := s[i], s[j]
	if a.Precedence != b.Precedence {
		return a.Precedence > b.Precedence
	}

	// Tie break on lexicographic order of the 4-tuple in canonical form (SrcNS,
	// Src, DstNS, Dst). This is arbitrary but it keeps sorting deterministic
	// which is a nice property for consistency. It is arguably open to abuse if
	// implementations rely on this however by definition the order among
	// same-precedence rules is arbitrary and doesn't affect whether an allow or
	// deny rule is acted on since all applicable rules are checked.
	if a.SourceNS != b.SourceNS {
		return a.SourceNS < b.SourceNS
	}
	if a.SourceName != b.SourceName {
		return a.SourceName < b.SourceName
	}
	if a.DestinationNS != b.DestinationNS {
		return a.DestinationNS < b.DestinationNS
	}
	return a.DestinationName < b.DestinationName
}
