package automation

import (
	"context"
	"fmt"

	"github.com/nexuscrm/backend/internal/bootstrap"
	"github.com/nexuscrm/backend/pkg/formula"
	"github.com/nexuscrm/shared/pkg/models"
)

type actionFieldDefaultsCtxKey struct{}

// WithActionFieldDefaults stamps the pre-loaded _System_ActionFieldDefault
// rows on ctx. The dispatcher loads once per request (indexed lookup on
// StandardActionId) and stamps here; the executor's recordCreate resolver
// reads from ctx at each create, filters by TargetObject, evaluates formulas.
//
// Lazy evaluation (formulas run at create time, not pre-flight) lets rows
// reference flow-interview state like $Record in their ValueFormula when
// that becomes needed; currently shipped rows reference only Input.<field>.
func WithActionFieldDefaults(ctx context.Context, rows []bootstrap.ActionFieldDefaultRow) context.Context {
	return context.WithValue(ctx, actionFieldDefaultsCtxKey{}, rows)
}

// ActionFieldDefaultsFromCtx returns the stamped rows if any. Empty/absent
// means stage 3 is a no-op.
func ActionFieldDefaultsFromCtx(ctx context.Context) ([]bootstrap.ActionFieldDefaultRow, bool) {
	rows, ok := ctx.Value(actionFieldDefaultsCtxKey{}).([]bootstrap.ActionFieldDefaultRow)
	return rows, ok
}

// FieldDefaultResolver applies stage 3 (ActionFieldDefault) of the
// recordCreate default pipeline. See docs/design/lead-convert-refactor-outcome.md
// and the plan file for the full layered ordering.
//
// Contract:
//   - Only writes record[FieldAPIName] when the key is ABSENT (not nil-valued).
//     Nil-valued keys are preserved as caller/flow-written intent.
//   - Rows are filtered to the given targetObject.
//   - Rows are expected to be pre-sorted by Priority DESC within (TargetObject, FieldAPIName)
//     (GetActionFieldDefaults already does this); the first winning row
//     per (TargetObject, FieldAPIName) claims the slot.
//   - Condition and ValueFormula are evaluated against the current flow
//     context exposed under the `Input` namespace — so the caller payload
//     (`leadId`, `accountId`, …) plus any flow-state keys populated earlier
//     in the interview (e.g. resolved `$lead`) are all reachable as
//     `Input.<key>`. Caller supplies that map via `Apply(ctx, rec, obj, inputs)`.
//     Return value is stored verbatim in the record map; downstream
//     persistence coercion handles type normalization (e.g. "2026-05-23" →
//     DATE column).
type FieldDefaultResolver interface {
	Apply(ctx context.Context, record models.SObject, targetObject string, inputs map[string]interface{}) error
}

// defaultFieldDefaultResolver is the production impl. Stateless apart from
// the injected formula engine.
type defaultFieldDefaultResolver struct {
	formulas *formula.Engine
}

// NewDefaultFieldDefaultResolver constructs the production resolver. Passing
// nil constructs a fresh formula.Engine; production callers should pass the
// executor's engine so compiled regex/function tables are shared.
func NewDefaultFieldDefaultResolver(formulas *formula.Engine) FieldDefaultResolver {
	if formulas == nil {
		formulas = formula.NewEngine()
	}
	return &defaultFieldDefaultResolver{formulas: formulas}
}

func (r *defaultFieldDefaultResolver) Apply(ctx context.Context, record models.SObject, targetObject string, inputs map[string]interface{}) error {
	rows, ok := ActionFieldDefaultsFromCtx(ctx)
	if !ok || len(rows) == 0 {
		return nil
	}
	fctx := &formula.Context{Variables: map[string]interface{}{"Input": inputs}}
	claimed := make(map[string]struct{})
	for _, row := range rows {
		if row.TargetObject != targetObject {
			continue
		}
		if _, already := claimed[row.FieldAPIName]; already {
			continue
		}
		if _, present := record[row.FieldAPIName]; present {
			// Layer 1 (caller write) or layer 2 (flow InputAssignment) already
			// claimed this field. Stage 3 only fills absent keys.
			continue
		}
		if row.Condition != "" {
			raw, err := r.formulas.Evaluate(row.Condition, fctx)
			if err != nil {
				return fmt.Errorf("ActionFieldDefault condition %q for %s.%s is invalid: %w",
					row.Condition, row.TargetObject, row.FieldAPIName, err)
			}
			if !IsFormulaTruthy(raw) {
				continue
			}
		}
		value, err := r.formulas.Evaluate(row.ValueFormula, fctx)
		if err != nil {
			return fmt.Errorf("ActionFieldDefault formula %q for %s.%s is invalid: %w",
				row.ValueFormula, row.TargetObject, row.FieldAPIName, err)
		}
		record[row.FieldAPIName] = value
		claimed[row.FieldAPIName] = struct{}{}
	}
	return nil
}

// IsFormulaTruthy is the single definition of formula-result truthiness
// shared by the ActionFieldDefault stage-3 resolver and the dispatcher's
// InputGuard evaluator. Empty string, "false", 0 (int/int64/float64), and
// nil all read as false; everything else is true. Consolidated here so the
// two callers can't drift — if the contract changes, it changes for both.
func IsFormulaTruthy(v interface{}) bool {
	switch typed := v.(type) {
	case nil:
		return false
	case bool:
		return typed
	case string:
		return typed != "" && typed != "false"
	case int:
		return typed != 0
	case int64:
		return typed != 0
	case float64:
		return typed != 0
	default:
		return true
	}
}
