package automation

import (
	"context"
	"fmt"
	"strings"

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

// evaluateConditionsCtx is the __r-aware counterpart of evaluateConditions. It
// routes each condition through evaluateConditionCtx so decisions and loop
// filters can reason about `$Record.Order__r.Status__c` without changing the
// non-__r code paths.  Logic dispatch is shared with evaluateConditions via
// evaluateConditionLogic.
func (e *FlowExecutor) evaluateConditionsCtxE(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, conditions []models.FlowCondition, logic string) (bool, error) {
	if len(conditions) == 0 {
		return true, nil
	}

	results := make([]bool, len(conditions))
	for i, cond := range conditions {
		var err error
		results[i], err = e.evaluateConditionCtxE(ctx, interview, user, &cond)
		if err != nil {
			return false, err
		}
	}

	return evaluateConditionLogic(results, logic), nil
}

// sfKeyPrefixMatch evaluates an SF-style `StartsWith "<3-char-prefix>"` check
// against a reference-type value. Salesforce's 15/18-char IDs encode object
// type in the prefix; UUIDs do not. When a flow condition compares an ID
// against a known standard SObject key prefix, translate it to a polymorphic
// type check by looking up the ID in the prefix's canonical table.
//
// The prefix → object table is a cache rebuilt from _System_Object.KeyPrefix.
// Returns (matched, handled):
//   - handled=true, matched=bool    → definitive result, use matched
//   - handled=false                  → prefix does not look like an SF key
//     prefix, or resolution unavailable; caller falls back to literal
//     string matching so non-ID StartsWith comparisons stay unchanged.
func (e *FlowExecutor) sfKeyPrefixMatch(ctx context.Context, leftValue interface{}, prefix string) (matched bool, handled bool) {
	if len(prefix) != 3 {
		return false, false
	}
	if e == nil || e.metadata == nil {
		return false, false
	}
	expectedObject, ok := e.metadata.ResolveObjectForKeyPrefix(ctx, prefix)
	if !ok {
		return false, false
	}
	idStr, ok := leftValue.(string)
	if !ok || idStr == "" {
		return false, false
	}
	// Backward-compat: if the raw string literally starts with the prefix
	// (a non-UUID ID that happens to carry the SF-style prefix), defer to
	// the literal comparison so existing behaviour is preserved.
	if strings.HasPrefix(idStr, prefix) {
		return false, false
	}
	if e.query == nil {
		return false, false
	}
	result, err := e.query.QueryWithSpec(ctx, queryspec.QuerySpec{
		ObjectAPIName: expectedObject,
		Filter:        queryspec.FieldPredicate(constants.FieldID, "=", idStr),
		Limit:         1,
	}, newSystemAutomationUser())
	if err != nil || result == nil {
		return false, false
	}
	return result.TotalSize > 0 && len(result.Records) > 0, true
}

// lookupTriggerFieldMeta returns the FieldMetadata for a direct
// `$Record.<field>`, `$Record__Old.<field>`, or `$Record__Prior.<field>`
// LeftValueReference on the
// flow's trigger object, or nil for any other shape: bare variables,
// formula results, $User references, multi-segment related-record paths
// (`$Record.Account.Name`), or non-record-triggered flows where the
// FlowRuntimeContext has no ObjectType.
//
// Used by evaluateConditionCtx to route direct trigger-object multi-picklist
// fields through field-aware operator semantics regardless of storage shape.
// The production before-save path normalises multi-picklist values into
// JSON-array strings via NormalizeSObject, so the runtime storage-shape gate
// alone is insufficient for direct `$Record.<field>` comparisons. See codex
// review on PR #263 (round-3 finding) for the full data-flow trace.
//
// Multi-segment LVRs (`$Record.Account.Name`) are intentionally not
// handled here: the leaf field's metadata lives on the related object,
// not the trigger object, and resolving it requires schema chain
// traversal that is out of scope for the #229 fix. Those paths fall
// through to picklistAwareEqual.
func (e *FlowExecutor) lookupTriggerFieldMeta(ctx context.Context, lvr string) *models.FieldMetadata {
	if e == nil || lvr == "" {
		return nil
	}
	parts := strings.Split(lvr, ".")
	if len(parts) != 2 {
		return nil
	}
	if parts[0] != "$Record" && parts[0] != "$Record__Old" && parts[0] != "$Record__Prior" {
		return nil
	}
	frc := flowRuntimeContextFromCtx(ctx)
	if frc == nil {
		return nil
	}
	objectType := frc.ObjectType()
	if objectType == "" {
		return nil
	}
	return e.lookupFieldMeta(ctx, objectType, parts[1])
}

// evaluateConditionCtxE mirrors evaluateCondition but resolves both sides of
// the comparison through the __r-aware value resolver, then delegates common
// operator semantics to evaluateFlowBinaryOperator. The ctx-aware path also
// supplies trigger-field metadata and a live context for field-aware
// multi-picklist and key-prefix comparisons.
func (e *FlowExecutor) evaluateConditionCtxE(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, cond *models.FlowCondition) (bool, error) {
	// Decision conditions always treat LeftValueReference as a variable
	// path (Salesforce Flow contract). Wrap it as an elementReference so
	// ResolveFlowResource returns nil for unresolved names instead of echoing
	// the literal string — otherwise an unset variable looks non-blank to
	// IsBlank/IsNotBlank and a missing input silently takes the wrong
	// decision branch.
	leftValue, _, err := e.ResolveFlowResource(ctx, interview, user, map[string]interface{}{"elementReference": cond.LeftValueReference})
	if err != nil {
		return false, err
	}
	rawRight, _, err := e.ResolveFlowResource(ctx, interview, user, cond.RightValue)
	if err != nil {
		return false, err
	}
	rightValue, _ := extractTypedValue(rawRight)

	// Salesforce Process Builder emits conditions like
	//   leftValueReference: "isChangedRule_2_..."  operator: "EqualTo"  rightValue: {all-null}
	// where an all-null typed wrapper means "check if leftValue is truthy."
	// Detect this pattern and short-circuit to a truthiness check.
	if isAllNilRightValue(rawRight) {
		switch normalizeFlowOperator(cond.Operator) {
		case "EqualTo":
			return isTruthy(leftValue), nil
		case "NotEqualTo":
			return !isTruthy(leftValue), nil
		}
	}

	if matched, handled := evaluateFlowDecisionRegexOperator(cond.Operator, leftValue, rightValue); handled {
		return matched, nil
	}

	fieldMeta := e.lookupTriggerFieldMeta(ctx, cond.LeftValueReference)
	if matched, handled := e.evaluateFlowBinaryOperator(cond.Operator, leftValue, rightValue, flowBinaryOperatorOptions{
		ctx:                 ctx,
		fieldMeta:           fieldMeta,
		useFieldSetEquality: true,
	}); handled {
		return matched, nil
	}

	switch normalizeFlowOperator(cond.Operator) {
	case "IsChanged":
		// See evaluateCondition for semantics. The Ctx variant routes the
		// prior-field lookup through ResolveFlowResource so __r-relationship
		// traversal stays consistent with the rest of the evaluator.
		priorRef, ok := deriveRecordOldRef(cond.LeftValueReference)
		if !ok {
			return false, nil
		}
		priorValue, _, err := e.ResolveFlowResource(ctx, interview, user, priorRef)
		if err != nil {
			return false, err
		}
		return fmt.Sprintf("%v", leftValue) != fmt.Sprintf("%v", priorValue), nil
	default:
		return false, nil
	}
}
