package workflow

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

	"switchboard-core/models"
)

// ── Conditional Routing Engine ───────────────
//
// Evaluates branch_rules[] against accumulated stage_data to determine
// which stage to advance to. Falls back to ordinal + 1 when no rules
// are defined or none match.

// Condition is a single routing rule within branch_rules.
type Condition struct {
	Field       string `json:"field"`
	Op          string `json:"op"`
	Value       any    `json:"value"`
	TargetStage string `json:"target_stage"` // stage name or ordinal as string
}

// StageConfig holds non-routing config from stage_config JSON.
type StageConfig struct {
	AutoAssign   string              `json:"auto_assign,omitempty"`
	RequiredRole string              `json:"required_role,omitempty"`
	Validation   *ValidationConfig   `json:"validation,omitempty"`
	OnAdvance    *OnAdvanceReference `json:"on_advance,omitempty"`
}

// ValidationConfig defines multi-party sign-off requirements for a stage.
type ValidationConfig struct {
	RequiredApprovals int    `json:"required_approvals"`           // e.g. 2
	RequiredRole      string `json:"required_role,omitempty"`      // role needed to sign off
	RejectAction      string `json:"reject_action,omitempty"`      // "cancel" | stage name
}

// ParseStageConfig unmarshals a stage_config JSON blob into StageConfig.
// Returns zero-value StageConfig on empty/invalid input.
func ParseStageConfig(raw json.RawMessage) StageConfig {
	var sc StageConfig
	if len(raw) == 0 || string(raw) == "{}" || string(raw) == "null" {
		return sc
	}
	_ = json.Unmarshal(raw, &sc)
	return sc
}

// OnAdvanceReference points to a Starlark hook for on_advance processing.
type OnAdvanceReference struct {
	PackageID  string `json:"package_id"`
	EntryPoint string `json:"entry_point"`
}

// ResolveNextStage evaluates branch_rules from the current stage against
// accumulated stageData. Returns the target stage ordinal.
// Falls back to currentStage + 1 if no rules match or none are defined.
func ResolveNextStage(stages []models.WorkflowStage, currentStage int, stageData json.RawMessage) (int, error) {
	if currentStage < 0 || currentStage >= len(stages) {
		return currentStage + 1, nil
	}

	stage := stages[currentStage]

	var rules []Condition
	if len(stage.BranchRules) > 0 && string(stage.BranchRules) != "[]" {
		_ = json.Unmarshal(stage.BranchRules, &rules)
	}

	if len(rules) == 0 {
		return currentStage + 1, nil
	}

	var data map[string]any
	if len(stageData) > 0 {
		_ = json.Unmarshal(stageData, &data)
	}
	if data == nil {
		data = map[string]any{}
	}

	for _, cond := range rules {
		if evaluateCondition(cond, data) {
			target, err := resolveTarget(stages, cond.TargetStage)
			if err != nil {
				return 0, fmt.Errorf("condition matched but target invalid: %w", err)
			}
			return target, nil
		}
	}

	return currentStage + 1, nil
}

// ResolveStageByName finds a stage ordinal by name (case-insensitive).
func ResolveStageByName(stages []models.WorkflowStage, name string) (int, error) {
	lower := strings.ToLower(strings.TrimSpace(name))
	for _, s := range stages {
		if strings.ToLower(s.Name) == lower {
			return s.Ordinal, nil
		}
	}
	return 0, fmt.Errorf("stage %q not found", name)
}

// resolveTarget resolves a target_stage string to an ordinal.
// Accepts either a stage name or a numeric ordinal string.
func resolveTarget(stages []models.WorkflowStage, target string) (int, error) {
	// Try as stage name first
	ordinal, err := ResolveStageByName(stages, target)
	if err == nil {
		return ordinal, nil
	}

	// Try as numeric ordinal
	var n int
	if _, err := fmt.Sscanf(target, "%d", &n); err == nil {
		if n >= 0 && n < len(stages) {
			return n, nil
		}
		return 0, fmt.Errorf("ordinal %d out of range (0..%d)", n, len(stages)-1)
	}

	return 0, fmt.Errorf("cannot resolve target stage %q", target)
}

// evaluateCondition checks if a single condition matches against data.
func evaluateCondition(cond Condition, data map[string]any) bool {
	val, exists := data[cond.Field]

	switch cond.Op {
	case "exists":
		return exists
	case "not_exists":
		return !exists
	}

	if !exists {
		return false
	}

	switch cond.Op {
	case "eq":
		return compareEq(val, cond.Value)
	case "neq":
		return !compareEq(val, cond.Value)
	case "gt":
		return compareNum(val, cond.Value) > 0
	case "lt":
		return compareNum(val, cond.Value) < 0
	case "gte":
		return compareNum(val, cond.Value) >= 0
	case "lte":
		return compareNum(val, cond.Value) <= 0
	case "in":
		return compareIn(val, cond.Value)
	case "contains":
		return compareContains(val, cond.Value)
	default:
		return false
	}
}

// compareEq does loose equality: normalizes both sides to string for comparison.
func compareEq(a, b any) bool {
	return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
}

// compareNum extracts float64 from both sides and returns -1, 0, or 1.
// Returns 0 if either side is not numeric.
func compareNum(a, b any) int {
	af := toFloat(a)
	bf := toFloat(b)
	if af == nil || bf == nil {
		return 0
	}
	switch {
	case *af < *bf:
		return -1
	case *af > *bf:
		return 1
	default:
		return 0
	}
}

func toFloat(v any) *float64 {
	switch n := v.(type) {
	case float64:
		return &n
	case int:
		f := float64(n)
		return &f
	case json.Number:
		if f, err := n.Float64(); err == nil {
			return &f
		}
	case string:
		var f float64
		if _, err := fmt.Sscanf(n, "%f", &f); err == nil {
			return &f
		}
	}
	return nil
}

// compareIn checks if val is in the list (cond.Value must be []any).
func compareIn(val, list any) bool {
	arr, ok := list.([]any)
	if !ok {
		return false
	}
	vs := fmt.Sprintf("%v", val)
	for _, item := range arr {
		if fmt.Sprintf("%v", item) == vs {
			return true
		}
	}
	return false
}

// compareContains checks if val (as string) contains the target substring.
func compareContains(val, target any) bool {
	s := fmt.Sprintf("%v", val)
	t := fmt.Sprintf("%v", target)
	return strings.Contains(s, t)
}
