// Package enemyevents prepares validated D&D combat grounding for enemy-event
// extraction.
package enemyevents

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"mime"
	"sort"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	combatturncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
	occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
	npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
	scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
	npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
	sceneregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/scenedescriptions/registry"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
)

const (
	NPCRegistryReferenceSlot      = npcregistry.ReferenceSlot
	SceneDescriptionReferenceSlot = sceneregistry.ReferenceSlot
	CombatTurnReferenceSlot       = "combat_turns"
	NPCOccurrenceReferenceSlot    = "npc_occurrences"
	ReferenceMaxBytes             = 1048576
	promptProjectionMediaType     = "application/json"
)

var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
	Glossary: "Optional campaign glossary reference material used only for disambiguation.",
	Party:    "Optional party roster reference material used only for disambiguation.",
	Players:  "Optional player list reference material used only for disambiguation.",
	Roster:   "Deprecated alias for party roster reference material used only for disambiguation.",
}

func referenceSlots() []contracts.ReferenceSlot {
	slots := shared.ReferenceSlots(referenceSlotDescriptions)
	slots = append(slots,
		contracts.ReferenceSlot{
			Name:                  NPCRegistryReferenceSlot,
			Description:           "Required normalized NPC registry used only for enemy-subject grounding, never as event evidence.",
			Required:              true,
			AcceptedMediaTypes:    []string{npccodec.MediaType},
			AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
			MaxBytes:              ReferenceMaxBytes,
		},
		contracts.ReferenceSlot{
			Name:                  SceneDescriptionReferenceSlot,
			Description:           "Required scene descriptions used only to determine exact combat eligibility, never as event evidence.",
			Required:              true,
			AcceptedMediaTypes:    []string{scenecodec.MediaType},
			AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.SceneDescriptionListKind},
			MaxBytes:              ReferenceMaxBytes,
		},
		contracts.ReferenceSlot{
			Name:                  CombatTurnReferenceSlot,
			Description:           "Required combat-turn artifact used only as source-free enemy-event grounding.",
			Required:              true,
			AcceptedMediaTypes:    []string{combatturncodec.MediaType},
			AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.CombatTurnListKind},
			MaxBytes:              ReferenceMaxBytes,
		},
		contracts.ReferenceSlot{
			Name:                  NPCOccurrenceReferenceSlot,
			Description:           "Required NPC-occurrence artifact used only as source-free enemy-event grounding.",
			Required:              true,
			AcceptedMediaTypes:    []string{occurrencecodec.MediaType},
			AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCOccurrenceListKind},
			MaxBytes:              ReferenceMaxBytes,
		},
	)
	sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })
	return contracts.CloneReferenceSlots(slots)
}

// groundingResolver retains only validated, compact construction-time views.
// Per-operation generated references are decoded when supplied and never
// become static metadata.
type groundingResolver struct {
	npcs   *npcregistry.Resolver
	scenes *sceneregistry.Resolver

	combatTurns    *contracts.LLMInputMaterial
	npcOccurrences *contracts.LLMInputMaterial
}

type grounding struct {
	npcInput           contracts.LLMInputMaterial
	combatTurnInput    contracts.LLMInputMaterial
	npcOccurrenceInput contracts.LLMInputMaterial
}

func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver, error) {
	npcs, err := npcregistry.NewResolver(references)
	if err != nil {
		return nil, fmt.Errorf("prepare NPC registry grounding: %w", err)
	}
	scenes, err := sceneregistry.NewResolver(references)
	if err != nil {
		return nil, fmt.Errorf("prepare scene eligibility: %w", err)
	}
	combatTurns, err := prepareCombatTurnInput(references)
	if err != nil {
		return nil, err
	}
	npcOccurrences, err := prepareNPCOccurrenceInput(references)
	if err != nil {
		return nil, err
	}
	return &groundingResolver{
		npcs:           npcs,
		scenes:         scenes,
		combatTurns:    combatTurns,
		npcOccurrences: npcOccurrences,
	}, nil
}

func (r *groundingResolver) Resolve(references contracts.ReferenceSet) (grounding, error) {
	if r == nil {
		return grounding{}, fmt.Errorf("grounding resolver must not be nil")
	}
	npcs, err := r.npcs.Resolve(references)
	if err != nil {
		return grounding{}, fmt.Errorf("resolve NPC registry grounding: %w", err)
	}
	if !npcs.Bound() {
		return grounding{}, fmt.Errorf("NPC registry reference is required")
	}
	combatTurns, err := resolveInput(references, CombatTurnReferenceSlot, r.combatTurns, prepareCombatTurnInput)
	if err != nil {
		return grounding{}, err
	}
	npcOccurrences, err := resolveInput(references, NPCOccurrenceReferenceSlot, r.npcOccurrences, prepareNPCOccurrenceInput)
	if err != nil {
		return grounding{}, err
	}
	return grounding{
		npcInput:           npcs.PromptInput(),
		combatTurnInput:    combatTurns,
		npcOccurrenceInput: npcOccurrences,
	}, nil
}

func (r *groundingResolver) SceneMatch(references contracts.ReferenceSet, chunk *source.Chunk) (sceneregistry.ChunkMatch, error) {
	if r == nil {
		return sceneregistry.ChunkMatch{}, fmt.Errorf("grounding resolver must not be nil")
	}
	scenes, err := r.resolveScenes(references)
	if err != nil {
		return sceneregistry.ChunkMatch{}, err
	}
	return scenes.Match(chunk), nil
}

func (r *groundingResolver) resolveScenes(references contracts.ReferenceSet) (*sceneregistry.Registry, error) {
	scenes, err := r.scenes.Resolve(references)
	if err != nil {
		return nil, fmt.Errorf("resolve scene eligibility: %w", err)
	}
	if !scenes.Bound() {
		return nil, fmt.Errorf("scene descriptions reference is required")
	}
	return scenes, nil
}

func resolveInput(references contracts.ReferenceSet, slot string, seeded *contracts.LLMInputMaterial, prepare func(contracts.ReferenceSet) (*contracts.LLMInputMaterial, error)) (contracts.LLMInputMaterial, error) {
	if _, ok := references.Slots[slot]; ok {
		value, err := prepare(references)
		if err != nil {
			return contracts.LLMInputMaterial{}, err
		}
		if value == nil {
			return contracts.LLMInputMaterial{}, fmt.Errorf("%s reference is required", slot)
		}
		return value.Clone(), nil
	}
	if seeded == nil {
		return contracts.LLMInputMaterial{}, fmt.Errorf("%s reference is required", slot)
	}
	return seeded.Clone(), nil
}

func (g grounding) PromptInputs() contracts.LLMInputSet {
	return contracts.LLMInputSet{
		NPCRegistryReferenceSlot:   g.npcInput.Clone(),
		CombatTurnReferenceSlot:    g.combatTurnInput.Clone(),
		NPCOccurrenceReferenceSlot: g.npcOccurrenceInput.Clone(),
	}
}

func prepareCombatTurnInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) {
	item, ok, err := referenceItem(references, CombatTurnReferenceSlot, combatturncodec.MediaType)
	if err != nil || !ok {
		return nil, err
	}
	value, err := combatturncodec.New().Decode(item.Content)
	if err != nil {
		return nil, fmt.Errorf("decode combat-turn grounding: invalid approved combat-turn JSON")
	}
	content, err := json.Marshal(struct {
		CombatTurns []combatTurnProjection `json:"combat_turns"`
	}{CombatTurns: projectCombatTurns(value.CombatTurns)})
	if err != nil {
		return nil, fmt.Errorf("encode combat-turn grounding: %w", err)
	}
	return newPromptInput(CombatTurnReferenceSlot, content), nil
}

func prepareNPCOccurrenceInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) {
	item, ok, err := referenceItem(references, NPCOccurrenceReferenceSlot, occurrencecodec.MediaType)
	if err != nil || !ok {
		return nil, err
	}
	value, err := occurrencecodec.New().Decode(item.Content)
	if err != nil {
		return nil, fmt.Errorf("decode NPC-occurrence grounding: invalid approved NPC-occurrence JSON")
	}
	content, err := json.Marshal(struct {
		Occurrences []npcOccurrenceProjection `json:"npc_occurrences"`
	}{Occurrences: projectNPCOccurrences(value.Occurrences)})
	if err != nil {
		return nil, fmt.Errorf("encode NPC-occurrence grounding: %w", err)
	}
	return newPromptInput(NPCOccurrenceReferenceSlot, content), nil
}

func referenceItem(references contracts.ReferenceSet, slotName, expectedMediaType string) (contracts.ReferenceItem, bool, error) {
	slot, ok := references.Slots[slotName]
	if !ok {
		return contracts.ReferenceItem{}, false, nil
	}
	if len(slot.Items) == 0 {
		return contracts.ReferenceItem{}, false, nil
	}
	if len(slot.Items) != 1 {
		return contracts.ReferenceItem{}, false, fmt.Errorf("reference slot %q must contain exactly one item", slotName)
	}
	item := slot.Items[0]
	mediaType, _, err := mime.ParseMediaType(item.MediaType)
	if err != nil {
		return contracts.ReferenceItem{}, false, fmt.Errorf("reference slot %q item media type is invalid", slotName)
	}
	if !strings.EqualFold(mediaType, expectedMediaType) {
		return contracts.ReferenceItem{}, false, fmt.Errorf("reference slot %q item media type must be %s", slotName, expectedMediaType)
	}
	if len(item.Content) > ReferenceMaxBytes {
		return contracts.ReferenceItem{}, false, fmt.Errorf("reference slot %q item is %d bytes, limit %d", slotName, len(item.Content), ReferenceMaxBytes)
	}
	return item, true, nil
}

type combatTurnProjection struct {
	Actor    string             `json:"actor"`
	TurnKind dnd.CombatTurnKind `json:"turn_kind"`
}

func projectCombatTurns(turns []dnd.CombatTurn) []combatTurnProjection {
	if turns == nil {
		return nil
	}
	projection := make([]combatTurnProjection, len(turns))
	for index, turn := range turns {
		projection[index] = combatTurnProjection{Actor: turn.Actor, TurnKind: turn.TurnKind}
	}
	return projection
}

type npcOccurrenceProjection struct {
	Name string                `json:"name"`
	Kind dnd.NPCOccurrenceKind `json:"kind"`
}

func projectNPCOccurrences(occurrences []dnd.NPCOccurrence) []npcOccurrenceProjection {
	projection := make([]npcOccurrenceProjection, 0, len(occurrences))
	for _, occurrence := range occurrences {
		if occurrence.Kind == dnd.NPCOccurrenceKindCombatOpponent {
			projection = append(projection, npcOccurrenceProjection{Name: occurrence.Name, Kind: occurrence.Kind})
		}
	}
	return projection
}

func newPromptInput(name string, content []byte) *contracts.LLMInputMaterial {
	sum := sha256.Sum256(content)
	material := contracts.NewLLMInputMaterial(name, promptProjectionMediaType, content, "sha256:"+hex.EncodeToString(sum[:]), "")
	return &material
}
