package pipeline

import (
	"fmt"
	"sort"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)

type ModuleStage string

const (
	StageInput     ModuleStage = "input"
	StageChunk     ModuleStage = "chunk"
	StageExtract   ModuleStage = "extract"
	StageMerge     ModuleStage = "merge"
	StageNormalize ModuleStage = "normalize"
	StageValidate  ModuleStage = "validate"
	StageOutput    ModuleStage = "output"
)

type ModuleSpec struct {
	Key                string
	Stage              ModuleStage
	ExecutionClass     contracts.ExecutionClass
	CorrectionProtocol contracts.CorrectionProtocol
	ArtifactKind       contracts.ArtifactKind
	Provides           []string
	Requires           []string
	ReferenceSlots     []contracts.ReferenceSlot
}

func normalizeModuleSpec(spec ModuleSpec) ModuleSpec {
	executionClass := contracts.ExecutionClass(strings.TrimSpace(string(spec.ExecutionClass)))
	correctionProtocol := contracts.CorrectionProtocol(strings.TrimSpace(string(spec.CorrectionProtocol)))
	return ModuleSpec{
		Key:                strings.TrimSpace(spec.Key),
		Stage:              spec.Stage,
		ExecutionClass:     executionClass,
		CorrectionProtocol: correctionProtocol,
		ArtifactKind:       normalizeArtifactKind(spec.ArtifactKind),
		Provides:           normalizeCapabilities(spec.Provides),
		Requires:           normalizeCapabilities(spec.Requires),
		ReferenceSlots:     normalizeReferenceSlots(spec.ReferenceSlots),
	}
}

func normalizeCapabilities(values []string) []string {
	if len(values) == 0 {
		return nil
	}

	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		normalized := strings.TrimSpace(value)
		if normalized == "" {
			continue
		}
		seen[normalized] = struct{}{}
	}
	if len(seen) == 0 {
		return nil
	}

	capabilities := make([]string, 0, len(seen))
	for value := range seen {
		capabilities = append(capabilities, value)
	}
	sort.Strings(capabilities)
	return capabilities
}

func cloneModuleSpec(spec ModuleSpec) ModuleSpec {
	return ModuleSpec{
		Key:                spec.Key,
		Stage:              spec.Stage,
		ExecutionClass:     spec.ExecutionClass,
		CorrectionProtocol: spec.CorrectionProtocol,
		ArtifactKind:       spec.ArtifactKind,
		Provides:           append([]string(nil), spec.Provides...),
		Requires:           append([]string(nil), spec.Requires...),
		ReferenceSlots:     contracts.CloneReferenceSlots(spec.ReferenceSlots),
	}
}

func validateModuleSpec(kind string, expectedStage ModuleStage, spec ModuleSpec) error {
	if spec.Key == "" {
		return fmt.Errorf("%s key must not be empty", kind)
	}
	if spec.Stage != expectedStage {
		return fmt.Errorf("%s %q must use %q stage, got %q", kind, spec.Key, expectedStage, spec.Stage)
	}
	if spec.ExecutionClass == "" {
		return fmt.Errorf("%s %q execution class must not be empty", kind, spec.Key)
	}
	if spec.ExecutionClass != contracts.ExecutionClassDeterministic && spec.ExecutionClass != contracts.ExecutionClassLLMBacked {
		return fmt.Errorf("%s %q has unsupported execution class %q", kind, spec.Key, spec.ExecutionClass)
	}
	if spec.CorrectionProtocol != "" {
		if err := spec.CorrectionProtocol.Validate(); err != nil {
			return fmt.Errorf("%s %q correction protocol: %w", kind, spec.Key, err)
		}
		if spec.ExecutionClass != contracts.ExecutionClassLLMBacked {
			return fmt.Errorf("%s %q correction protocol requires an LLM-backed execution class", kind, spec.Key)
		}
		switch spec.Stage {
		case StageChunk, StageExtract, StageMerge, StageNormalize:
		default:
			return fmt.Errorf("%s %q correction protocol is not supported for %q stage", kind, spec.Key, spec.Stage)
		}
	}
	if spec.ArtifactKind != "" && spec.Stage != StageExtract && spec.Stage != StageMerge && spec.Stage != StageNormalize {
		return fmt.Errorf("%s %q must not declare an artifact kind", kind, spec.Key)
	}
	if !referenceSlotStage(spec.Stage) && len(spec.ReferenceSlots) > 0 {
		return fmt.Errorf("%s %q must not declare reference slots", kind, spec.Key)
	}
	if err := validateReferenceSlots(spec.ReferenceSlots); err != nil {
		return fmt.Errorf("%s %q reference slots: %w", kind, spec.Key, err)
	}
	return nil
}

func referenceSlotStage(stage ModuleStage) bool {
	return stage == StageChunk || stage == StageExtract || stage == StageMerge || stage == StageNormalize
}

func sortedRegistryKeys[C any](constructors map[string]C) []string {
	if len(constructors) == 0 {
		return nil
	}

	keys := make([]string, 0, len(constructors))
	for key := range constructors {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func sortArtifactKinds(kinds []contracts.ArtifactKind) {
	sort.Slice(kinds, func(i, j int) bool { return kinds[i] < kinds[j] })
}

func normalizeReferenceSlots(slots []contracts.ReferenceSlot) []contracts.ReferenceSlot {
	if len(slots) == 0 {
		return nil
	}

	normalized := make([]contracts.ReferenceSlot, 0, len(slots))
	for _, slot := range slots {
		slot.Name = strings.TrimSpace(slot.Name)
		slot.Description = strings.TrimSpace(slot.Description)
		slot.AcceptedMediaTypes = normalizeStringSet(slot.AcceptedMediaTypes)
		slot.AcceptedArtifactKinds = normalizeArtifactKinds(slot.AcceptedArtifactKinds)
		normalized = append(normalized, slot)
	}
	sort.SliceStable(normalized, func(i, j int) bool {
		return normalized[i].Name < normalized[j].Name
	})
	return normalized
}

func normalizeArtifactKinds(values []contracts.ArtifactKind) []contracts.ArtifactKind {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[contracts.ArtifactKind]struct{}, len(values))
	containsEmpty := false
	for _, value := range values {
		value = normalizeArtifactKind(value)
		if value != "" {
			seen[value] = struct{}{}
		} else {
			containsEmpty = true
		}
	}
	if len(seen) == 0 && !containsEmpty {
		return nil
	}
	result := make([]contracts.ArtifactKind, 0, len(seen))
	for value := range seen {
		result = append(result, value)
	}
	if containsEmpty {
		result = append(result, "")
	}
	sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
	return result
}

func normalizeStringSet(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		normalized := strings.TrimSpace(value)
		if normalized == "" {
			continue
		}
		seen[normalized] = struct{}{}
	}
	if len(seen) == 0 {
		return nil
	}

	out := make([]string, 0, len(seen))
	for value := range seen {
		out = append(out, value)
	}
	sort.Strings(out)
	return out
}

func validateReferenceSlots(slots []contracts.ReferenceSlot) error {
	seen := make(map[string]struct{}, len(slots))
	for i, slot := range slots {
		if slot.Name == "" {
			return fmt.Errorf("slot[%d].name must not be empty", i)
		}
		if _, ok := seen[slot.Name]; ok {
			return fmt.Errorf("slot name %q is duplicated", slot.Name)
		}
		seen[slot.Name] = struct{}{}
		if slot.MaxBytes < 0 {
			return fmt.Errorf("slot %q max_bytes must not be negative", slot.Name)
		}
		for _, kind := range slot.AcceptedArtifactKinds {
			if normalizeArtifactKind(kind) == "" {
				return fmt.Errorf("slot %q accepted artifact kind must not be empty", slot.Name)
			}
		}
	}
	return nil
}
