package automation

import (
	"fmt"
	"strings"

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

type BeforeSaveElementEffect string

const (
	BeforeSaveAllowedInMemoryTriggerRecordMutation BeforeSaveElementEffect = "allowed_in_memory_trigger_record_mutation"
	BeforeSaveAllowedValidationAbort               BeforeSaveElementEffect = "allowed_validation_abort"
	BeforeSaveAllowedReadOnly                      BeforeSaveElementEffect = "allowed_read_only"
	BeforeSaveSideEffecting                        BeforeSaveElementEffect = "side_effecting"
)

const beforeSaveSideEffectingMessage = "RecordBeforeSave flows cannot contain side-effecting elements; only in-place $Record mutation and customError are allowed"

type BeforeSaveEffectDecision struct {
	ElementName string
	ElementType string
	Effect      BeforeSaveElementEffect
	Reason      string
}

func ResolveBeforeSaveFlowEffects(meta *models.FlowMetadata) []BeforeSaveEffectDecision {
	if meta == nil {
		return nil
	}

	decisions := make([]BeforeSaveEffectDecision, 0,
		len(meta.Decisions)+len(meta.Assignments)+len(meta.RecordCreates)+len(meta.RecordUpdates)+len(meta.RecordDeletes)+
			len(meta.RecordLookups)+len(meta.ActionCalls)+len(meta.Subflows)+len(meta.Loops)+len(meta.Screens)+
			len(meta.CustomErrors)+len(meta.Waits))

	for _, decision := range meta.Decisions {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: decision.Name,
			ElementType: "Decision",
			Effect:      BeforeSaveAllowedReadOnly,
			Reason:      "decision only evaluates flow conditions",
		})
	}
	for _, assignment := range meta.Assignments {
		effect := BeforeSaveAllowedReadOnly
		reason := "assignment mutates in-memory flow state only"
		for _, item := range assignment.Items {
			if strings.HasPrefix(normalizeFlowReference(item.AssignToReference), "$record.") {
				effect = BeforeSaveAllowedInMemoryTriggerRecordMutation
				reason = "assignment mutates the in-flight trigger record only"
				break
			}
		}
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: assignment.Name,
			ElementType: "Assignment",
			Effect:      effect,
			Reason:      reason,
		})
	}
	for _, create := range meta.RecordCreates {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: create.Name,
			ElementType: "RecordCreate",
			Effect:      BeforeSaveSideEffecting,
			Reason:      "recordCreate writes persistent state",
		})
	}
	for _, update := range meta.RecordUpdates {
		decisions = append(decisions, ResolveBeforeSaveRecordUpdateEffect(meta, &update))
	}
	for _, del := range meta.RecordDeletes {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: del.Name,
			ElementType: "RecordDelete",
			Effect:      BeforeSaveSideEffecting,
			Reason:      "recordDelete writes persistent state",
		})
	}
	for _, lookup := range meta.RecordLookups {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: lookup.Name,
			ElementType: "RecordLookup",
			Effect:      BeforeSaveAllowedReadOnly,
			Reason:      "recordLookup reads records without writing persistent state",
		})
	}
	for _, action := range meta.ActionCalls {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: action.Name,
			ElementType: "ActionCall",
			Effect:      BeforeSaveSideEffecting,
			Reason:      "actionCall may perform side effects outside the trigger record",
		})
	}
	for _, subflow := range meta.Subflows {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: subflow.Name,
			ElementType: "Subflow",
			Effect:      BeforeSaveSideEffecting,
			Reason:      "subflow may perform writes outside the trigger record",
		})
	}
	for _, loop := range meta.Loops {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: loop.Name,
			ElementType: "Loop",
			Effect:      BeforeSaveAllowedReadOnly,
			Reason:      "loop only iterates in-memory collections",
		})
	}
	for _, screen := range meta.Screens {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: screen.Name,
			ElementType: "Screen",
			Effect:      BeforeSaveSideEffecting,
			Reason:      "screen interaction is not allowed in record before-save execution",
		})
	}
	for _, customError := range meta.CustomErrors {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: customError.Name,
			ElementType: "CustomError",
			Effect:      BeforeSaveAllowedValidationAbort,
			Reason:      "customError aborts the write without persisting state",
		})
	}
	for _, wait := range meta.Waits {
		decisions = append(decisions, BeforeSaveEffectDecision{
			ElementName: wait.Name,
			ElementType: "Wait",
			Effect:      BeforeSaveSideEffecting,
			Reason:      "wait pauses execution and persists interview state",
		})
	}

	return decisions
}

func ValidateBeforeSaveFlowEffects(meta *models.FlowMetadata) error {
	for _, decision := range ResolveBeforeSaveFlowEffects(meta) {
		if decision.Effect == BeforeSaveSideEffecting {
			return beforeSaveInvalidFlowError(decision)
		}
	}
	return nil
}

func ResolveBeforeSaveRecordUpdateEffect(meta *models.FlowMetadata, update *models.FlowRecordUpdate) BeforeSaveEffectDecision {
	startObject := ""
	if meta != nil && meta.Start != nil {
		startObject = strings.TrimSpace(meta.Start.Object)
	}
	return resolveBeforeSaveRecordUpdateEffectForStartObject(startObject, update)
}

func resolveBeforeSaveRecordUpdateEffectForStartObject(startObject string, update *models.FlowRecordUpdate) BeforeSaveEffectDecision {
	decision := BeforeSaveEffectDecision{
		ElementType: "RecordUpdate",
		Effect:      BeforeSaveSideEffecting,
		Reason:      "recordUpdate target is ambiguous under RecordBeforeSave execution",
	}
	if update == nil {
		decision.Reason = "recordUpdate metadata is missing"
		return decision
	}

	decision.ElementName = update.Name

	inputRef := normalizeFlowReference(update.InputReference)
	targetObject := strings.TrimSpace(update.Object)
	startObject = strings.TrimSpace(startObject)

	if len(update.Filters) > 0 {
		// The filtered cross-object DML path requires InputReference == ""
		// (executor_record_ops.go:319). With an explicit InputReference (e.g.
		// "$Record"), the executor takes the per-node filter gate in-place path,
		// not the filtered cross-object path — so filters alone cannot make the
		// node side-effecting when InputReference is set.
		if inputRef == "" && (targetObject != "" || hasImplicitCrossObjectIdFilter(update.Filters)) {
			decision.Reason = "recordUpdate with filters reaches the filtered persistence route"
			return decision
		}
		// InputReference is set, or Object is empty with no implicit-Id filter:
		// executor stays on the in-place path; filters only gate the mutation.
	}

	if inputRef != "" && inputRef != "$record" {
		decision.Reason = fmt.Sprintf("recordUpdate inputReference %q targets a non-trigger record persistence route", strings.TrimSpace(update.InputReference))
		return decision
	}

	if startObject == "" {
		decision.Reason = "recordUpdate cannot prove trigger-record identity without a known start object"
		return decision
	}

	if targetObject != "" && !strings.EqualFold(targetObject, startObject) {
		decision.Reason = fmt.Sprintf("recordUpdate object %q does not match before-save trigger object %q", targetObject, startObject)
		return decision
	}

	decision.Effect = BeforeSaveAllowedInMemoryTriggerRecordMutation
	decision.Reason = "recordUpdate mutates the in-flight trigger record only"
	return decision
}

func beforeSaveFlowHasSideEffects(meta *models.FlowMetadata) bool {
	return ValidateBeforeSaveFlowEffects(meta) != nil
}

func beforeSaveInvalidFlowError(decision BeforeSaveEffectDecision) error {
	if decision.ElementName == "" && decision.Reason == "" {
		return fmt.Errorf("%s", beforeSaveSideEffectingMessage)
	}
	return fmt.Errorf("%s: element %q (%s): %s", beforeSaveSideEffectingMessage, decision.ElementName, decision.ElementType, decision.Reason)
}

// hasImplicitCrossObjectIdFilter reports whether any filter has the form
// "Id EqualTo $Record.<field>" — the pattern executeRecordUpdate forwards to
// deriveObjectFromIdFilter when Object is empty. If field metadata resolves a
// target type at runtime, the executor takes the cross-object DML path. The
// before-save validator cannot run metadata lookups, so it treats this pattern
// as potentially cross-object (fail-closed).
func hasImplicitCrossObjectIdFilter(filters []models.FlowRecordFilter) bool {
	for _, f := range filters {
		if !strings.EqualFold(f.Field, "Id") || !strings.EqualFold(f.Operator, "EqualTo") {
			continue
		}
		ref := ""
		if m, ok := f.Value.(map[string]interface{}); ok {
			if er, ok := m["elementReference"].(string); ok {
				ref = er
			}
		}
		if !strings.HasPrefix(ref, "$Record.") {
			continue
		}
		fieldName := strings.TrimPrefix(ref, "$Record.")
		// $Record.Id is the record's own primary key, not a lookup field;
		// deriveObjectFromIdFilter cannot derive a cross-object target from it.
		if strings.EqualFold(fieldName, "Id") {
			continue
		}
		return true
	}
	return false
}

func normalizeFlowReference(ref string) string {
	ref = strings.TrimSpace(ref)
	if strings.EqualFold(ref, "$Record") {
		return "$record"
	}
	return strings.ToLower(ref)
}
