package automation

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"

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

// setNestedContextValue writes `value` to `context[ref]`, handling dot-notation
// references like `$Record.OwnerId` by mutating the referenced sub-map in place.
//
// This is the write-side mirror of resolveNestedReference. Salesforce before-save
// record-triggered flows assign to `$Record.FieldName` expecting the sObject to
// be mutated directly; a flat-key write would leave the underlying record map
// untouched and the assignment effectively a no-op (issue #550).
//
// Behavior:
//   - Simple names (`myVar`) write the flat key, preserving existing behavior.
//   - Dotted refs walk each segment; intermediate values must be map-like
//     (models.SObject or map[string]interface{}). The final segment is set on
//     the innermost map, mutating the shared reference held by the caller.
//   - If the root key is missing or any intermediate segment is not a map,
//     the write is skipped silently. Creating intermediate maps here could
//     mask a misconfigured flow and would not propagate to the caller's
//     record anyway.
func setNestedContextValue(context map[string]interface{}, ref string, value interface{}) {
	if !strings.Contains(ref, ".") {
		context[ref] = value
		return
	}
	parts := strings.Split(ref, ".")
	// Walk to the parent of the final segment.
	var current map[string]interface{}
	if root, ok := context[parts[0]].(models.SObject); ok {
		current = root
	} else if root, ok := context[parts[0]].(map[string]interface{}); ok {
		current = root
	} else {
		return // root missing or not map-like — skip
	}
	for i := 1; i < len(parts)-1; i++ {
		next, ok := current[parts[i]].(models.SObject)
		if ok {
			current = next
			continue
		}
		nextM, ok := current[parts[i]].(map[string]interface{})
		if !ok {
			return
		}
		current = nextM
	}
	current[parts[len(parts)-1]] = value
}

// executeDecision evaluates decision rules and returns the next element
func (e *FlowExecutor) executeDecision(ctx context.Context, interview *models.FlowInterview, decision *models.FlowDecision, user *models.UserSession) (string, error) {
	for _, rule := range decision.Rules {
		matched := e.evaluateConditionsCtx(ctx, interview, user, rule.Conditions, rule.ConditionLogic)
		// Store outcome so downstream decisions can reference it
		// (Salesforce Process Builder pattern: isChanged decision outcome
		// is referenced by name in a later decision's condition).
		//
		// Skip writes that would collide with a reserved system variable
		// (`$Record`, `$Record__Old`, `$Flow.*`, `$Organization.*`, …) or
		// an empty name. The bool outcome would otherwise clobber the
		// trigger record or flow state (PR #1583 review round-1 H-5).
		if rule.Name != "" && !strings.HasPrefix(rule.Name, "$") {
			interview.ContextData[rule.Name] = matched
		}
		if matched {
			if rule.Connector != nil {
				return rule.Connector.TargetReference, nil
			}
		}
	}

	// Default path
	if decision.DefaultConnector != nil {
		return decision.DefaultConnector.TargetReference, nil
	}

	return "", nil
}

// executeAssignment assigns values to flow variables
func (e *FlowExecutor) executeAssignment(ctx context.Context, interview *models.FlowInterview, assignment *models.FlowAssignment, user *models.UserSession) (string, error) {
	for _, item := range assignment.Items {
		// Use the presence-aware resolver so we can distinguish:
		//   (value, true)  — the reference resolved (value may be nil = explicit null)
		//   (nil,  false)  — the reference could not be resolved (e.g. __r lazy-load
		//                    failed because relatedType is unknown or the related record
		//                    is absent). In this case we must NOT overwrite the target
		//                    with nil — doing so silently destroys the prior value and
		//                    is the root cause of F4 (issue #1817).
		value, resolved := e.resolveValueCtxWithPresence(ctx, interview, user, item.Value)
		targetRef := item.AssignToReference // Capture to avoid shadowing
		existing := e.currentAssignmentValueCtx(ctx, interview, user, targetRef)

		switch item.Operator {
		case "Assign", "":
			// Skip the write when the source reference could not be resolved.
			// resolved=false means "no value was produced" (lazy-load miss, absent
			// variable, etc.) — not "the author explicitly set null". Writing nil
			// in that case corrupts the target field with a ghost null.
			if !resolved {
				break
			}
			// Dotted refs (e.g. `$Record.OwnerId`) must mutate the nested
			// sObject rather than create a flat key — see setNestedContextValue.
			setNestedContextValue(interview.ContextData, targetRef, value)

		case "Add":
			// Add works for both numbers and collections
			if existingNum, ok := existing.(float64); ok {
				if addVal, ok := value.(float64); ok {
					e.setNestedReference(interview.ContextData, targetRef, existingNum+addVal)
				}
			} else if existingSlice, ok := existing.([]interface{}); ok {
				// Add to collection
				e.setNestedReference(interview.ContextData, targetRef, append(existingSlice, value))
			} else if existing == nil {
				// Initialize as collection with single item
				e.setNestedReference(interview.ContextData, targetRef, []interface{}{value})
			}

		case "AddAtStart":
			// Add to beginning of collection
			if existingSlice, ok := existing.([]interface{}); ok {
				e.setNestedReference(interview.ContextData, targetRef, append([]interface{}{value}, existingSlice...))
			} else {
				e.setNestedReference(interview.ContextData, targetRef, []interface{}{value})
			}

		case "Subtract":
			if existingNum, ok := existing.(float64); ok {
				if subVal, ok := value.(float64); ok {
					e.setNestedReference(interview.ContextData, targetRef, existingNum-subVal)
				}
			}

		case "RemoveFirst":
			// Remove first occurrence from collection
			if existingSlice, ok := existing.([]interface{}); ok {
				valueStr := fmt.Sprintf("%v", value)
				for i, elem := range existingSlice {
					if fmt.Sprintf("%v", elem) == valueStr {
						e.setNestedReference(interview.ContextData, targetRef, append(existingSlice[:i], existingSlice[i+1:]...))
						break
					}
				}
			}

		case "RemoveAll":
			// Remove all occurrences from collection
			if existingSlice, ok := existing.([]interface{}); ok {
				valueStr := fmt.Sprintf("%v", value)
				newSlice := make([]interface{}, 0)
				for _, elem := range existingSlice {
					if fmt.Sprintf("%v", elem) != valueStr {
						newSlice = append(newSlice, elem)
					}
				}
				e.setNestedReference(interview.ContextData, targetRef, newSlice)
			}

		case "RemoveAfterFirst":
			// Remove all occurrences after the first one
			if existingSlice, ok := existing.([]interface{}); ok {
				valueStr := fmt.Sprintf("%v", value)
				foundFirst := false
				newSlice := make([]interface{}, 0)
				for _, elem := range existingSlice {
					if fmt.Sprintf("%v", elem) == valueStr {
						if !foundFirst {
							newSlice = append(newSlice, elem)
							foundFirst = true
						}
					} else {
						newSlice = append(newSlice, elem)
					}
				}
				e.setNestedReference(interview.ContextData, targetRef, newSlice)
			}

		case "RemoveBeforeFirst":
			// Remove all occurrences before the first one
			if existingSlice, ok := existing.([]interface{}); ok {
				valueStr := fmt.Sprintf("%v", value)
				foundFirst := false
				newSlice := make([]interface{}, 0)
				for _, elem := range existingSlice {
					if fmt.Sprintf("%v", elem) == valueStr {
						foundFirst = true
					}
					if foundFirst {
						newSlice = append(newSlice, elem)
					}
				}
				e.setNestedReference(interview.ContextData, targetRef, newSlice)
			}
		}
	}

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

// executeLoop handles iteration over a collection
func (e *FlowExecutor) executeLoop(ctx context.Context, interview *models.FlowInterview, loop *models.FlowLoop, user *models.UserSession) (string, error) {
	// Get or initialize loop state
	loopStateKey := "_loop_" + loop.Name + "_index"
	loopIndex := 0
	_, alreadyStarted := interview.ContextData[loopStateKey]
	if idx, ok := interview.ContextData[loopStateKey].(float64); ok {
		loopIndex = int(idx)
	}

	// Get the collection
	collection, ok := interview.ContextData[loop.CollectionReference]
	if !ok {
		// No collection - exit loop
		if alreadyStarted {
			setLoopDepth(interview.ContextData, loopDepth(interview.ContextData)-1)
		}
		if loop.NoMoreValuesConnector != nil {
			return loop.NoMoreValuesConnector.TargetReference, nil
		}
		return "", nil
	}

	// Convert to slice for iteration
	var items []interface{}
	switch c := collection.(type) {
	case []interface{}:
		items = c
	case []models.SObject:
		for _, obj := range c {
			items = append(items, obj)
		}
	case []map[string]interface{}:
		for _, obj := range c {
			items = append(items, obj)
		}
	default:
		// Single item or unsupported type
		if alreadyStarted {
			setLoopDepth(interview.ContextData, loopDepth(interview.ContextData)-1)
		}
		if loop.NoMoreValuesConnector != nil {
			return loop.NoMoreValuesConnector.TargetReference, nil
		}
		return "", nil
	}

	// Check iteration order
	if loop.IterationOrder == "Desc" && loopIndex == 0 {
		// Reverse the collection for descending order (only on first iteration)
		reversed := make([]interface{}, len(items))
		for i, item := range items {
			reversed[len(items)-1-i] = item
		}
		items = reversed
		// Store reversed collection back
		interview.ContextData[loop.CollectionReference] = items
	}

	// Check if we have more items
	if loopIndex >= len(items) {
		// Clean up loop state and iteration variable.
		delete(interview.ContextData, loopStateKey)
		// When assignNextValueToReference is null (Process Builder convention),
		// the item was stored under loop.Name — remove it to prevent stale leaks
		// to post-loop elements.
		iterVar := loop.AssignNextValueToReference
		if iterVar == "" {
			iterVar = loop.Name
		}
		if iterVar != "" {
			delete(interview.ContextData, iterVar)
		}
		// Exit loop: decrement depth (only if we had incremented it on entry).
		if alreadyStarted {
			setLoopDepth(interview.ContextData, loopDepth(interview.ContextData)-1)
		}
		if loop.NoMoreValuesConnector != nil {
			return loop.NoMoreValuesConnector.TargetReference, nil
		}
		return "", nil
	}

	// First entry into this Loop — bump the nesting depth so downstream
	// `__r` resolutions can emit an N+1 warning (see resolveNestedReferenceCtx).
	if !alreadyStarted {
		setLoopDepth(interview.ContextData, loopDepth(interview.ContextData)+1)
	}

	// Assign current item to loop variable. Process Builder omits
	// assignNextValueToReference and uses the loop's Name as the variable
	// (e.g. Loop_Orgs.source__c resolves as loop.Name = "Loop_Orgs").
	iterVar := loop.AssignNextValueToReference
	if iterVar == "" {
		iterVar = loop.Name
	}
	if iterVar != "" {
		interview.ContextData[iterVar] = items[loopIndex]
	}

	// Increment loop index for next iteration
	interview.ContextData[loopStateKey] = float64(loopIndex + 1)

	// Continue to loop body
	if loop.NextValueConnector != nil {
		return loop.NextValueConnector.TargetReference, nil
	}

	return "", nil
}

// isTimeBasedWaitEvent reports whether the given EventType causes the
// interview to pause on wall-clock time rather than fire as soon as its
// conditions evaluate true. Matches Salesforce's standard alarm types.
func isTimeBasedWaitEvent(eventType string) bool {
	switch eventType {
	case "AlarmEvent", "DateRefAlarmEvent":
		return true
	default:
		return false
	}
}

// resolveWaitEventTime computes the absolute wake-up time for a time-based
// WaitEvent using its InputParameters. Supported parameter shapes (matching
// Salesforce Metadata API):
//
//   - AlarmTime                         : ISO-8601 datetime literal
//   - DateRef + TimeOffset + TimeOffsetUnit
//     DateRef resolves against interview.ContextData; TimeOffsetUnit ∈
//     {"Minutes","Hours","Days"} (case-insensitive). Negative offsets go
//     before the reference.
//
// Returns (time, true) on success, (zero, false) if the parameters are
// missing or malformed. The caller decides whether that should become a
// fault path or trigger the default connector.
func (e *FlowExecutor) resolveWaitEventTime(ctxData map[string]interface{}, event *models.FlowWaitEvent) (time.Time, bool) {
	if event == nil {
		return time.Time{}, false
	}
	params := map[string]interface{}{}
	for _, p := range event.InputParameters {
		params[p.Name] = p.Value
	}

	if raw, ok := params["AlarmTime"]; ok {
		if s, ok := raw.(string); ok && s != "" {
			if t, err := parseFlowDateTime(s); err == nil {
				return t, true
			}
		}
	}

	dateRef, _ := params["DateRef"].(string)
	offsetRaw, hasOffset := params["TimeOffset"]
	unit, _ := params["TimeOffsetUnit"].(string)

	if dateRef == "" && !hasOffset {
		return time.Time{}, false
	}

	var base time.Time
	if dateRef != "" {
		resolved := e.resolveValue(ctxData, dateRef)
		if t, ok := resolved.(time.Time); ok {
			base = t
		} else if s, ok := resolved.(string); ok && s != "" {
			if t, err := parseFlowDateTime(s); err == nil {
				base = t
			}
		}
	}
	if base.IsZero() {
		// DateRef was set but could not be resolved (null field, bad format).
		// Falling back to Now() would fire the Wait at the wrong time.
		// Return false so the caller uses the default/fault connector.
		if dateRef != "" {
			return time.Time{}, false
		}
		// No DateRef specified — relative to current time is correct.
		base = time.Now()
	}

	offsetDur, ok := parseTimeOffset(offsetRaw, unit)
	if !ok {
		return time.Time{}, false
	}
	return base.Add(offsetDur), true
}

// parseFlowDateTime accepts the subset of ISO-8601 forms Salesforce emits
// in Flow metadata. Falls back across formats and returns the parse error
// from the final attempt so callers can surface a useful message.
func parseFlowDateTime(s string) (time.Time, error) {
	formats := []string{
		time.RFC3339Nano,
		time.RFC3339,
		"2006-01-02T15:04:05",
		"2006-01-02 15:04:05",
		"2006-01-02",
	}
	var lastErr error
	for _, f := range formats {
		t, err := time.Parse(f, s)
		if err == nil {
			return t, nil
		}
		lastErr = err
	}
	return time.Time{}, lastErr
}

// parseTimeOffset converts a (numeric offset, unit) pair into a Duration.
// Accepts numeric types the JSON decoder commonly produces (float64, int,
// int64) plus numeric strings. Unknown units fall back to minutes so a
// misconfigured flow still pauses (observable) rather than fires instantly.
//
// Returns ok=false when the raw value is non-numeric (e.g., a garbled
// string) so the caller can route to FaultConnector instead of silently
// firing the wait at Now()+0. Passing nil raw returns (0, true) because
// an absent offset is a valid "wait until the DateRef" configuration.
func parseTimeOffset(raw interface{}, unit string) (time.Duration, bool) {
	if raw == nil {
		return 0, true
	}
	var n float64
	switch v := raw.(type) {
	case float64:
		n = v
	case int:
		n = float64(v)
	case int64:
		n = float64(v)
	case string:
		if _, err := fmt.Sscanf(v, "%f", &n); err != nil {
			return 0, false
		}
	default:
		return 0, false
	}

	switch strings.ToLower(unit) {
	case "days", "day":
		return time.Duration(n * float64(24*time.Hour)), true
	case "hours", "hour":
		return time.Duration(n * float64(time.Hour)), true
	case "minutes", "minute", "":
		return time.Duration(n * float64(time.Minute)), true
	default:
		return time.Duration(n * float64(time.Minute)), true
	}
}

// executeWait processes a Wait element. Returns the next element name
// (halt execution by returning "" if the interview has been paused).
//
// Algorithm:
//
//  1. Evaluate each WaitEvent:
//     - Non-time-based: fires immediately if its conditions evaluate true.
//     - Time-based: fires immediately if its target time is already in the
//     past (catch-up case) and its conditions evaluate true.
//  2. If exactly one event fires, return its connector target.
//  3. If any events are time-based with future target times, pick the
//     earliest, persist the interview as Paused (atomic transient-promotion
//     when necessary), and halt.
//  4. If no event can ever fire, take DefaultConnector; if inputs are
//     malformed, take FaultConnector.
func (e *FlowExecutor) executeWait(ctx context.Context, interview *models.FlowInterview, wait *models.FlowWait, user *models.UserSession) (string, error) {
	if wait == nil {
		return "", apperrors.NewValidationError("wait", "wait element cannot be nil")
	}

	var (
		fired       *models.FlowWaitEvent
		earliest    time.Time
		earliestEvt *models.FlowWaitEvent
		malformed   bool
		now         = time.Now()
	)

	for i := range wait.WaitEvents {
		event := &wait.WaitEvents[i]
		condsTrue := e.evaluateConditions(interview.ContextData, event.Conditions, event.ConditionLogic)

		if !isTimeBasedWaitEvent(event.EventType) {
			// Non-time-based events must already be ready to qualify.
			if condsTrue {
				if fired == nil {
					fired = event
				}
			}
			continue
		}

		target, ok := e.resolveWaitEventTime(interview.ContextData, event)
		if !ok {
			malformed = true
			continue
		}
		if !target.After(now) && condsTrue {
			// Catch-up: target already elapsed and conditions hold.
			if fired == nil {
				fired = event
			}
			continue
		}
		if earliestEvt == nil || target.Before(earliest) {
			earliest = target
			earliestEvt = event
		}
	}

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

	if earliestEvt != nil {
		// Pause until the earliest future alarm.
		if err := e.pauseForWait(ctx, interview, wait.Name, earliest, user); err != nil {
			if wait.FaultConnector != nil {
				return wait.FaultConnector.TargetReference, nil
			}
			return "", apperrors.NewInternalError("failed to pause flow interview", err)
		}
		// Halt: scheduler will resume.
		return "", nil
	}

	if malformed {
		if wait.FaultConnector != nil {
			return wait.FaultConnector.TargetReference, nil
		}
		return "", apperrors.NewValidationError("wait", "wait element has no valid events and no fault connector")
	}

	if wait.DefaultConnector != nil {
		return wait.DefaultConnector.TargetReference, nil
	}
	return "", nil
}

// transientContextKeys lists keys that must not be persisted. They hold
// large runtime-only snapshots ($Record, $Record__Old) or ephemeral loop
// / cache state that is meaningless after a resume.
// $Profile / $User / $UserRole are re-injected from the live UserSession on
// every executor entry and resume (see injectUserGlobalsIntoContext); persisting
// them would leak stale identity into the DB and waste bytes.
var transientContextKeys = []string{
	"$Record",
	"$Record__Old",
	"__r_cache",
	"_objectType",
	"_loopDepth",
	// User global variables — transient, re-injected on every executor entry.
	"$Profile",
	"$User",
	"$UserRole",
}

// marshalPersistableContext returns a JSON encoding of ctxData with all
// transient runtime keys excluded. The original map is never mutated.
func marshalPersistableContext(ctxData map[string]interface{}) ([]byte, error) {
	clean := make(map[string]interface{}, len(ctxData))
	for k, v := range ctxData {
		clean[k] = v
	}
	for _, key := range transientContextKeys {
		delete(clean, key)
	}
	// Also strip dynamic loop-index keys (e.g., _loop_MyLoop_index) which
	// are transient per-execution state. If a Wait pauses mid-loop, stale
	// indices would corrupt the loop on resume.
	for k := range clean {
		if strings.HasPrefix(k, "_loop_") && strings.HasSuffix(k, "_index") {
			delete(clean, k)
		}
	}
	return json.Marshal(clean)
}

// pauseForWait persists the interview as Paused with PausedUntil set, and
// — when the interview is still transient (in-memory only) — performs the
// insert + paused-state write in a single transaction so partial state is
// impossible. On success, interview.IsTransient is cleared and the paused
// fields are mirrored into the in-memory struct.
func (e *FlowExecutor) pauseForWait(ctx context.Context, interview *models.FlowInterview, currentElement string, pausedUntil time.Time, user *models.UserSession) error {
	if interview == nil {
		return apperrors.NewValidationError("interview", "interview cannot be nil")
	}

	pausedUntilSQL := pausedUntil.UTC().Format("2006-01-02 15:04:05")
	pausedDateSQL := time.Now().UTC().Format("2006-01-02 15:04:05")

	// Serialise ContextData sans transient fields. Uses a copy so the
	// original in-memory map is never mutated (safe for concurrent reads).
	contextJSON, err := marshalPersistableContext(interview.ContextData)
	if err != nil {
		return fmt.Errorf("failed to marshal context data: %w", err)
	}

	txErr := e.persistence.RunInTransaction(ctx, func(txCtx context.Context) (fnErr error) {
		// A panic inside the Insert / Update path must surface as an
		// error so RunInTransaction rolls back. Without this recover,
		// a crash between the INSERT and the commit could leave a
		// half-written row (the very orphan state this transaction
		// was opened to prevent).
		defer func() {
			if r := recover(); r != nil {
				fnErr = fmt.Errorf("panic during paused interview persistence: %v", r)
			}
		}()
		if interview.IsTransient {
			// Promote: ensure a stable Id, then insert the full row.
			if interview.Id == "" {
				id, err := uuid.NewRandom()
				if err != nil {
					return apperrors.NewInternalError("failed to generate interview id", err)
				}
				interview.Id = id.String()
			}
			ownerId := interview.OwnerId
			if ownerId == "" && user != nil {
				ownerId = user.ID
			}
			insert := models.SObject{
				constants.FieldID:                            interview.Id,
				constants.FieldFlowInterview_FlowVersionID:   interview.FlowVersionId,
				constants.FieldFlowInterview_InterviewLabel:  interview.InterviewLabel,
				constants.FieldFlowInterview_InterviewStatus: models.FlowInterviewStatusPaused,
				constants.FieldFlowInterview_CurrentElement:  currentElement,
				constants.FieldFlowInterview_ContextData:     string(contextJSON),
				constants.FieldFlowInterview_PausedUntil:     pausedUntilSQL,
				"PausedDate":                                 pausedDateSQL,
				"OwnerId":                                    ownerId,
			}
			if _, err := e.persistence.Insert(txCtx, constants.TableFlowInterview, insert, user); err != nil {
				return err
			}
		} else {
			updates := models.SObject{
				constants.FieldFlowInterview_InterviewStatus: models.FlowInterviewStatusPaused,
				constants.FieldFlowInterview_CurrentElement:  currentElement,
				constants.FieldFlowInterview_ContextData:     string(contextJSON),
				constants.FieldFlowInterview_PausedUntil:     pausedUntilSQL,
				"PausedDate":                                 pausedDateSQL,
			}
			if err := e.persistence.Update(txCtx, constants.TableFlowInterview, interview.Id, updates, user); err != nil {
				return err
			}
		}
		return nil
	})

	if txErr != nil {
		return txErr
	}

	// Mirror new state to the in-memory struct.
	interview.InterviewStatus = models.FlowInterviewStatusPaused
	interview.CurrentElement = currentElement
	pu := pausedUntil
	interview.PausedUntil = &pu
	interview.IsTransient = false
	return nil
}
