package pipeline

import (
	"fmt"
	"reflect"
	"sort"
	"strings"

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

// ArtifactEvidenceProjector returns the direct source references represented
// by one normalized artifact value.
type ArtifactEvidenceProjector[T any] func(T) []source.SourceRef

// ArtifactEvidenceRegistry keeps the typed projection boundary private while
// allowing preparation and execution to discover registered capabilities.
type ArtifactEvidenceRegistry struct {
	entries map[contracts.ArtifactKind]artifactEvidenceEntry
}

type artifactEvidenceEntry struct {
	valueType reflect.Type
	project   func(any) ([]source.SourceRef, error)
}

func NewArtifactEvidenceRegistry() *ArtifactEvidenceRegistry {
	return &ArtifactEvidenceRegistry{entries: make(map[contracts.ArtifactKind]artifactEvidenceEntry)}
}

// RegisterArtifactEvidence registers one evidence projector for an artifact
// kind. Projectors are invoked only after an exact Go-type check.
func RegisterArtifactEvidence[T any](registry *ArtifactEvidenceRegistry, kind contracts.ArtifactKind, projector ArtifactEvidenceProjector[T]) error {
	if registry == nil {
		return fmt.Errorf("artifact evidence registry must not be nil")
	}
	if projector == nil {
		return fmt.Errorf("artifact evidence projector must not be nil")
	}
	kind = normalizeArtifactKind(kind)
	if kind == "" {
		return fmt.Errorf("artifact evidence kind must not be empty")
	}
	if _, ok := registry.entries[kind]; ok {
		return fmt.Errorf("artifact evidence %q is already registered", kind)
	}
	valueType := reflect.TypeFor[T]()
	if registry.entries == nil {
		registry.entries = make(map[contracts.ArtifactKind]artifactEvidenceEntry)
	}
	registry.entries[kind] = artifactEvidenceEntry{
		valueType: valueType,
		project: func(value any) ([]source.SourceRef, error) {
			actualType := reflect.TypeOf(value)
			if actualType != valueType {
				return nil, newArtifactEvidenceTypeError(kind, valueType, actualType)
			}
			typed, ok := value.(T)
			if !ok {
				return nil, newArtifactEvidenceTypeError(kind, valueType, actualType)
			}
			return append([]source.SourceRef(nil), projector(typed)...), nil
		},
	}
	return nil
}

func (r *ArtifactEvidenceRegistry) RegisteredKinds() []contracts.ArtifactKind {
	if r == nil || len(r.entries) == 0 {
		return nil
	}
	kinds := make([]contracts.ArtifactKind, 0, len(r.entries))
	for kind := range r.entries {
		kinds = append(kinds, kind)
	}
	sort.Slice(kinds, func(i, j int) bool { return kinds[i] < kinds[j] })
	return kinds
}

// Project returns independently owned direct references for a registered
// artifact value.
func (r *ArtifactEvidenceRegistry) Project(kind contracts.ArtifactKind, value any) ([]source.SourceRef, error) {
	entry, normalizedKind, err := r.entry(kind)
	if err != nil {
		return nil, err
	}
	refs, err := entry.project(value)
	if err != nil {
		return nil, fmt.Errorf("project artifact evidence %q: %w", normalizedKind, err)
	}
	return append([]source.SourceRef(nil), refs...), nil
}

func (r *ArtifactEvidenceRegistry) entry(kind contracts.ArtifactKind) (artifactEvidenceEntry, contracts.ArtifactKind, error) {
	if r == nil {
		return artifactEvidenceEntry{}, "", fmt.Errorf("artifact evidence registry must not be nil")
	}
	kind = normalizeArtifactKind(kind)
	if kind == "" {
		return artifactEvidenceEntry{}, "", fmt.Errorf("artifact evidence kind must not be empty")
	}
	entry, ok := r.entries[kind]
	if !ok {
		return artifactEvidenceEntry{}, kind, fmt.Errorf("artifact evidence %q is not registered", kind)
	}
	return entry, kind, nil
}

func (r *ArtifactEvidenceRegistry) valueType(kind contracts.ArtifactKind) (reflect.Type, bool) {
	if r == nil {
		return nil, false
	}
	entry, ok := r.entries[normalizeArtifactKind(kind)]
	if !ok {
		return nil, false
	}
	return entry.valueType, true
}

func newArtifactEvidenceTypeError(kind contracts.ArtifactKind, expected, actual reflect.Type) error {
	actualName := "<nil>"
	if actual != nil {
		actualName = actual.String()
	}
	return fmt.Errorf("project artifact evidence %q: expected exact Go type %s, got %s", kind, expected, actualName)
}

func normalizeEvidenceLaneIDs(values []string) ([]string, error) {
	if len(values) == 0 {
		return nil, fmt.Errorf("evidence lane ids must not be empty")
	}
	seen := make(map[string]struct{}, len(values))
	lanes := make([]string, 0, len(values))
	for _, raw := range values {
		lane := strings.TrimSpace(raw)
		if lane == "" {
			return nil, fmt.Errorf("evidence lane id must not be empty")
		}
		if _, ok := seen[lane]; ok {
			return nil, fmt.Errorf("evidence lane id %q is duplicated", lane)
		}
		seen[lane] = struct{}{}
		lanes = append(lanes, lane)
	}
	sort.Strings(lanes)
	return lanes, nil
}
