// Package scenedescriptions normalizes merged D&D scene descriptions.
package scenedescriptions

import (
	"context"
	"fmt"
	"sort"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/scenedescriptions/shape"
)

const (
	Key                          = "dnd/scene-descriptions"
	normalizerPolicy             = "dnd.scene_descriptions.normalizer.v1"
	ReasonCodeProseNormalized    = "scene_description_prose_normalized"
	ReasonCodeOrderNormalized    = "scene_description_order_normalized"
	ReasonCodeDuplicateCollapsed = "scene_description_duplicate_collapsed"
)

var requiredCapabilities = []string{"merged"}
var providedCapabilities = []string{"normalized"}

var _ contracts.Normalizer[dnd.SceneDescriptionList] = (*Normalizer)(nil)
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil)

type Options struct{}
type Normalizer struct{}

func New(Options) *Normalizer { return &Normalizer{} }

func (n *Normalizer) Key() string { return Key }

func (n *Normalizer) ReferenceSlots() []contracts.ReferenceSlot { return nil }

func (n *Normalizer) ManifestMetadata() map[string]any {
	if n == nil {
		return nil
	}
	return map[string]any{"normalizer_policy": normalizerPolicy}
}

func (n *Normalizer) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
	if n == nil {
		return nil
	}
	return []pipeline.CheckpointFingerprint{{Name: "normalizer_policy", Value: normalizerPolicy}}
}

func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.SceneDescriptionList]) (contracts.TypedNormalizeResult[dnd.SceneDescriptionList], error) {
	if n == nil {
		return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("normalizer must not be nil")
	}
	if ctx == nil {
		return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("context must not be nil")
	}
	if err := ctx.Err(); err != nil {
		return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("context error before normalize: %w", err)
	}
	value, findings, err := normalizeList(req.MergeOutput.Value, req.Source)
	if err != nil {
		return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("normalize scenes: %w", err)
	}
	diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings)
	if err != nil {
		return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("collect diagnostics: %w", err)
	}
	return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{Value: value, Diagnostics: diagnosticGroups}, nil
}

type normalizedScene struct {
	scene      dnd.SceneDescription
	inputIndex int
}

func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (dnd.SceneDescriptionList, []diagnostics.Finding, error) {
	if doc == nil {
		return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source document must not be nil")
	}
	if input.Scenes == nil {
		return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scenes must be present")
	}
	if len(input.Scenes) == 0 {
		return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scenes must not be empty")
	}

	documentIndex := source.NewDocumentIndex(doc)
	records := make([]normalizedScene, len(input.Scenes))
	findings := make([]diagnostics.Finding, 0)
	for sceneIndex, scene := range input.Scenes {
		originalTitle, originalSummary := scene.Title, scene.Summary
		scene.Title = strings.TrimSpace(scene.Title)
		scene.Summary = strings.TrimSpace(scene.Summary)
		if err := shape.Validate(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{scene}}); err != nil {
			return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scenes[%d]: %w", sceneIndex, err)
		}
		if err := documentIndex.ValidateRef(scene.SourceRef); err != nil {
			return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scenes[%d].source_ref: %s", sceneIndex, diagnostics.Truncate(err.Error()))
		}
		if originalTitle != scene.Title || originalSummary != scene.Summary {
			findings = append(findings, diagnostics.Finding{
				Scope:      sceneScope(sceneIndex),
				ReasonCode: ReasonCodeProseNormalized,
				Message:    fmt.Sprintf("input index %d: title and/or summary whitespace normalized", sceneIndex),
			})
		}
		records[sceneIndex] = normalizedScene{scene: scene, inputIndex: sceneIndex}
	}

	sort.SliceStable(records, func(left, right int) bool {
		leftStart, _ := documentIndex.Position(records[left].scene.SourceRef.StartUnitID)
		rightStart, _ := documentIndex.Position(records[right].scene.SourceRef.StartUnitID)
		if leftStart != rightStart {
			return leftStart < rightStart
		}
		return records[left].scene.ID < records[right].scene.ID
	})
	for position, record := range records {
		if position == record.inputIndex {
			continue
		}
		findings = append(findings, diagnostics.Finding{
			Scope:      sceneScope(record.inputIndex),
			ReasonCode: ReasonCodeOrderNormalized,
			Message: fmt.Sprintf("input index %d moved to normalized position %d by canonical scene order",
				record.inputIndex, position),
		})
	}

	unique := make([]dnd.SceneDescription, 0, len(records))
	byID := make(map[string]dnd.SceneDescription, len(records))
	byRange := make(map[source.SourceRef]dnd.SceneDescription, len(records))
	seen := make(map[dnd.SceneDescription]int, len(records))
	for _, record := range records {
		scene := record.scene
		if previous, ok := byID[scene.ID]; ok && !identical(previous, scene) {
			return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scene ID %s has conflicting records", diagnostics.Quote(scene.ID))
		}
		if previous, ok := byRange[scene.SourceRef]; ok && !sameModelContent(previous, scene) {
			return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source range %s has conflicting records", sourceRefLabel(scene.SourceRef))
		}
		if retainedIndex, ok := seen[scene]; ok {
			findings = append(findings, diagnostics.Finding{
				Scope:      sceneScope(record.inputIndex),
				ReasonCode: ReasonCodeDuplicateCollapsed,
				Message: fmt.Sprintf("input index %d: exact duplicate scene collapsed; retained input index %d",
					record.inputIndex, retainedIndex),
			})
			continue
		}
		byID[scene.ID] = scene
		byRange[scene.SourceRef] = scene
		seen[scene] = record.inputIndex
		unique = append(unique, scene)
	}
	return dnd.SceneDescriptionList{Scenes: unique}, findings, nil
}

func sceneScope(index int) string { return fmt.Sprintf("scenes[%d]", index) }

func identical(left, right dnd.SceneDescription) bool {
	return left == right
}

func sameModelContent(left, right dnd.SceneDescription) bool {
	return left.Kind == right.Kind && left.Title == right.Title && left.Summary == right.Summary
}

func sourceRefLabel(ref source.SourceRef) string {
	return fmt.Sprintf("%s:%d-%d", diagnostics.Quote(ref.SourceID), ref.StartUnitID, ref.EndUnitID)
}

func ModuleSpec() pipeline.ModuleSpec {
	return pipeline.ModuleSpec{
		Key:            Key,
		Stage:          pipeline.StageNormalize,
		ExecutionClass: contracts.ExecutionClassDeterministic,
		Requires:       append([]string(nil), requiredCapabilities...),
		Provides:       append([]string(nil), providedCapabilities...),
		ArtifactKind:   dnd.SceneDescriptionListKind,
	}
}

func Register(registry *pipeline.NormalizerRegistry) error {
	return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.SceneDescriptionList], error) {
		options, err := DecodeOptions(request.Options)
		if err != nil {
			return nil, err
		}
		return New(options), nil
	})
}

func DecodeOptions(options map[string]any) (Options, error) {
	if err := pipeline.RejectUnknownOptions(options); err != nil {
		return Options{}, normalizerErrorf("%w", err)
	}
	return Options{}, nil
}

func validateOptions(options map[string]any) error { _, err := DecodeOptions(options); return err }

func normalizerErrorf(format string, args ...any) error {
	return fmt.Errorf("dnd scene descriptions normalizer: "+format, args...)
}
