package formula

import (
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/models"
)

// Verdict is the three-state outcome of a frozen-formula install/verify gate.
//
// The zero value is Unverified by design: any caller path that creates a
// Verdict but forgets to set it MUST default to "block install", not "pass".
// This is the structural fail-closed-by-default contract that replaces the
// old `len(violations) == 0 → healthy` shape, where every walker miss, every
// infra error path that returned nil, and every future ast node default
// silently classified the pack as healthy.
//
// Caller contract:
//
//   - Verified  → positive proof the formula is safe to install.
//     (parse OK + AST walker exhaustive pass — Round-11
//     retracted the prior DryEvaluate-against-stubs gate after
//     it surfaced as both too strict on typed-record formulas
//     and as a CLAUDE.md-violating L0/L1 boundary leak; see
//     ValidateSyntax godoc in engine.go for the full rationale.)
//   - Unverified → could not prove safe; block install. err is non-nil and
//     carries the reason. Callers MUST NOT treat this as healthy.
//   - Failed    → actively rejected (e.g. unknown function name, method-call
//     shape, parse error, arity mismatch). violations carry the
//     reason; err may be nil because the gate ran to completion.
//
// Valid acceptance check: `verdict == Verified && err == nil`. Anything
// else blocks. Specifically, `len(violations) == 0` is NOT a valid health
// proof — IANTHEREAL Round-1/2/3/9 P1 (PR #268) all expressed the same
// underlying bug: callers using that shape treated walker misses, infra
// errors, and panics as "no violation recorded → pack is fine".
type Verdict int

const (
	// Unverified is the zero value. ANY uninitialised Verdict, including
	// one returned alongside a non-nil err, MUST be treated as "block".
	Unverified Verdict = iota
	// Verified means the formula passed every install gate and is safe to
	// install. Returned ONLY by code paths that have positively cleared
	// every check (parse OK + walker exhaustive pass — Round-11 dropped
	// the DryEvaluate runtime gate). NEVER returned alongside a non-nil err.
	Verified
	// Failed means the formula was actively rejected (unknown function
	// name, method-call shape, parse error). violations carry per-formula
	// reasons; err may be nil because the gate ran to completion.
	Failed
)

// String implements fmt.Stringer for human-readable telemetry.
func (v Verdict) String() string {
	switch v {
	case Verified:
		return "Verified"
	case Failed:
		return "Failed"
	default:
		return "Unverified"
	}
}

type FrozenFormulaAuthority string

const (
	FrozenFormulaAuthorityPackPayload   FrozenFormulaAuthority = "pack_payload"
	FrozenFormulaAuthorityLiveReadiness FrozenFormulaAuthority = "live_readiness"
)

type FrozenFormulaRef struct {
	Authority              FrozenFormulaAuthority
	PackName               string
	ComponentKind          string
	BusinessKey            string
	Manageability          string
	ObjectAPIName          string
	FieldAPIName           string
	FormulaMode            string
	Formula                string
	Schema                 *models.ObjectMetadata
	SchemasByObjectAPIName map[string]*models.ObjectMetadata
	ExpectedType           *models.FieldType
}

type FrozenFormulaViolation struct {
	Authority     FrozenFormulaAuthority
	PackName      string
	ComponentKind string
	BusinessKey   string
	Manageability string
	ObjectAPIName string
	FieldAPIName  string
	FormulaMode   string
	Formula       string
	Reason        string
}

func (v FrozenFormulaViolation) Error() string {
	target := strings.TrimSpace(v.FieldAPIName)
	if object := strings.TrimSpace(v.ObjectAPIName); object != "" {
		if target == "" {
			target = object
		} else {
			target = object + "." + target
		}
	}
	if target == "" {
		target = strings.TrimSpace(v.BusinessKey)
	}
	if target == "" {
		target = "unknown formula ref"
	}
	return fmt.Sprintf("%s: %s", target, v.Reason)
}

// VerifyFrozenFormulaRefs is the pure static validator for frozen formula
// install proof. It inspects only refs explicitly supplied by the caller and
// does not read env vars, metadata, or the database.
func VerifyFrozenFormulaRefs(refs []FrozenFormulaRef, engine *Engine) (Verdict, []FrozenFormulaViolation, error) {
	if engine == nil {
		return Unverified, nil, fmt.Errorf("frozen formula verification requires a formula engine")
	}

	violations := make([]FrozenFormulaViolation, 0)
	for _, ref := range refs {
		if strings.TrimSpace(ref.FormulaMode) != "frozen" {
			continue
		}
		if strings.TrimSpace(ref.Formula) == "" {
			continue
		}
		var err error
		if ref.Schema != nil {
			err = engine.validateSyntaxWithSchema(ref.Formula, ref.Schema, ref.SchemasByObjectAPIName, ref.ExpectedType)
		} else {
			err = engine.ValidateSyntax(ref.Formula)
		}
		if err != nil {
			violations = append(violations, FrozenFormulaViolation{
				Authority:     ref.Authority,
				PackName:      strings.TrimSpace(ref.PackName),
				ComponentKind: strings.TrimSpace(ref.ComponentKind),
				BusinessKey:   strings.TrimSpace(ref.BusinessKey),
				Manageability: strings.TrimSpace(ref.Manageability),
				ObjectAPIName: strings.TrimSpace(ref.ObjectAPIName),
				FieldAPIName:  strings.TrimSpace(ref.FieldAPIName),
				FormulaMode:   strings.TrimSpace(ref.FormulaMode),
				Formula:       ref.Formula,
				Reason:        err.Error(),
			})
		}
	}
	if len(violations) > 0 {
		return Failed, violations, nil
	}
	return Verified, nil, nil
}
