package cel

import (
	"crypto/sha256"
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/checker/decls"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/common/types/ref"
	"github.com/google/cel-go/ext"
	"github.com/google/uuid"

	"github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1"

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

type CELParser struct {
	workflowStrEnv     *cel.Env
	stepRunEnv         *cel.Env
	eventEnv           *cel.Env
	incomingWebhookEnv *cel.Env
}

var checksumDecl = decls.NewFunction("checksum",
	decls.NewOverload("checksum_string",
		[]*expr.Type{decls.String},
		decls.String,
	),
)

var checksum = cel.Function("checksum",
	cel.MemberOverload(
		"checksum_string_impl",
		[]*cel.Type{cel.StringType},
		cel.StringType,
		cel.FunctionBinding(func(args ...ref.Val) ref.Val {
			if len(args) != 1 {
				return types.NewErr("checksum requires 1 argument")
			}
			str, ok := args[0].(types.String)
			if !ok {
				return types.NewErr("argument must be a string")
			}
			hash := sha256.Sum256([]byte(str))
			return types.String(fmt.Sprintf("%x", hash))
		})),
)

func NewCELParser() *CELParser {
	workflowStrEnv, _ := cel.NewEnv(
		cel.Declarations(
			decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("additional_metadata", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("workflow_run_id", decls.String),
			checksumDecl,
		),
		checksum,
		ext.Strings(),
	)

	stepRunEnv, _ := cel.NewEnv(
		cel.Declarations(
			decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("additional_metadata", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("parents", decls.NewMapType(decls.String, decls.NewMapType(decls.String, decls.Dyn))),
			decls.NewVar("workflow_run_id", decls.String),
			checksumDecl,
		),
		checksum,
		ext.Strings(),
	)

	eventEnv, _ := cel.NewEnv(
		cel.Declarations(
			decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("additional_metadata", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("payload", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("event_id", decls.String),
			decls.NewVar("event_key", decls.String),
			checksumDecl,
		),
		ext.Strings(),
	)

	incomingWebhookEnv, _ := cel.NewEnv(
		cel.Declarations(
			decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
			decls.NewVar("headers", decls.NewMapType(decls.String, decls.String)),
			checksumDecl,
		),
	)

	return &CELParser{
		workflowStrEnv:     workflowStrEnv,
		stepRunEnv:         stepRunEnv,
		eventEnv:           eventEnv,
		incomingWebhookEnv: incomingWebhookEnv,
	}
}

type Input map[string]interface{}

type InputOpts func(Input)

func WithInput(input map[string]interface{}) InputOpts {
	return func(w Input) {
		w["input"] = input
	}
}

func WithHeaders(headers map[string]string) InputOpts {
	return func(w Input) {
		w["headers"] = headers
	}
}

func WithParents(parents any) InputOpts {
	return func(w Input) {
		w["parents"] = parents
	}
}

func WithAdditionalMetadata(metadata map[string]interface{}) InputOpts {
	return func(w Input) {
		w["additional_metadata"] = metadata
	}
}

func WithWorkflowRunID(workflowRunID uuid.UUID) InputOpts {
	return func(w Input) {
		w["workflow_run_id"] = workflowRunID.String()
	}
}

func WithPayload(payload map[string]interface{}) InputOpts {
	return func(w Input) {
		w["payload"] = payload
	}
}

func WithEventID(eventID uuid.UUID) InputOpts {
	return func(w Input) {
		w["event_id"] = eventID
	}
}

func WithEventKey(key string) InputOpts {
	return func(w Input) {
		w["event_key"] = key
	}
}

func NewInput(opts ...InputOpts) Input {
	res := make(map[string]interface{})

	for _, opt := range opts {
		opt(res)
	}

	return res
}

func (p *CELParser) ParseWorkflowString(workflowExp string) (cel.Program, error) {
	ast, issues := p.workflowStrEnv.Compile(workflowExp)

	if issues != nil && issues.Err() != nil {
		return nil, issues.Err()
	}

	return p.workflowStrEnv.Program(ast)
}

func (p *CELParser) ParseAndEvalWorkflowString(workflowExp string, in Input) (string, error) {
	prg, err := p.ParseWorkflowString(workflowExp)
	if err != nil {
		return "", err
	}

	var inMap map[string]interface{} = in

	out, _, err := prg.Eval(inMap)
	if err != nil {
		return "", err
	}

	// Switch on the type of the output.
	switch out.Type() {
	case types.StringType:
		return out.Value().(string), nil
	default:
		return "", fmt.Errorf("output must evaluate to a string: got %s", out.Type().TypeName())
	}
}

type StepRunOutType string

const (
	StepRunOutTypeString StepRunOutType = "string"
	StepRunOutTypeInt    StepRunOutType = "int"
)

type StepRunOut struct {
	String *string
	Int    *int
	Type   StepRunOutType
}

func (p *CELParser) ParseStepRun(stepRunExpr string) (cel.Program, error) {
	ast, issues := p.stepRunEnv.Compile(stepRunExpr)

	if issues != nil && issues.Err() != nil {
		return nil, issues.Err()
	}

	return p.stepRunEnv.Program(ast)
}

func (p *CELParser) ParseAndEvalStepRun(stepRunExpr string, in Input) (*StepRunOut, error) {
	prg, err := p.ParseWorkflowString(stepRunExpr)
	if err != nil {
		return nil, err
	}

	var inMap map[string]interface{} = in

	out, _, err := prg.Eval(inMap)
	if err != nil {
		return nil, err
	}

	res := &StepRunOut{}

	switch out.Type() {
	case cel.StringType:
		str := out.Value().(string)
		res.String = &str
		res.Type = StepRunOutTypeString
	case cel.IntType:
		i := int(out.Value().(int64))
		res.Int = &i
		res.Type = StepRunOutTypeInt
	case cel.DoubleType:
		i := int(out.Value().(float64))
		res.Int = &i
		res.Type = StepRunOutTypeInt
	default:
		return nil, fmt.Errorf("output must evaluate to a string or integer: got %s", out.Type().TypeName())
	}

	return res, nil
}

func (p *CELParser) CheckStepRunOutAgainstKnown(out *StepRunOut, knownType sqlcv1.StepExpressionKind) error {
	switch knownType {
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITKEY:
		if out.String == nil {
			prefix := "expected string output for dynamic rate limit key"

			if out.Int != nil {
				return fmt.Errorf("%s, got int", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITVALUE:
		if out.Int == nil {
			prefix := "expected int output for dynamic rate limit value"

			if out.String != nil {
				return fmt.Errorf("%s, got string", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITWINDOW:
		if out.String == nil {
			prefix := "expected string output for dynamic rate limit window"

			if out.Int != nil {
				return fmt.Errorf("%s, got int", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITUNITS:
		if out.Int == nil {
			prefix := "expected int output for dynamic rate limit units"

			if out.String != nil {
				return fmt.Errorf("%s, got string", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	}

	return nil
}

func (p *CELParser) CheckStepRunOutAgainstKnownV1(out *StepRunOut, knownType sqlcv1.StepExpressionKind) error {
	switch knownType {
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITKEY:
		if out.String == nil {
			prefix := "expected string output for dynamic rate limit key"

			if out.Int != nil {
				return fmt.Errorf("%s, got int", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITVALUE:
		if out.Int == nil {
			prefix := "expected int output for dynamic rate limit value"

			if out.String != nil {
				return fmt.Errorf("%s, got string", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITWINDOW:
		if out.String == nil {
			prefix := "expected string output for dynamic rate limit window"

			if out.Int != nil {
				return fmt.Errorf("%s, got int", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	case sqlcv1.StepExpressionKindDYNAMICRATELIMITUNITS:
		if out.Int == nil {
			prefix := "expected int output for dynamic rate limit units"

			if out.String != nil {
				return fmt.Errorf("%s, got string", prefix)
			}

			return fmt.Errorf("%s, got unknown type", prefix)
		}
	}

	return nil
}

func (p *CELParser) EvaluateEventExpression(expr string, input Input) (bool, error) {
	ast, issues := p.eventEnv.Compile(expr)

	if issues != nil && issues.Err() != nil {
		return false, fmt.Errorf("failed to compile expression: %w", issues.Err())
	}

	program, err := p.eventEnv.Program(ast)
	if err != nil {
		return false, fmt.Errorf("failed to create program: %w", err)
	}

	var inMap map[string]interface{} = input

	out, _, err := program.Eval(inMap)
	if err != nil {
		return false, fmt.Errorf("failed to evaluate expression: %w", err)
	}

	if out.Type() != types.BoolType {
		return false, fmt.Errorf("expression did not evaluate to a boolean: got %s", out.Type().TypeName())
	}

	return out.Value().(bool), nil
}

func (p *CELParser) EvaluateIncomingWebhookExpression(expr string, input Input) (string, error) {
	ast, issues := p.incomingWebhookEnv.Compile(expr)

	if issues != nil && issues.Err() != nil {
		return "", fmt.Errorf("failed to compile expression: %w", issues.Err())
	}

	program, err := p.incomingWebhookEnv.Program(ast)
	if err != nil {
		return "", fmt.Errorf("failed to create program: %w", err)
	}

	var inMap map[string]interface{} = input

	out, _, err := program.Eval(inMap)
	if err != nil {
		return "", fmt.Errorf("failed to evaluate expression: %w", err)
	}

	if out.Type() != types.StringType {
		return "", fmt.Errorf("expression did not evaluate to a string: got %s", out.Type().TypeName())
	}

	return out.Value().(string), nil
}
