// Copyright 2018 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 containers defines types and functions for resolving qualified names within a namespace
// or type provided to CEL.
package containers

import (
	"fmt"
	"strings"

	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)

var (
	// DefaultContainer has an empty container name.
	DefaultContainer *Container = nil

	// Empty map to search for aliases when needed.
	noAliases = make(map[string]string)
)

// NewContainer creates a new Container with the fully-qualified name.
func NewContainer(opts ...ContainerOption) (*Container, error) {
	var c *Container
	var err error
	for _, opt := range opts {
		c, err = opt(c)
		if err != nil {
			return nil, err
		}
	}
	return c, nil
}

// Container holds a reference to an optional qualified container name and set of aliases.
//
// The program container can be used to simplify variable, function, and type specification within
// CEL programs and behaves more or less like a C++ namespace. See ResolveCandidateNames for more
// details.
type Container struct {
	name    string
	aliases map[string]string
}

// Extend creates a new Container with the existing settings and applies a series of
// ContainerOptions to further configure the new container.
func (c *Container) Extend(opts ...ContainerOption) (*Container, error) {
	if c == nil {
		return NewContainer(opts...)
	}
	// Copy the name and aliases of the existing container.
	ext := &Container{name: c.Name()}
	if len(c.aliasSet()) > 0 {
		aliasSet := make(map[string]string, len(c.aliasSet()))
		for k, v := range c.aliasSet() {
			aliasSet[k] = v
		}
		ext.aliases = aliasSet
	}
	// Apply the new options to the container.
	var err error
	for _, opt := range opts {
		ext, err = opt(ext)
		if err != nil {
			return nil, err
		}
	}
	return ext, nil
}

// Name returns the fully-qualified name of the container.
//
// The name may conceptually be a namespace, package, or type.
func (c *Container) Name() string {
	if c == nil {
		return ""
	}
	return c.name
}

// ResolveCandidateNames returns the candidates name of namespaced identifiers in C++ resolution
// order.
//
// Names which shadow other names are returned first. If a name includes a leading dot ('.'),
// the name is treated as an absolute identifier which cannot be shadowed.
//
// Given a container name a.b.c.M.N and a type name R.s, this will deliver in order:
//
//	a.b.c.M.N.R.s
//	a.b.c.M.R.s
//	a.b.c.R.s
//	a.b.R.s
//	a.R.s
//	R.s
//
// If aliases or abbreviations are configured for the container, then alias names will take
// precedence over containerized names.
func (c *Container) ResolveCandidateNames(name string) []string {
	if strings.HasPrefix(name, ".") {
		qn := name[1:]
		alias, isAlias := c.findAlias(qn)
		if isAlias {
			return []string{alias}
		}
		return []string{qn}
	}
	alias, isAlias := c.findAlias(name)
	if isAlias {
		return []string{alias}
	}
	if c.Name() == "" {
		return []string{name}
	}
	nextCont := c.Name()
	candidates := []string{nextCont + "." + name}
	for i := strings.LastIndex(nextCont, "."); i >= 0; i = strings.LastIndex(nextCont, ".") {
		nextCont = nextCont[:i]
		candidates = append(candidates, nextCont+"."+name)
	}
	return append(candidates, name)
}

// aliasSet returns the alias to fully-qualified name mapping stored in the container.
func (c *Container) aliasSet() map[string]string {
	if c == nil || c.aliases == nil {
		return noAliases
	}
	return c.aliases
}

// findAlias takes a name as input and returns an alias expansion if one exists.
//
// If the name is qualified, the first component of the qualified name is checked against known
// aliases. Any alias that is found in a qualified name is expanded in the result:
//
//	alias: R -> my.alias.R
//	name: R.S.T
//	output: my.alias.R.S.T
//
// Note, the name must not have a leading dot.
func (c *Container) findAlias(name string) (string, bool) {
	// If an alias exists for the name, ensure it is searched last.
	simple := name
	qualifier := ""
	dot := strings.Index(name, ".")
	if dot >= 0 {
		simple = name[0:dot]
		qualifier = name[dot:]
	}
	alias, found := c.aliasSet()[simple]
	if !found {
		return "", false
	}
	return alias + qualifier, true
}

// ContainerOption specifies a functional configuration option for a Container.
//
// Note, ContainerOption implementations must be able to handle nil container inputs.
type ContainerOption func(*Container) (*Container, error)

// Abbrevs configures a set of simple names as abbreviations for fully-qualified names.
//
// An abbreviation (abbrev for short) is a simple name that expands to a fully-qualified name.
// Abbreviations can be useful when working with variables, functions, and especially types from
// multiple namespaces:
//
//	// CEL object construction
//	qual.pkg.version.ObjTypeName{
//	   field: alt.container.ver.FieldTypeName{value: ...}
//	}
//
// Only one the qualified names above may be used as the CEL container, so at least one of these
// references must be a long qualified name within an otherwise short CEL program. Using the
// following abbreviations, the program becomes much simpler:
//
//	// CEL Go option
//	Abbrevs("qual.pkg.version.ObjTypeName", "alt.container.ver.FieldTypeName")
//	// Simplified Object construction
//	ObjTypeName{field: FieldTypeName{value: ...}}
//
// There are a few rules for the qualified names and the simple abbreviations generated from them:
// - Qualified names must be dot-delimited, e.g. `package.subpkg.name`.
// - The last element in the qualified name is the abbreviation.
// - Abbreviations must not collide with each other.
// - The abbreviation must not collide with unqualified names in use.
//
// Abbreviations are distinct from container-based references in the following important ways:
//   - Abbreviations must expand to a fully-qualified name.
//   - Expanded abbreviations do not participate in namespace resolution.
//   - Abbreviation expansion is done instead of the container search for a matching identifier.
//   - Containers follow C++ namespace resolution rules with searches from the most qualified name
//     to the least qualified name.
//   - Container references within the CEL program may be relative, and are resolved to fully
//     qualified names at either type-check time or program plan time, whichever comes first.
//
// If there is ever a case where an identifier could be in both the container and as an
// abbreviation, the abbreviation wins as this will ensure that the meaning of a program is
// preserved between compilations even as the container evolves.
func Abbrevs(qualifiedNames ...string) ContainerOption {
	return func(c *Container) (*Container, error) {
		for _, qn := range qualifiedNames {
			ind := strings.LastIndex(qn, ".")
			if ind <= 0 || ind >= len(qn)-1 {
				return nil, fmt.Errorf(
					"invalid qualified name: %s, wanted name of the form 'qualified.name'", qn)
			}
			alias := qn[ind+1:]
			var err error
			c, err = aliasAs("abbreviation", qn, alias)(c)
			if err != nil {
				return nil, err
			}
		}
		return c, nil
	}
}

// Alias associates a fully-qualified name with a user-defined alias.
//
// In general, Abbrevs is preferred to Alias since the names generated from the Abbrevs option
// are more easily traced back to source code. The Alias option is useful for propagating alias
// configuration from one Container instance to another, and may also be useful for remapping
// poorly chosen protobuf message / package names.
//
// Note: all of the rules that apply to Abbrevs also apply to Alias.
func Alias(qualifiedName, alias string) ContainerOption {
	return aliasAs("alias", qualifiedName, alias)
}

func aliasAs(kind, qualifiedName, alias string) ContainerOption {
	return func(c *Container) (*Container, error) {
		if len(alias) == 0 || strings.Contains(alias, ".") {
			return nil, fmt.Errorf(
				"%s must be non-empty and simple (not qualified): %s=%s", kind, kind, alias)
		}

		if qualifiedName[0:1] == "." {
			return nil, fmt.Errorf("qualified name must not begin with a leading '.': %s",
				qualifiedName)
		}
		ind := strings.LastIndex(qualifiedName, ".")
		if ind <= 0 || ind == len(qualifiedName)-1 {
			return nil, fmt.Errorf("%s must refer to a valid qualified name: %s",
				kind, qualifiedName)
		}
		aliasRef, found := c.aliasSet()[alias]
		if found {
			return nil, fmt.Errorf(
				"%s collides with existing reference: name=%s, %s=%s, existing=%s",
				kind, qualifiedName, kind, alias, aliasRef)
		}
		if strings.HasPrefix(c.Name(), alias+".") || c.Name() == alias {
			return nil, fmt.Errorf(
				"%s collides with container name: name=%s, %s=%s, container=%s",
				kind, qualifiedName, kind, alias, c.Name())
		}
		if c == nil {
			c = &Container{}
		}
		if c.aliases == nil {
			c.aliases = make(map[string]string)
		}
		c.aliases[alias] = qualifiedName
		return c, nil
	}
}

// Name sets the fully-qualified name of the Container.
func Name(name string) ContainerOption {
	return func(c *Container) (*Container, error) {
		if len(name) > 0 && name[0:1] == "." {
			return nil, fmt.Errorf("container name must not contain a leading '.': %s", name)
		}
		if c.Name() == name {
			return c, nil
		}
		if c == nil {
			return &Container{name: name}, nil
		}
		c.name = name
		return c, nil
	}
}

// ToQualifiedName converts an expression AST into a qualified name if possible, with a boolean
// 'found' value that indicates if the conversion is successful.
func ToQualifiedName(e *exprpb.Expr) (string, bool) {
	switch e.GetExprKind().(type) {
	case *exprpb.Expr_IdentExpr:
		id := e.GetIdentExpr()
		return id.GetName(), true
	case *exprpb.Expr_SelectExpr:
		sel := e.GetSelectExpr()
		// Test only expressions are not valid as qualified names.
		if sel.GetTestOnly() {
			return "", false
		}
		if qual, found := ToQualifiedName(sel.GetOperand()); found {
			return qual + "." + sel.GetField(), true
		}
	}
	return "", false
}
