package config

import (
	"fmt"
	"regexp"
	"sort"

	"gopkg.in/yaml.v3"
)

var varKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9]{1,8}$`)

// MatrixConfig represents the swap matrix configuration block.
type MatrixConfig struct {
	Var        map[string]string `yaml:"vars"`
	EvictCosts map[string]int    `yaml:"evict_costs"`
	Sets       OrderedSets       `yaml:"sets"`

	// populated by ValidateMatrix; not settable from yaml
	ExpandedSets []ExpandedSet `yaml:"-"`
}

// SetEntry is a single named set with its DSL expression.
type SetEntry struct {
	Name string
	DSL  string
}

// OrderedSets preserves YAML definition order of sets (used for tie-breaking).
type OrderedSets []SetEntry

func (os *OrderedSets) UnmarshalYAML(value *yaml.Node) error {
	if value.Kind != yaml.MappingNode {
		return fmt.Errorf("sets must be a mapping")
	}

	entries := make([]SetEntry, 0, len(value.Content)/2)
	for i := 0; i < len(value.Content); i += 2 {
		keyNode := value.Content[i]
		valueNode := value.Content[i+1]

		var name string
		if err := keyNode.Decode(&name); err != nil {
			return fmt.Errorf("failed to decode set name: %w", err)
		}

		var dsl string
		if err := valueNode.Decode(&dsl); err != nil {
			return fmt.Errorf("failed to decode DSL for set %q: %w", name, err)
		}

		entries = append(entries, SetEntry{Name: name, DSL: dsl})
	}

	*os = entries
	return nil
}

// ExpandedSet is one valid combination of concurrent models (real model names).
type ExpandedSet struct {
	SetName string
	DSL     string
	Models  []string // real model names, sorted
}

// ValidateMatrix validates the matrix config and returns all expanded sets.
func ValidateMatrix(matrix MatrixConfig, models map[string]ModelConfig) ([]ExpandedSet, error) {
	if len(matrix.Sets) == 0 {
		return nil, fmt.Errorf("matrix must define at least one set")
	}

	if len(matrix.Var) == 0 {
		return nil, fmt.Errorf("matrix must define at least one var")
	}

	// Validate var entries
	if matrix.Var != nil {
		for id, modelName := range matrix.Var {
			if !varKeyPattern.MatchString(id) {
				return nil, fmt.Errorf("var key %q must be alphanumeric and 1-8 characters", id)
			}
			if _, exists := models[modelName]; !exists {
				return nil, fmt.Errorf("var key %q references unknown model %q", id, modelName)
			}
		}
	}

	// Validate evict_costs
	if matrix.EvictCosts != nil {
		for key, cost := range matrix.EvictCosts {
			if cost <= 0 {
				return nil, fmt.Errorf("evict_cost for %q must be a positive integer, got %d", key, cost)
			}
			if _, ok := matrix.Var[key]; !ok {
				return nil, fmt.Errorf("evict_costs: unknown var ID %q", key)
			}
		}
	}

	// Build dependency graph for +ref topological sort
	setNames := make(map[string]bool)
	for _, entry := range matrix.Sets {
		setNames[entry.Name] = true
	}

	deps := make(map[string][]string) // setName -> set names it depends on
	for _, entry := range matrix.Sets {
		refs, err := extractRefs(entry.DSL)
		if err != nil {
			return nil, fmt.Errorf("set %q: %w", entry.Name, err)
		}
		for _, ref := range refs {
			if !setNames[ref] {
				return nil, fmt.Errorf("set %q references undefined set %q", entry.Name, ref)
			}
		}
		deps[entry.Name] = refs
	}

	// Topological sort with cycle detection
	order, err := topologicalSort(matrix.Sets, deps)
	if err != nil {
		return nil, err
	}

	// Expand sets in topological order
	resolvedRefs := make(map[string][][]string) // set name -> expanded alias-level combos
	var allExpanded []ExpandedSet
	totalCombinations := 0

	// Build ordered map for efficient lookup
	setDSL := make(map[string]string)
	for _, entry := range matrix.Sets {
		setDSL[entry.Name] = entry.DSL
	}

	for _, name := range order {
		dsl := setDSL[name]
		combos, err := ParseAndExpandDSL(dsl, resolvedRefs)
		if err != nil {
			return nil, fmt.Errorf("set %q: %w", name, err)
		}

		resolvedRefs[name] = combos

		// Resolve var IDs to real model names
		for _, combo := range combos {
			resolved := make([]string, len(combo))
			for i, ident := range combo {
				realName, ok := matrix.Var[ident]
				if !ok {
					return nil, fmt.Errorf("set %q: unknown var ID %q", name, ident)
				}
				resolved[i] = realName
			}
			sort.Strings(resolved)
			allExpanded = append(allExpanded, ExpandedSet{
				SetName: name,
				DSL:     dsl,
				Models:  resolved,
			})
		}

		totalCombinations += len(combos)
		if totalCombinations > maxDSLExpansions {
			return nil, fmt.Errorf("total expanded combinations (%d) exceed limit of %d", totalCombinations, maxDSLExpansions)
		}
	}

	return allExpanded, nil
}

// topologicalSort returns set names in dependency order.
// Returns an error if a cycle is detected.
func topologicalSort(sets OrderedSets, deps map[string][]string) ([]string, error) {
	// States: 0 = unvisited, 1 = visiting, 2 = visited
	state := make(map[string]int)
	var order []string

	var visit func(name string) error
	visit = func(name string) error {
		switch state[name] {
		case 1:
			return fmt.Errorf("circular reference detected involving set %q", name)
		case 2:
			return nil
		}
		state[name] = 1

		for _, dep := range deps[name] {
			if err := visit(dep); err != nil {
				return err
			}
		}

		state[name] = 2
		order = append(order, name)
		return nil
	}

	// Visit in definition order for deterministic output
	for _, entry := range sets {
		if state[entry.Name] == 0 {
			if err := visit(entry.Name); err != nil {
				return nil, err
			}
		}
	}

	return order, nil
}

// ResolvedEvictCosts returns a map of real model name -> evict cost,
// resolving var IDs. Models not listed default to 1.
func (m *MatrixConfig) ResolvedEvictCosts() map[string]int {
	costs := make(map[string]int)
	if m.EvictCosts == nil {
		return costs
	}
	for key, cost := range m.EvictCosts {
		// Resolve var ID if present
		if realName, ok := m.Var[key]; ok {
			costs[realName] = cost
		} else {
			costs[key] = cost
		}
	}
	return costs
}
