package core

import (
	"fmt"
	"regexp"
)

// functionIDPattern matches registry IDs in `<name>_<lang>_<domain>` form.
// Compiled once and reused across validations.
var functionIDPattern = regexp.MustCompile(`^[a-z0-9_]+_[a-z]+_[a-z]+$`)

// DagValidate validates a DagDefinition for structural correctness.
// Checks: steps have name/ID, no duplicate names/IDs, all depends reference
// existing steps, no dependency cycles. On success, computes topological levels.
// Returns warnings for steps with both command and script set.
func DagValidate(dag DagDefinition) DagValidationResult {
	result := DagValidationResult{Valid: true}

	// Build name/ID sets and check for missing identifiers and duplicates.
	seen := make(map[string]bool)
	for i, step := range dag.Steps {
		ref := stepRef(step)
		if ref == "" {
			result.Errors = append(result.Errors,
				fmt.Sprintf("step[%d]: must have name or id", i))
			result.Valid = false
			continue
		}
		if seen[ref] {
			result.Errors = append(result.Errors,
				fmt.Sprintf("step[%d]: duplicate name/id %q", i, ref))
			result.Valid = false
		}
		seen[ref] = true

		// Warning: command and script both set.
		if step.Command != "" && step.Script != "" {
			result.Warnings = append(result.Warnings,
				fmt.Sprintf("step %q: has both command and script", ref))
		}

		// Function-step validation.
		if step.Function != "" {
			if !functionIDPattern.MatchString(step.Function) {
				result.Errors = append(result.Errors,
					fmt.Sprintf("step %s: invalid function id format: %s", ref, step.Function))
				result.Valid = false
			}
			if step.Command != "" || step.Script != "" {
				result.Warnings = append(result.Warnings,
					fmt.Sprintf("step %s: function takes precedence; command/script ignored", ref))
			}
		}
	}

	if !result.Valid {
		return result
	}

	// Check that all depends reference existing steps.
	for _, step := range dag.Steps {
		for _, dep := range step.Depends {
			if !seen[dep] {
				result.Errors = append(result.Errors,
					fmt.Sprintf("step %q: depends on unknown step %q", stepRef(step), dep))
				result.Valid = false
			}
		}
	}

	if !result.Valid {
		return result
	}

	// Topological sort with Kahn's — detects cycles and computes levels.
	levels, err := DagTopoSort(dag.Steps)
	if err != nil {
		result.Errors = append(result.Errors, fmt.Sprintf("cycle detected: %v", err))
		result.Valid = false
		return result
	}

	// Convert [][]DagStep to [][]string for the result.
	strLevels := make([][]string, len(levels))
	for i, level := range levels {
		refs := make([]string, len(level))
		for j, s := range level {
			refs[j] = stepRef(s)
		}
		strLevels[i] = refs
	}
	result.Levels = strLevels

	return result
}

// stepRef returns the canonical reference for a step (ID preferred, then Name).
func stepRef(s DagStep) string {
	if s.ID != "" {
		return s.ID
	}
	return s.Name
}
