package automation

import (
	"context"
	"fmt"
	"strings"
	"time"

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// stampConvertedDateField is the Salesforce-convention API name for the
// auto-stamped convert timestamp on a source SObject (Lead.ConvertedDate
// is the canonical example). Any SObject whose schema declares this
// field opts into auto-stamp-on-convert. Standard SF identifier per
// CLAUDE.md's permitted-hardcoded-strings carve-out.
const stampConvertedDateField = "ConvertedDate"

// maxFlowCallDepth caps how deep the parent→child subflow chain can go before
// we assume the author accidentally introduced infinite recursion. Salesforce
// documents a similar depth cap (their runtime uses 2000 elements per interview
// but also guards recursion). 10 is generous for the migrated flows (the known
// Order_Expiration_Notice_90 chain is 1 deep) while remaining safe.
const maxFlowCallDepth = 10

// flowCallStackKey carries the stack of flow API names currently executing in
// the caller's goroutine. It is used to detect direct recursion (A → A) and
// cycles (A → B → A) when an action with actionType="flow" is dispatched.
type flowCallStackKey struct{}

// withFlowCall pushes flowApiName onto the call stack on ctx. Returns the new
// context and an error if pushing flowApiName would create a cycle or exceed
// maxFlowCallDepth.
func withFlowCall(ctx context.Context, flowApiName string) (context.Context, error) {
	existing, _ := ctx.Value(flowCallStackKey{}).([]string)
	for _, name := range existing {
		if strings.EqualFold(name, flowApiName) {
			return ctx, fmt.Errorf(
				"flow subflow cycle detected: %s is already in the call chain [%s]",
				flowApiName, strings.Join(append(existing, flowApiName), " -> "),
			)
		}
	}
	if len(existing) >= maxFlowCallDepth {
		return ctx, fmt.Errorf(
			"flow subflow depth %d exceeds maximum %d (chain: %s)",
			len(existing)+1, maxFlowCallDepth,
			strings.Join(append(existing, flowApiName), " -> "),
		)
	}
	// Copy-on-write to avoid mutating a slice shared with concurrent siblings.
	next := make([]string, len(existing)+1)
	copy(next, existing)
	next[len(existing)] = flowApiName
	return context.WithValue(ctx, flowCallStackKey{}, next), nil
}

// executeActionCall invokes an action
func (e *FlowExecutor) executeActionCall(ctx context.Context, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
	// GAP 5 (Phase 2.4): actionType="flow" invokes another flow synchronously.
	// The parent interview blocks until the child finishes, inputs map to child
	// variables, outputs flow back into the parent's ContextData — matching the
	// behaviour of native FlowSubflow elements so that Salesforce metadata that
	// chains flows via actionCalls (e.g. Order_Expiration_Notice_90 calling
	// Order_Expiration_Alert_7) works without editing.
	if strings.EqualFold(action.ActionType, "flow") {
		return e.executeFlowAction(ctx, interview, action, user)
	}

	// emailAlert actionCalls reference a _System_WorkflowAlert row via their
	// ActionName (format: "ObjectApiName.AlertName"). The alert row holds the
	// template + recipients + sender — none of which are carried in the flow's
	// inputParameters. Dispatch via ApprovalService which owns the shared
	// WorkflowAlert lookup + render + send pipeline.
	if strings.EqualFold(action.ActionType, "emailAlert") {
		return e.executeEmailAlertAction(ctx, interview, flowMeta, action, user)
	}

	// stampConvertedAnchors is a generic invocable action that reads the
	// source object's FieldDefinition rows for converted-anchor lookup fields
	// and stamps whichever anchors the caller supplied. Its only Go-side
	// knowledge is the Salesforce converted-anchor naming convention
	// (via MetadataService.ConvertedLookups); which fields exist, which
	// references, and which targets to stamp all come from metadata and the
	// flow's inputParameters. A tenant adding a custom converted-anchor lookup
	// is picked up with no code change.
	if strings.EqualFold(action.ActionType, "stampConvertedAnchors") {
		if !constants.HasStandardActionExecutionContext(ctx) || !constants.HasStandardActionProtectedFlowExecution(ctx) {
			err := fmt.Errorf("stampConvertedAnchors requires protected standard action flow execution context")
			if action.FaultConnector != nil {
				interview.ContextData["$Flow.FaultMessage"] = err.Error()
				return action.FaultConnector.TargetReference, nil
			}
			return "", err
		}
		return e.executeStampConvertedAnchors(ctx, interview, action, user)
	}

	if actionMeta := e.buildStandardActionMetadata(action, flowMeta); actionMeta != nil {
		record := coerceFlowRecord(interview.ContextData["$Record"])
		if record == nil {
			record = models.SObject{}
		}
		err := e.actions.ExecuteActionDirect(ctx, actionMeta, record, user)
		if err != nil {
			if action.FaultConnector != nil {
				interview.ContextData["$Flow.FaultMessage"] = err.Error()
				return action.FaultConnector.TargetReference, nil
			}
			return "", err
		}
		if action.Connector != nil {
			return action.Connector.TargetReference, nil
		}
		return "", nil
	}

	// Preserve the existing action-call behavior for metadata-backed actions.
	inputRecord := models.SObject{}
	for _, param := range action.InputParameters {
		inputRecord[param.Name] = e.resolveValueCtx(ctx, interview, user, param.Value)
	}

	// Get action metadata
	actionMeta := e.metadata.GetActionByID(ctx, action.ActionName)
	if actionMeta == nil {
		// Try by name via the metadata service
		actions := e.metadata.GetAllActions(ctx)
		for _, a := range actions {
			if a.Name == action.ActionName || a.ID == action.ActionName {
				actionMeta = a
				break
			}
		}
	}

	if actionMeta == nil {
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = fmt.Sprintf("Action '%s' not found", action.ActionName)
			return action.FaultConnector.TargetReference, nil
		}
		return "", apperrors.NewNotFoundError("action", action.ActionName)
	}

	// Execute using ActionService.ExecuteActionDirect
	err := e.actions.ExecuteActionDirect(ctx, actionMeta, inputRecord, user)
	if err != nil {
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}

func (e *FlowExecutor) buildStandardActionMetadata(action *models.FlowActionCall, flowMeta *models.FlowMetadata) *models.ActionMetadata {
	actionType := action.ActionType
	// emailAlert is handled separately in executeActionCall via
	// executeEmailAlertAction — it needs a WorkflowAlert lookup, not the
	// emailSimple (template+recipient-in-config) contract.
	if !strings.EqualFold(actionType, "emailSimple") {
		return nil
	}

	config := make(map[string]interface{}, len(action.InputParameters))
	for _, param := range action.InputParameters {
		config[param.Name] = normalizeFlowActionInputValue(param.Value)
	}

	objectAPIName := ""
	if flowMeta != nil && flowMeta.Start != nil {
		objectAPIName = flowMeta.Start.Object
	}

	return &models.ActionMetadata{
		ID:            action.Name,
		Name:          action.ActionName,
		Label:         action.Label,
		Type:          constants.ActionTypeSendEmail,
		ObjectAPIName: objectAPIName,
		Config:        config,
	}
}

func normalizeFlowActionInputValue(value interface{}) interface{} {
	switch v := value.(type) {
	case map[string]interface{}:
		if ref, ok := v["elementReference"].(string); ok {
			return convertFlowReferenceToActionFormula(ref)
		}
		if str, ok := v["stringValue"].(string); ok {
			return str
		}
		if number, ok := v["numberValue"]; ok {
			return number
		}
		if boolean, ok := v["booleanValue"]; ok {
			return boolean
		}
		return v
	case string:
		return convertFlowReferenceToActionFormula(v)
	default:
		return value
	}
}

func convertFlowReferenceToActionFormula(ref string) string {
	if strings.HasPrefix(ref, "{!") && strings.HasSuffix(ref, "}") {
		return ref
	}
	if strings.HasPrefix(ref, "$Record.") {
		return "{!Record." + strings.TrimPrefix(ref, "$Record.") + "}"
	}
	if strings.HasPrefix(ref, "$User.") {
		return "{!User." + strings.TrimPrefix(ref, "$User.") + "}"
	}
	return ref
}

func coerceFlowRecord(value interface{}) models.SObject {
	switch record := value.(type) {
	case models.SObject:
		return record
	case map[string]interface{}:
		return models.SObject(record)
	default:
		return nil
	}
}

// executeEmailAlertAction dispatches an actionCall whose actionType is
// "emailAlert". Salesforce emailAlert actions carry a reference (via ActionName,
// formatted "ObjectApiName.AlertName") to a _System_WorkflowAlert row that holds
// the template, recipients, sender, and cc — none of these live on the
// actionCall itself, which is why routing emailAlert through the emailSimple /
// executeSendEmail path fails with "emailTemplate not specified". This method
// splits the reference, looks up the WorkflowAlert (scoped by object to avoid
// same-named alerts on different objects), and delegates to ApprovalService's
// shared send pipeline.
//
// The emailAlert action is fire-and-forget in Salesforce — send errors are
// logged inside the dispatcher and do not fault the flow. Only a missing
// WorkflowAlert (data-integrity misconfiguration) is routed through the flow's
// FaultConnector / caller error.
func (e *FlowExecutor) executeEmailAlertAction(ctx context.Context, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
	if e.approvals == nil {
		err := apperrors.NewInternalError("emailAlert dispatch unavailable: ApprovalService not wired into FlowExecutor", nil)
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	objectApiName, alertName := splitEmailAlertActionName(action.ActionName, flowMeta)
	if alertName == "" {
		err := apperrors.NewValidationError("actionName", "emailAlert action has empty actionName")
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	targetRecord := coerceFlowRecord(interview.ContextData["$Record"])
	if targetRecord == nil {
		targetRecord = models.SObject{}
	}

	if err := e.approvals.DispatchEmailAlertForFlow(ctx, objectApiName, alertName, targetRecord, user); err != nil {
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}

// splitEmailAlertActionName parses an emailAlert actionName into (objectApiName,
// alertName). Salesforce formats this as "ObjectApiName.AlertName" (e.g.
// "Account.Welcome_Email"). If the actionName has no dot, the object is
// inferred from the flow's Start.Object so old / non-standard metadata still
// resolves. A trailing-dot actionName (e.g. "Account.") is treated as
// malformed — the whole thing is returned as the alert name so the caller
// can raise a clear validation error rather than silently querying for a
// nonexistent alert.
func splitEmailAlertActionName(actionName string, flowMeta *models.FlowMetadata) (string, string) {
	actionName = strings.TrimSpace(actionName)
	if actionName == "" {
		return "", ""
	}
	// Well-formed qualified name: "Object.Name" where both sides are non-empty.
	if idx := strings.Index(actionName, "."); idx > 0 && idx < len(actionName)-1 {
		return actionName[:idx], actionName[idx+1:]
	}
	// Unqualified name (no dot at all) — fall back to the flow's Start.Object.
	// Malformed qualified names ("Account.", ".Alert") fall through here too;
	// DispatchEmailAlertForFlow's validation will surface the issue.
	objectApiName := ""
	if flowMeta != nil && flowMeta.Start != nil {
		objectApiName = flowMeta.Start.Object
	}
	return objectApiName, actionName
}

// executeFlowAction dispatches an actionCall whose actionType is "flow" —
// Salesforce's alternative to the native <subflows> element. Input parameters
// map to the child flow's input variables, output parameters assign the
// child's returned output values back into the parent's ContextData.
//
// The call is synchronous (parent blocks until child completes) and is guarded
// against direct recursion and cycles via the flowCallStackKey on ctx. The
// stack also caps overall depth at maxFlowCallDepth to bound runaway chains.
//
// Fault handling mirrors the existing executeSubflow behaviour: any error or
// non-success result from the child is routed through action.FaultConnector
// when present, otherwise the error bubbles up to abort the parent flow.
func (e *FlowExecutor) executeFlowAction(ctx context.Context, interview *models.FlowInterview, action *models.FlowActionCall, user *models.UserSession) (string, error) {
	if action.ActionName == "" {
		err := apperrors.NewValidationError("actionName", "flow action call requires an actionName (child flow API name)")
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	childCtx, cycleErr := withFlowCall(ctx, action.ActionName)
	if cycleErr != nil {
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = cycleErr.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", cycleErr
	}
	childCtx, scopeErr := e.standardActionScopedChildContext(childCtx, action.ActionName, user)
	if scopeErr != nil {
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = scopeErr.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", scopeErr
	}

	// Build child flow inputs: parameter name -> resolved value from parent
	// context. Values may be literals or elementReference strings; resolveValue
	// already unwraps the Salesforce typed-value wrappers.
	inputs := make(map[string]interface{}, len(action.InputParameters))
	for _, param := range action.InputParameters {
		if param.Name == "" {
			continue
		}
		inputs[param.Name] = e.resolveValue(interview.ContextData, param.Value)
	}

	result, err := e.ExecuteFlow(childCtx, action.ActionName, inputs, user)
	if err != nil {
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", fmt.Errorf("flow action '%s' failed: %w", action.ActionName, err)
	}
	if result == nil || !result.IsSuccess {
		msg := fmt.Sprintf("flow action '%s' returned failure", action.ActionName)
		if result != nil && len(result.Errors) > 0 {
			msg = result.Errors[0].Message
		}
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = msg
			return action.FaultConnector.TargetReference, nil
		}
		return "", fmt.Errorf("%s", msg)
	}

	// Map child outputs back to parent context. For actionType="flow",
	// Salesforce stores the parent target in the output parameter's Value
	// (typically an elementReference like "parentVar"). We resolve that to a
	// plain string via extractTypedValue so both forms are supported.
	for _, param := range action.OutputParameters {
		if param.Name == "" {
			continue
		}
		target := extractFlowOutputTarget(param.Value)
		if target == "" {
			continue
		}
		if val, ok := result.OutputValues[param.Name]; ok {
			setNestedContextValue(interview.ContextData, target, val)
		}
	}

	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}

// extractFlowOutputTarget pulls the parent-side variable reference out of an
// output parameter's Value. Salesforce serialises it either as a bare string
// ("parentVar") or as an elementReference-typed wrapper
// ({"elementReference": "parentVar"}). Anything else yields "" — the caller
// should skip the assignment rather than fault, matching Salesforce's lenient
// behaviour when an outputParameter lacks a target.
func extractFlowOutputTarget(value interface{}) string {
	unwrapped, _ := extractTypedValue(value)
	if s, ok := unwrapped.(string); ok {
		return s
	}
	return ""
}

// executeSubflow executes another flow
func (e *FlowExecutor) executeSubflow(ctx context.Context, interview *models.FlowInterview, subflow *models.FlowSubflow, user *models.UserSession) (string, error) {
	// Guard against cycles and runaway depth for native <subflows> calls too —
	// the same stack protects both actionType="flow" action calls and
	// <subflows> elements so a chain that mixes the two cannot loop forever.
	childCtx, cycleErr := withFlowCall(ctx, subflow.FlowName)
	if cycleErr != nil {
		if subflow.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = cycleErr.Error()
			return subflow.FaultConnector.TargetReference, nil
		}
		return "", cycleErr
	}
	childCtx, scopeErr := e.standardActionScopedChildContext(childCtx, subflow.FlowName, user)
	if scopeErr != nil {
		if subflow.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = scopeErr.Error()
			return subflow.FaultConnector.TargetReference, nil
		}
		return "", scopeErr
	}

	// Build inputs. For sObject inputs passed to the child flow, stamp
	// attributes.type so the child can resolve __r traversal.
	parentObjectType, _ := interview.ContextData[flowContextKeyRootObject].(string)
	inputs := make(map[string]interface{})
	for _, input := range subflow.InputAssignments {
		val := e.resolveValueCtx(ctx, interview, user, input.Value)
		if ref, ok := input.Value.(string); ok && parentObjectType != "" {
			if ref == "$Record" || ref == "$Record__Old" {
				if rec, ok := val.(models.SObject); ok {
					stampAttributesType(rec, parentObjectType)
				} else if rec, ok := val.(map[string]interface{}); ok {
					stampAttributesType(models.SObject(rec), parentObjectType)
				}
			}
		}
		inputs[input.Name] = val
	}

	// Execute the subflow
	result, err := e.ExecuteFlow(childCtx, subflow.FlowName, inputs, user)
	if err != nil {
		if subflow.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return subflow.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	if !result.IsSuccess {
		msg := fmt.Sprintf("subflow %q failed", subflow.FlowName)
		if len(result.Errors) > 0 {
			msg = result.Errors[0].Message
		}
		if subflow.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = msg
			return subflow.FaultConnector.TargetReference, nil
		}
		// No fault connector declared → subflow failure MUST propagate. A
		// parent running under RunAtomically has its tx rolled back by the
		// returned error; a non-atomic parent surfaces the child error
		// cleanly. Silently falling through to the normal Connector here
		// previously let e.g. Std_ConvertLead bookkeeping subflows fail and
		// still commit MarkLeadConverted, defeating the atomicity contract.
		// If a flow author wants the "child failure is optional" semantics,
		// they must declare a faultConnector explicitly.
		return "", fmt.Errorf("subflow %q failed without fault connector: %s", subflow.FlowName, msg)
	}

	// Store outputs
	for _, output := range subflow.OutputAssignments {
		if val, ok := result.OutputValues[output.Name]; ok {
			interview.ContextData[output.AssignToReference] = val
		}
	}

	if subflow.Connector != nil {
		return subflow.Connector.TargetReference, nil
	}
	return "", nil
}

// executeStampConvertedAnchors is a built-in invocable action that updates
// a source record with dynamic converted-anchor lookup fields. The set of
// anchor fields is resolved at runtime via
// MetadataService.ConvertedLookups(sourceObject) — tenants adding a custom
// converted-anchor lookup field get coverage with zero code change.
//
// SF-contract invariant: if the source schema declares a `ConvertedDate`
// field (Date or DateTime), the action auto-stamps it with the current
// timestamp unless the flow explicitly supplied `extraFields.ConvertedDate`.
// Lead.ConvertedDate is the canonical SF-convention name; any other SObject
// that ships this field opts into the same semantics.
//
// Expected input parameters (any may be omitted if the flow hasn't
// collected that target):
//
//	sourceObject string       — API name of the record being stamped (required)
//	sourceId     string       — record Id to update (required)
//	anchors      map[string]interface{} — { "<ObjectApiName>": "<recordId>", ... }
//	                            Each converted-anchor lookup whose ReferenceTo
//	                            matches a key in this map gets stamped with
//	                            that value.
//	extraFields  map[string]interface{} — arbitrary additional fields to stamp
//	                            on the same update (e.g. IsConverted, Status).
//	                            FLS/validation-rule enforcement flows through
//	                            persistence.Update exactly like RecordUpdate's
//	                            InputAssignments — extraFields is author-trusted
//	                            in the same sense.
//
// The action emits exactly one persistence.Update call per invocation,
// keeping atomicity under a parent RunAtomically flow.
func (e *FlowExecutor) executeStampConvertedAnchors(ctx context.Context, interview *models.FlowInterview, action *models.FlowActionCall, user *models.UserSession) (string, error) {
	fault := func(err error) (string, error) {
		if action.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return action.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	params := map[string]interface{}{}
	for _, p := range action.InputParameters {
		// deepResolveValue handles nested elementReference wrappers inside
		// literal map/slice values so the flow can pass
		//   {"Account": {elementReference: "accountId"}, "Contact": ...}
		// without authoring a chain of Assignment elements to build the
		// map first.
		params[p.Name] = e.deepResolveValue(ctx, interview, user, p.Value)
	}

	sourceObject, _ := params["sourceObject"].(string)
	sourceObject = strings.TrimSpace(sourceObject)
	sourceID, _ := params["sourceId"].(string)
	sourceID = strings.TrimSpace(sourceID)
	if sourceObject == "" || sourceID == "" {
		return fault(fmt.Errorf("stampConvertedAnchors requires sourceObject and sourceId"))
	}

	lookupFn := e.convertedLookupsFn
	if lookupFn == nil {
		if e.metadata == nil {
			return fault(fmt.Errorf("metadata service unavailable for stampConvertedAnchors"))
		}
		lookupFn = e.metadata.ConvertedLookups
	}

	anchors, _ := params["anchors"].(map[string]interface{})
	extra, _ := params["extraFields"].(map[string]interface{})

	update := models.SObject{}

	for _, field := range lookupFn(ctx, sourceObject) {
		for _, referenceTo := range field.ReferenceTo {
			if referenceTo == "" {
				continue
			}
			val, present := anchors[referenceTo]
			if !present {
				continue
			}
			str, _ := val.(string)
			if str == "" {
				continue
			}
			update[field.APIName] = str
			break
		}
	}
	for k, v := range extra {
		update[k] = v
	}

	// SF-contract: auto-stamp ConvertedDate when the source schema declares
	// that field. Salesforce guarantees Lead.ConvertedDate is populated on
	// every successful convert; the same semantics extend to any SObject
	// that ships this field. A flow may still override the value explicitly
	// via extraFields (see flow_stamp_converted_anchors_test.go).
	if f := e.lookupFieldMeta(sourceObject, stampConvertedDateField); f != nil {
		t := constants.NormalizeFieldType(string(f.Type))
		if t == constants.FieldTypeDate || t == constants.FieldTypeDatetime {
			if _, overridden := update[stampConvertedDateField]; !overridden {
				update[stampConvertedDateField] = time.Now().UTC()
			}
		}
	}

	if len(update) == 0 {
		if action.Connector != nil {
			return action.Connector.TargetReference, nil
		}
		return "", nil
	}

	if err := e.persistence.Update(ctx, sourceObject, sourceID, update, user); err != nil {
		return fault(err)
	}

	if action.Connector != nil {
		return action.Connector.TargetReference, nil
	}
	return "", nil
}
