// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package interpreter

import (
	"github.com/google/cel-go/common/containers"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/common/types/ref"
)

// AttributePattern represents a top-level variable with an optional set of qualifier patterns.
//
// When using a CEL expression within a container, e.g. a package or namespace, the variable name
// in the pattern must match the qualified name produced during the variable namespace resolution.
// For example, if variable `c` appears in an expression whose container is `a.b`, the variable
// name supplied to the pattern must be `a.b.c`
//
// The qualifier patterns for attribute matching must be one of the following:
//
//   - valid map key type: string, int, uint, bool
//   - wildcard (*)
//
// Examples:
//
//  1. ns.myvar["complex-value"]
//  2. ns.myvar["complex-value"][0]
//  3. ns.myvar["complex-value"].*.name
//
// The first example is simple: match an attribute where the variable is 'ns.myvar' with a
// field access on 'complex-value'. The second example expands the match to indicate that only
// a specific index `0` should match. And lastly, the third example matches any indexed access
// that later selects the 'name' field.
type AttributePattern struct {
	variable          string
	qualifierPatterns []*AttributeQualifierPattern
}

// NewAttributePattern produces a new mutable AttributePattern based on a variable name.
func NewAttributePattern(variable string) *AttributePattern {
	return &AttributePattern{
		variable:          variable,
		qualifierPatterns: []*AttributeQualifierPattern{},
	}
}

// QualString adds a string qualifier pattern to the AttributePattern. The string may be a valid
// identifier, or string map key including empty string.
func (apat *AttributePattern) QualString(pattern string) *AttributePattern {
	apat.qualifierPatterns = append(apat.qualifierPatterns,
		&AttributeQualifierPattern{value: pattern})
	return apat
}

// QualInt adds an int qualifier pattern to the AttributePattern. The index may be either a map or
// list index.
func (apat *AttributePattern) QualInt(pattern int64) *AttributePattern {
	apat.qualifierPatterns = append(apat.qualifierPatterns,
		&AttributeQualifierPattern{value: pattern})
	return apat
}

// QualUint adds an uint qualifier pattern for a map index operation to the AttributePattern.
func (apat *AttributePattern) QualUint(pattern uint64) *AttributePattern {
	apat.qualifierPatterns = append(apat.qualifierPatterns,
		&AttributeQualifierPattern{value: pattern})
	return apat
}

// QualBool adds a bool qualifier pattern for a map index operation to the AttributePattern.
func (apat *AttributePattern) QualBool(pattern bool) *AttributePattern {
	apat.qualifierPatterns = append(apat.qualifierPatterns,
		&AttributeQualifierPattern{value: pattern})
	return apat
}

// Wildcard adds a special sentinel qualifier pattern that will match any single qualifier.
func (apat *AttributePattern) Wildcard() *AttributePattern {
	apat.qualifierPatterns = append(apat.qualifierPatterns,
		&AttributeQualifierPattern{wildcard: true})
	return apat
}

// VariableMatches returns true if the fully qualified variable matches the AttributePattern
// fully qualified variable name.
func (apat *AttributePattern) VariableMatches(variable string) bool {
	return apat.variable == variable
}

// QualifierPatterns returns the set of AttributeQualifierPattern values on the AttributePattern.
func (apat *AttributePattern) QualifierPatterns() []*AttributeQualifierPattern {
	return apat.qualifierPatterns
}

// AttributeQualifierPattern holds a wildcard or valued qualifier pattern.
type AttributeQualifierPattern struct {
	wildcard bool
	value    any
}

// Matches returns true if the qualifier pattern is a wildcard, or the Qualifier implements the
// qualifierValueEquator interface and its IsValueEqualTo returns true for the qualifier pattern.
func (qpat *AttributeQualifierPattern) Matches(q Qualifier) bool {
	if qpat.wildcard {
		return true
	}
	qve, ok := q.(qualifierValueEquator)
	return ok && qve.QualifierValueEquals(qpat.value)
}

// qualifierValueEquator defines an interface for determining if an input value, of valid map key
// type, is equal to the value held in the Qualifier. This interface is used by the
// AttributeQualifierPattern to determine pattern matches for non-wildcard qualifier patterns.
//
// Note: Attribute values are also Qualifier values; however, Attributes are resolved before
// qualification happens. This is an implementation detail, but one relevant to why the Attribute
// types do not surface in the list of implementations.
//
// See: partialAttributeFactory.matchesUnknownPatterns for more details on how this interface is
// used.
type qualifierValueEquator interface {
	// QualifierValueEquals returns true if the input value is equal to the value held in the
	// Qualifier.
	QualifierValueEquals(value any) bool
}

// QualifierValueEquals implementation for boolean qualifiers.
func (q *boolQualifier) QualifierValueEquals(value any) bool {
	bval, ok := value.(bool)
	return ok && q.value == bval
}

// QualifierValueEquals implementation for field qualifiers.
func (q *fieldQualifier) QualifierValueEquals(value any) bool {
	sval, ok := value.(string)
	return ok && q.Name == sval
}

// QualifierValueEquals implementation for string qualifiers.
func (q *stringQualifier) QualifierValueEquals(value any) bool {
	sval, ok := value.(string)
	return ok && q.value == sval
}

// QualifierValueEquals implementation for int qualifiers.
func (q *intQualifier) QualifierValueEquals(value any) bool {
	return numericValueEquals(value, q.celValue)
}

// QualifierValueEquals implementation for uint qualifiers.
func (q *uintQualifier) QualifierValueEquals(value any) bool {
	return numericValueEquals(value, q.celValue)
}

// QualifierValueEquals implementation for double qualifiers.
func (q *doubleQualifier) QualifierValueEquals(value any) bool {
	return numericValueEquals(value, q.celValue)
}

// numericValueEquals uses CEL equality to determine whether two number values are
func numericValueEquals(value any, celValue ref.Val) bool {
	val := types.DefaultTypeAdapter.NativeToValue(value)
	return celValue.Equal(val) == types.True
}

// NewPartialAttributeFactory returns an AttributeFactory implementation capable of performing
// AttributePattern matches with PartialActivation inputs.
func NewPartialAttributeFactory(container *containers.Container,
	adapter ref.TypeAdapter,
	provider ref.TypeProvider) AttributeFactory {
	fac := NewAttributeFactory(container, adapter, provider)
	return &partialAttributeFactory{
		AttributeFactory: fac,
		container:        container,
		adapter:          adapter,
		provider:         provider,
	}
}

type partialAttributeFactory struct {
	AttributeFactory
	container *containers.Container
	adapter   ref.TypeAdapter
	provider  ref.TypeProvider
}

// AbsoluteAttribute implementation of the AttributeFactory interface which wraps the
// NamespacedAttribute resolution in an internal attributeMatcher object to dynamically match
// unknown patterns from PartialActivation inputs if given.
func (fac *partialAttributeFactory) AbsoluteAttribute(id int64, names ...string) NamespacedAttribute {
	attr := fac.AttributeFactory.AbsoluteAttribute(id, names...)
	return &attributeMatcher{fac: fac, NamespacedAttribute: attr}
}

// MaybeAttribute implementation of the AttributeFactory interface which ensure that the set of
// 'maybe' NamespacedAttribute values are produced using the partialAttributeFactory rather than
// the base AttributeFactory implementation.
func (fac *partialAttributeFactory) MaybeAttribute(id int64, name string) Attribute {
	return &maybeAttribute{
		id: id,
		attrs: []NamespacedAttribute{
			fac.AbsoluteAttribute(id, fac.container.ResolveCandidateNames(name)...),
		},
		adapter:  fac.adapter,
		provider: fac.provider,
		fac:      fac,
	}
}

// matchesUnknownPatterns returns true if the variable names and qualifiers for a given
// Attribute value match any of the ActivationPattern objects in the set of unknown activation
// patterns on the given PartialActivation.
//
// For example, in the expression `a.b`, the Attribute is composed of variable `a`, with string
// qualifier `b`. When a PartialActivation is supplied, it indicates that some or all of the data
// provided in the input is unknown by specifying unknown AttributePatterns. An AttributePattern
// that refers to variable `a` with a string qualifier of `c` will not match `a.b`; however, any
// of the following patterns will match Attribute `a.b`:
//
// - `AttributePattern("a")`
// - `AttributePattern("a").Wildcard()`
// - `AttributePattern("a").QualString("b")`
// - `AttributePattern("a").QualString("b").QualInt(0)`
//
// Any AttributePattern which overlaps an Attribute or vice-versa will produce an Unknown result
// for the last pattern matched variable or qualifier in the Attribute. In the first matching
// example, the expression id representing variable `a` would be listed in the Unknown result,
// whereas in the other pattern examples, the qualifier `b` would be returned as the Unknown.
func (fac *partialAttributeFactory) matchesUnknownPatterns(
	vars PartialActivation,
	attrID int64,
	variableNames []string,
	qualifiers []Qualifier) (types.Unknown, error) {
	patterns := vars.UnknownAttributePatterns()
	candidateIndices := map[int]struct{}{}
	for _, variable := range variableNames {
		for i, pat := range patterns {
			if pat.VariableMatches(variable) {
				candidateIndices[i] = struct{}{}
			}
		}
	}
	// Determine whether to return early if there are no candidate unknown patterns.
	if len(candidateIndices) == 0 {
		return nil, nil
	}
	// Determine whether to return early if there are no qualifiers.
	if len(qualifiers) == 0 {
		return types.Unknown{attrID}, nil
	}
	// Resolve the attribute qualifiers into a static set. This prevents more dynamic
	// Attribute resolutions than necessary when there are multiple unknown patterns
	// that traverse the same Attribute-based qualifier field.
	newQuals := make([]Qualifier, len(qualifiers))
	for i, qual := range qualifiers {
		attr, isAttr := qual.(Attribute)
		if isAttr {
			val, err := attr.Resolve(vars)
			if err != nil {
				return nil, err
			}
			// If this resolution behavior ever changes, new implementations of the
			// qualifierValueEquator may be required to handle proper resolution.
			qual, err = fac.NewQualifier(nil, qual.ID(), val, attr.IsOptional())
			if err != nil {
				return nil, err
			}
		}
		newQuals[i] = qual
	}
	// Determine whether any of the unknown patterns match.
	for patIdx := range candidateIndices {
		pat := patterns[patIdx]
		isUnk := true
		matchExprID := attrID
		qualPats := pat.QualifierPatterns()
		for i, qual := range newQuals {
			if i >= len(qualPats) {
				break
			}
			matchExprID = qual.ID()
			qualPat := qualPats[i]
			// Note, the AttributeQualifierPattern relies on the input Qualifier not being an
			// Attribute, since there is no way to resolve the Attribute with the information
			// provided to the Matches call.
			if !qualPat.Matches(qual) {
				isUnk = false
				break
			}
		}
		if isUnk {
			return types.Unknown{matchExprID}, nil
		}
	}
	return nil, nil
}

// attributeMatcher embeds the NamespacedAttribute interface which allows it to participate in
// AttributePattern matching against Attribute values without having to modify the code paths that
// identify Attributes in expressions.
type attributeMatcher struct {
	NamespacedAttribute
	qualifiers []Qualifier
	fac        *partialAttributeFactory
}

// AddQualifier implements the Attribute interface method.
func (m *attributeMatcher) AddQualifier(qual Qualifier) (Attribute, error) {
	// Add the qualifier to the embedded NamespacedAttribute. If the input to the Resolve
	// method is not a PartialActivation, or does not match an unknown attribute pattern, the
	// Resolve method is directly invoked on the underlying NamespacedAttribute.
	_, err := m.NamespacedAttribute.AddQualifier(qual)
	if err != nil {
		return nil, err
	}
	// The attributeMatcher overloads TryResolve and will attempt to match unknown patterns against
	// the variable name and qualifier set contained within the Attribute. These values are not
	// directly inspectable on the top-level NamespacedAttribute interface and so are tracked within
	// the attributeMatcher.
	m.qualifiers = append(m.qualifiers, qual)
	return m, nil
}

// Resolve is an implementation of the NamespacedAttribute interface method which tests
// for matching unknown attribute patterns and returns types.Unknown if present. Otherwise,
// the standard Resolve logic applies.
func (m *attributeMatcher) Resolve(vars Activation) (any, error) {
	id := m.NamespacedAttribute.ID()
	// Bug in how partial activation is resolved, should search parents as well.
	partial, isPartial := toPartialActivation(vars)
	if isPartial {
		unk, err := m.fac.matchesUnknownPatterns(
			partial,
			id,
			m.CandidateVariableNames(),
			m.qualifiers)
		if err != nil {
			return nil, err
		}
		if unk != nil {
			return unk, nil
		}
	}
	return m.NamespacedAttribute.Resolve(vars)
}

// Qualify is an implementation of the Qualifier interface method.
func (m *attributeMatcher) Qualify(vars Activation, obj any) (any, error) {
	return attrQualify(m.fac, vars, obj, m)
}

// QualifyIfPresent is an implementation of the Qualifier interface method.
func (m *attributeMatcher) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) {
	return attrQualifyIfPresent(m.fac, vars, obj, m, presenceOnly)
}

func toPartialActivation(vars Activation) (PartialActivation, bool) {
	pv, ok := vars.(PartialActivation)
	if ok {
		return pv, true
	}
	if vars.Parent() != nil {
		return toPartialActivation(vars.Parent())
	}
	return nil, false
}
