// Copyright 2025 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
//
//    https://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 compiler exposes a standard way to set up a compiler which can be used for CEL
// expressions and policies.
package compiler

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"go.yaml.in/yaml/v3"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common"
	"github.com/google/cel-go/common/env"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/ext"
	"github.com/google/cel-go/policy"
	"google.golang.org/protobuf/encoding/prototext"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"

	celpb "cel.dev/expr"
	configpb "cel.dev/expr/conformance"
	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
	descpb "google.golang.org/protobuf/types/descriptorpb"
)

// FileFormat represents the format of the file being loaded.
type FileFormat int

const (
	// Unspecified is used when the file format is not determined.
	Unspecified FileFormat = iota + 1
	// BinaryProto is used for a binary proto file.
	BinaryProto
	// TextProto is used for a text proto file.
	TextProto
	// TextYAML is used for a YAML file.
	TextYAML
	// CELString is used for a CEL string expression defined in a file with .cel extension.
	CELString
	// CELPolicy is used for a CEL policy file with .celpolicy extension.
	CELPolicy
)

// ExpressionType is an enum for the type of input expression.
type ExpressionType int

const (
	// ExpressionTypeUnspecified is used when the expression type is not specified.
	ExpressionTypeUnspecified ExpressionType = iota
	// CompiledExpressionFile is file containing a checked expression.
	CompiledExpressionFile
	// PolicyFile is a file containing a CEL policy.
	PolicyFile
	// ExpressionFile is a file containing a CEL expression.
	ExpressionFile
	// RawExpressionString is a raw CEL expression string.
	RawExpressionString
)

// PolicyMetadataEnvOption represents a function which accepts a policy metadata map and returns an
// environment option used to extend the CEL environment.
//
// The policy metadata map is generally produced as a byproduct of parsing the policy and it can
// be optionally customised by providing a custom policy parser.
type PolicyMetadataEnvOption func(map[string]any) cel.EnvOption

// Compiler interface is used to set up a compiler with the following capabilities:
// - create a CEL environment
// - create a policy parser
// - fetch policy compiler options
// - fetch policy environment options
//
// Note: This compiler is not the same as the CEL expression compiler, rather it provides an
// abstraction layer which can create the different components needed for parsing and compiling CEL
// expressions and policies.
type Compiler interface {
	// CreateEnv creates a singleton CEL environment with the configured environment options.
	CreateEnv() (*cel.Env, error)
	// CreatePolicyParser creates a policy parser using the optionally configured parser options.
	CreatePolicyParser() (*policy.Parser, error)
	// PolicyCompilerOptions returns the policy compiler options.
	PolicyCompilerOptions() []policy.CompilerOption
	// PolicyMetadataEnvOptions returns the policy metadata environment options.
	PolicyMetadataEnvOptions() []PolicyMetadataEnvOption
}

type compiler struct {
	envOptions               []cel.EnvOption
	policyParserOptions      []policy.ParserOption
	policyCompilerOptions    []policy.CompilerOption
	policyMetadataEnvOptions []PolicyMetadataEnvOption
	env                      *cel.Env
	doOnce                   sync.Once
}

// NewCompiler creates a new compiler with a set of functional options.
func NewCompiler(opts ...any) (Compiler, error) {
	c := &compiler{
		envOptions:               []cel.EnvOption{},
		policyParserOptions:      []policy.ParserOption{},
		policyCompilerOptions:    []policy.CompilerOption{},
		policyMetadataEnvOptions: []PolicyMetadataEnvOption{},
	}
	for _, opt := range opts {
		switch opt := opt.(type) {
		case cel.EnvOption:
			c.envOptions = append(c.envOptions, opt)
		case policy.ParserOption:
			c.policyParserOptions = append(c.policyParserOptions, opt)
		case policy.CompilerOption:
			c.policyCompilerOptions = append(c.policyCompilerOptions, opt)
		case PolicyMetadataEnvOption:
			c.policyMetadataEnvOptions = append(c.policyMetadataEnvOptions, opt)
		default:
			return nil, fmt.Errorf("unsupported compiler option: %v", opt)
		}
	}
	c.envOptions = append(c.envOptions, extensionOpt())
	return c, nil
}

func extensionOpt() cel.EnvOption {
	return func(e *cel.Env) (*cel.Env, error) {
		envConfig := &env.Config{
			Extensions: []*env.Extension{
				&env.Extension{Name: "optional", Version: "latest"},
				&env.Extension{Name: "bindings", Version: "latest"},
			},
		}
		return e.Extend(cel.FromConfig(envConfig, ext.ExtensionOptionFactory))
	}
}

// CreateEnv creates a singleton CEL environment with the configured environment options.
func (c *compiler) CreateEnv() (*cel.Env, error) {
	var err error
	c.doOnce.Do(func() {
		c.env, err = cel.NewCustomEnv(c.envOptions...)
	})
	return c.env, err
}

// CreatePolicyParser creates a policy parser using the optionally configured parser options.
func (c *compiler) CreatePolicyParser() (*policy.Parser, error) {
	return policy.NewParser(c.policyParserOptions...)
}

// PolicyCompilerOptions returns the policy compiler options configured in the compiler.
func (c *compiler) PolicyCompilerOptions() []policy.CompilerOption {
	return c.policyCompilerOptions
}

// PolicyMetadataEnvOptions returns the policy metadata environment options configured in the compiler.
func (c *compiler) PolicyMetadataEnvOptions() []PolicyMetadataEnvOption {
	return c.policyMetadataEnvOptions
}

func loadFile(path string) ([]byte, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read file %q: %v", path, err)
	}
	return data, err
}

func loadProtoFile(path string, format FileFormat, out protoreflect.ProtoMessage) error {
	unmarshaller := proto.Unmarshal
	if format == TextProto {
		unmarshaller = prototext.Unmarshal
	}
	data, err := loadFile(path)
	if err != nil {
		return err
	}
	return unmarshaller(data, out)
}

// InferFileFormat infers the file format from the file path.
func InferFileFormat(path string) FileFormat {
	extension := filepath.Ext(path)
	switch extension {
	case ".textproto":
		return TextProto
	case ".yaml":
		return TextYAML
	case ".binarypb", ".fds", ".pb":
		return BinaryProto
	case ".cel":
		return CELString
	case ".celpolicy":
		return CELPolicy
	default:
		return Unspecified
	}
}

// EnvironmentFile returns an EnvOption which loads a serialized CEL environment from a file.
// The file must be in one of the following formats:
// - Textproto
// - YAML
// - Binarypb
func EnvironmentFile(path string) cel.EnvOption {
	return func(e *cel.Env) (*cel.Env, error) {
		format := InferFileFormat(path)
		if format != TextProto && format != TextYAML && format != BinaryProto {
			return nil, fmt.Errorf("file extension must be one of .textproto, .yaml, .binarypb: found %v", format)
		}
		var envConfig *env.Config
		var fileDescriptorSet *descpb.FileDescriptorSet
		switch format {
		case TextProto, BinaryProto:
			pbEnv := &configpb.Environment{}
			var err error
			if err = loadProtoFile(path, format, pbEnv); err != nil {
				return nil, err
			}
			envConfig, fileDescriptorSet, err = envProtoToConfig(pbEnv)
			if err != nil {
				return nil, err
			}
		case TextYAML:
			envConfig = &env.Config{}
			data, err := loadFile(path)
			if err != nil {
				return nil, err
			}
			err = yaml.Unmarshal(data, envConfig)
			if err != nil {
				return nil, fmt.Errorf("yaml.Unmarshal failed to map CEL environment: %w", err)
			}
		default:
			return nil, fmt.Errorf("unsupported format: %v, file: %s", format, path)
		}
		var envOpts []cel.EnvOption
		if fileDescriptorSet != nil {
			envOpts = append(envOpts, cel.TypeDescs(fileDescriptorSet))
		}
		envOpts = append(envOpts, cel.FromConfig(envConfig, ext.ExtensionOptionFactory))
		var err error
		e, err = e.Extend(envOpts...)
		if err != nil {
			return nil, fmt.Errorf("e.Extend() with env options %v failed: %w", envOpts, err)
		}
		return e, nil
	}
}

func envProtoToConfig(pbEnv *configpb.Environment) (*env.Config, *descpb.FileDescriptorSet, error) {
	if pbEnv == nil {
		return nil, nil, fmt.Errorf("proto environment is not set")
	}
	envConfig := env.NewConfig(pbEnv.GetName())
	envConfig.Description = pbEnv.GetDescription()
	envConfig.SetContainer(pbEnv.GetContainer())
	for _, imp := range pbEnv.GetImports() {
		envConfig.AddImports(env.NewImport(imp.GetName()))
	}
	stdLib, err := envToStdLib(pbEnv)
	if err != nil {
		return nil, nil, err
	}
	envConfig.SetStdLib(stdLib)
	extensions := make([]*env.Extension, 0, len(pbEnv.GetExtensions()))
	for _, extension := range pbEnv.GetExtensions() {
		extensions = append(extensions, &env.Extension{Name: extension.GetName(), Version: extension.GetVersion()})
	}
	envConfig.AddExtensions(extensions...)
	if contextVariable := pbEnv.GetContextVariable(); contextVariable != nil {
		envConfig.SetContextVariable(env.NewContextVariable(contextVariable.GetTypeName()))
	}
	functions, variables, err := protoDeclToFunctionsAndVariables(pbEnv.GetDeclarations())
	if err != nil {
		return nil, nil, err
	}
	envConfig.AddFunctions(functions...)
	envConfig.AddVariables(variables...)
	validators, err := envToValidators(pbEnv)
	if err != nil {
		return nil, nil, err
	}
	envConfig.AddValidators(validators...)
	features, err := envToFeatures(pbEnv)
	if err != nil {
		return nil, nil, err
	}
	envConfig.AddFeatures(features...)
	fileDescriptorSet := pbEnv.GetMessageTypeExtension()
	return envConfig, fileDescriptorSet, nil
}

func envToFeatures(pbEnv *configpb.Environment) ([]*env.Feature, error) {
	features := make([]*env.Feature, 0, len(pbEnv.GetFeatures())+1)
	for _, feature := range pbEnv.GetFeatures() {
		features = append(features, env.NewFeature(feature.GetName(), feature.GetEnabled()))
	}
	if pbEnv.GetEnableMacroCallTracking() {
		features = append(features, env.NewFeature("cel.feature.macro_call_tracking", true))
	}
	return features, nil
}

func envToValidators(pbEnv *configpb.Environment) ([]*env.Validator, error) {
	validators := make([]*env.Validator, 0, len(pbEnv.GetValidators()))
	for _, pbValidator := range pbEnv.GetValidators() {
		validator := env.NewValidator(pbValidator.GetName())
		config := map[string]any{}
		for k, v := range pbValidator.GetConfig() {
			val := types.DefaultTypeAdapter.NativeToValue(v)
			config[k] = val.Value()
		}
		validator.SetConfig(config)
		validators = append(validators, validator)
	}
	return validators, nil
}

func protoDeclToFunctionsAndVariables(declarations []*celpb.Decl) ([]*env.Function, []*env.Variable, error) {
	functions := make([]*env.Function, 0, len(declarations))
	variables := make([]*env.Variable, 0, len(declarations))
	for _, decl := range declarations {
		switch decl.GetDeclKind().(type) {
		case *celpb.Decl_Function:
			fn, err := protoDeclToFunction(decl)
			if err != nil {
				return nil, nil, fmt.Errorf("protoDeclToFunction(%s) failed to create function: %w", decl.GetName(), err)
			}
			functions = append(functions, fn)
		case *celpb.Decl_Ident:
			t, err := types.ProtoAsType(decl.GetIdent().GetType())
			if err != nil {
				return nil, nil, fmt.Errorf("types.ProtoAsType(%s) failed to create type: %w", decl.GetIdent().GetType(), err)
			}
			variables = append(variables, env.NewVariable(decl.GetName(), env.SerializeTypeDesc(t)))
		}
	}
	return functions, variables, nil
}

func envToStdLib(pbEnv *configpb.Environment) (*env.LibrarySubset, error) {
	stdLib := env.NewLibrarySubset()
	pbEnvStdLib := pbEnv.GetStdlib()
	if pbEnvStdLib == nil {
		if pbEnv.GetDisableStandardCelDeclarations() {
			stdLib.SetDisabled(true)
			return stdLib, nil
		}
		return nil, nil
	}
	if !stdLib.Disabled {
		stdLib.SetDisabled(pbEnvStdLib.GetDisabled())
	}
	stdLib.SetDisableMacros(pbEnvStdLib.GetDisableMacros())
	stdLib.AddIncludedMacros(pbEnvStdLib.GetIncludeMacros()...)
	stdLib.AddExcludedMacros(pbEnvStdLib.GetExcludeMacros()...)
	if pbEnvStdLib.GetIncludeFunctions() != nil {
		for _, includeFn := range pbEnvStdLib.GetIncludeFunctions() {
			if includeFn.GetFunction() != nil {
				fn, err := protoDeclToFunction(includeFn)
				if err != nil {
					return nil, err
				}
				stdLib.AddIncludedFunctions(fn)
			} else {
				return nil, fmt.Errorf("IncludeFunctions must specify a function decl")
			}
		}
	}
	if pbEnvStdLib.GetExcludeFunctions() != nil {
		for _, excludeFn := range pbEnvStdLib.GetExcludeFunctions() {
			if excludeFn.GetFunction() != nil {
				fn, err := protoDeclToFunction(excludeFn)
				if err != nil {
					return nil, err
				}
				stdLib.AddExcludedFunctions(fn)
			} else {
				return nil, fmt.Errorf("ExcludeFunctions must specify a function decl")
			}
		}
	}
	return stdLib, nil
}

func protoDeclToFunction(decl *celpb.Decl) (*env.Function, error) {
	declFn := decl.GetFunction()
	if declFn == nil {
		return nil, nil
	}
	overloads := make([]*env.Overload, 0, len(declFn.GetOverloads()))
	for _, o := range declFn.GetOverloads() {
		args := make([]*env.TypeDesc, 0, len(o.GetParams()))
		for _, p := range o.GetParams() {
			t, err := types.ProtoAsType(p)
			if err != nil {
				return nil, err
			}
			args = append(args, env.SerializeTypeDesc(t))
		}
		res, err := types.ProtoAsType(o.GetResultType())
		if err != nil {
			return nil, err
		}
		ret := env.SerializeTypeDesc(res)
		if o.IsInstanceFunction {
			overloads = append(overloads, env.NewMemberOverload(o.GetOverloadId(), args[0], args[1:], ret))
		} else {
			overloads = append(overloads, env.NewOverload(o.GetOverloadId(), args, ret))
		}
	}
	return env.NewFunction(decl.GetName(), overloads...), nil
}

// TypeDescriptorSetFile returns an EnvOption which loads type descriptors from a file.
// The file must be in binary format.
func TypeDescriptorSetFile(path string) cel.EnvOption {
	return func(e *cel.Env) (*cel.Env, error) {
		format := InferFileFormat(path)
		if format != BinaryProto {
			return nil, fmt.Errorf("type descriptor must be in binary format")
		}
		fds := &descpb.FileDescriptorSet{}
		if err := loadProtoFile(path, BinaryProto, fds); err != nil {
			return nil, err
		}
		var err error
		e, err = e.Extend(cel.TypeDescs(fds))
		if err != nil {
			return nil, fmt.Errorf("e.Extend() with type descriptor set %v failed: %w", fds, err)
		}
		return e, nil
	}
}

// InputExpression is an interface for an expression which can be compiled into a CEL AST and return
// an optional policy metadata map.
type InputExpression interface {
	// CreateAST creates a CEL AST from the input expression using the provided compiler.
	CreateAST(Compiler) (*cel.Ast, map[string]any, error)
}

// CompiledExpression is an InputExpression which loads a CheckedExpr from a file.
type CompiledExpression struct {
	Path string
}

// CreateAST creates a CEL AST from a checked expression file.
// The file must be in one of the following formats:
// - Binarypb
// - Textproto
func (c *CompiledExpression) CreateAST(_ Compiler) (*cel.Ast, map[string]any, error) {
	var expr exprpb.CheckedExpr
	format := InferFileFormat(c.Path)
	if format != BinaryProto && format != TextProto {
		return nil, nil, fmt.Errorf("invalid file extension wanted: .binarypb or .textproto found: %v", format)
	}
	if err := loadProtoFile(c.Path, format, &expr); err != nil {
		return nil, nil, err
	}
	return cel.CheckedExprToAst(&expr), nil, nil
}

// FileExpression is an InputExpression which loads a CEL expression or policy from a file.
type FileExpression struct {
	Path string
}

// CreateAST creates a CEL AST from a file using the provided compiler:
// - All policy metadata options are executed using the policy metadata map to extend the
// environment.
// - All policy compiler options are passed on to compile the parsed policy.
//
// The file must be in one of the following formats:
// - .cel: CEL string expression
// - .celpolicy: CEL policy
func (f *FileExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any, error) {
	e, err := compiler.CreateEnv()
	if err != nil {
		return nil, nil, err
	}
	format := InferFileFormat(f.Path)
	switch format {
	case CELString:
		data, err := loadFile(f.Path)
		if err != nil {
			return nil, nil, err
		}
		src := common.NewStringSource(string(data), f.Path)
		ast, iss := e.CompileSource(src)
		if iss.Err() != nil {
			return nil, nil, fmt.Errorf("e.CompileSource(%q) failed: %w", src.Content(), iss.Err())
		}
		return ast, nil, nil
	case CELPolicy, TextYAML:
		data, err := loadFile(f.Path)
		if err != nil {
			return nil, nil, err
		}
		src := policy.ByteSource(data, f.Path)
		parser, err := compiler.CreatePolicyParser()
		if err != nil {
			return nil, nil, err
		}
		p, iss := parser.Parse(src)
		if iss.Err() != nil {
			return nil, nil, fmt.Errorf("parser.Parse(%q) failed: %w", src.Content(), iss.Err())
		}
		policyMetadata := clonePolicyMetadata(p)
		for _, opt := range compiler.PolicyMetadataEnvOptions() {
			if e, err = e.Extend(opt(policyMetadata)); err != nil {
				return nil, nil, fmt.Errorf("e.Extend() with metadata option failed: %w", err)
			}
		}
		ast, iss := policy.Compile(e, p, compiler.PolicyCompilerOptions()...)
		if iss.Err() != nil {
			return nil, nil, fmt.Errorf("policy.Compile(%q) failed: %w", src.Content(), iss.Err())
		}
		return ast, policyMetadata, nil
	default:
		return nil, nil, fmt.Errorf("invalid file extension wanted: .cel or .celpolicy or .yaml found: %v", format)
	}
}

func clonePolicyMetadata(p *policy.Policy) map[string]any {
	metadataKeys := p.MetadataKeys()
	metadata := make(map[string]any, len(metadataKeys))
	for _, key := range metadataKeys {
		value, _ := p.Metadata(key)
		metadata[key] = value
	}
	return metadata
}

// RawExpression is an InputExpression which loads a CEL expression from a string.
type RawExpression struct {
	Value string
}

// CreateAST creates a CEL AST from a raw CEL expression using the provided compiler.
func (r *RawExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any, error) {
	e, err := compiler.CreateEnv()
	if err != nil {
		return nil, nil, err
	}
	format := InferFileFormat(r.Value)
	if format != Unspecified {
		return nil, nil, fmt.Errorf("invalid raw expression found file with extension: %v", format)
	}
	ast, iss := e.Compile(r.Value)
	if iss.Err() != nil {
		return nil, nil, fmt.Errorf("e.Compile(%q) failed: %w", r.Value, iss.Err())
	}
	return ast, nil, nil
}
