// Package registry resolves scene-description artifacts into immutable combat
// eligibility data.
package registry

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

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
)

const (
	ReferenceSlot   = "scene_descriptions"
	MaxBytes        = 1048576
	emptyProjection = `{"scenes":[]}`
)

// MatchState identifies how a chunk relates to the resolved scene records.
type MatchState string

const (
	MatchExact      MatchState = "exact"
	MatchMissing    MatchState = "missing"
	MatchMismatched MatchState = "mismatched"
)

// ChunkMatch describes whether a scene record exactly covers a chunk. Kind is
// populated only for an exact match.
type ChunkMatch struct {
	State MatchState
	Kind  dnd.SceneKind
}

type sceneEligibility struct {
	sourceID    string
	startUnitID int
	endUnitID   int
	kind        dnd.SceneKind
}

// Registry is an immutable, validated eligibility view. It retains no scene
// prose, raw reference bytes, or reference provenance.
type Registry struct {
	bound      bool
	digest     string
	scenesByID map[string]sceneEligibility
}

// Resolver holds an immutable construction-time view and memoizes immutable
// operation-time views by raw reference identity and eligibility digest.
type Resolver struct {
	seeded *Registry

	mu       sync.Mutex
	cache    map[string]*Registry
	rawCache map[string]*Registry
}

// NewResolver validates a materialized construction-time scene reference. An
// empty slot is permitted because a generated reference is supplied only at
// operation time.
func NewResolver(references contracts.ReferenceSet) (*Resolver, error) {
	seeded, err := Resolve(constructionReferences(references))
	if err != nil {
		return nil, err
	}
	return &Resolver{
		seeded:   seeded,
		cache:    make(map[string]*Registry),
		rawCache: make(map[string]*Registry),
	}, nil
}

func constructionReferences(references contracts.ReferenceSet) contracts.ReferenceSet {
	slot, ok := references.Slots[ReferenceSlot]
	if !ok || len(slot.Items) > 0 {
		return references
	}
	cloned := contracts.ReferenceSet{Slots: make(map[string]contracts.ResolvedReferenceSlot, len(references.Slots))}
	for name, value := range references.Slots {
		cloned.Slots[name] = value
	}
	delete(cloned.Slots, ReferenceSlot)
	return cloned
}

// Seeded returns the construction-time eligibility view.
func (r *Resolver) Seeded() *Registry {
	if r == nil {
		return nil
	}
	return r.seeded
}

// Resolve returns the generated operation-time view when the scene slot is
// present, otherwise it returns the construction-time view.
func (r *Resolver) Resolve(references contracts.ReferenceSet) (*Registry, error) {
	if r == nil {
		return Resolve(references)
	}
	if _, ok := references.Slots[ReferenceSlot]; !ok {
		return r.seeded, nil
	}

	slot := references.Slots[ReferenceSlot]
	rawKey := ""
	if len(slot.Items) == 1 {
		rawKey = rawReferenceKey(slot.Items[0])
	}

	r.mu.Lock()
	defer r.mu.Unlock()
	if rawKey != "" {
		if cached, ok := r.rawCache[rawKey]; ok {
			return cached, nil
		}
	}
	resolved, err := Resolve(references)
	if err != nil {
		return nil, err
	}

	if sameEligibility(r.seeded, resolved) {
		if rawKey != "" {
			r.rawCache[rawKey] = r.seeded
		}
		return r.seeded, nil
	}
	if cached, ok := r.cache[resolved.EligibilityDigest()]; ok {
		if rawKey != "" {
			r.rawCache[rawKey] = cached
		}
		return cached, nil
	}
	r.cache[resolved.EligibilityDigest()] = resolved
	if rawKey != "" {
		r.rawCache[rawKey] = resolved
	}
	return resolved, nil
}

func rawReferenceKey(item contracts.ReferenceItem) string {
	digest := strings.ToLower(strings.TrimSpace(item.Digest))
	if !validSHA256Digest(digest) {
		digest = semanticDigest(item.Content)
	}
	return strings.ToLower(strings.TrimSpace(item.MediaType)) + "\x00" + digest
}

func validSHA256Digest(value string) bool {
	if len(value) != len("sha256:")+sha256.Size*2 || !strings.HasPrefix(value, "sha256:") {
		return false
	}
	decoded, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
	return err == nil && len(decoded) == sha256.Size
}

func sameEligibility(first, second *Registry) bool {
	if first == nil || second == nil {
		return first == second
	}
	return first.bound == second.bound && first.digest == second.digest
}

// Resolve validates one optional scene-description reference into an
// eligibility-only view. An absent slot is represented by the canonical empty
// projection so required generated bindings have a stable sentinel identity.
func Resolve(references contracts.ReferenceSet) (*Registry, error) {
	slot, ok := references.Slots[ReferenceSlot]
	if !ok {
		return &Registry{
			digest:     semanticDigest([]byte(emptyProjection)),
			scenesByID: map[string]sceneEligibility{},
		}, nil
	}
	if len(slot.Items) != 1 {
		return nil, fmt.Errorf("reference slot %q must contain exactly one item", ReferenceSlot)
	}

	item := slot.Items[0]
	mediaType, _, err := mime.ParseMediaType(item.MediaType)
	if err != nil {
		return nil, fmt.Errorf("reference slot %q item media type is invalid", ReferenceSlot)
	}
	if !strings.EqualFold(mediaType, scenecodec.MediaType) {
		return nil, fmt.Errorf("reference slot %q item media type must be %s", ReferenceSlot, scenecodec.MediaType)
	}
	if len(item.Content) > MaxBytes {
		return nil, fmt.Errorf("reference slot %q item is %d bytes, limit %d", ReferenceSlot, len(item.Content), MaxBytes)
	}

	value, err := scenecodec.New().Decode(item.Content)
	if err != nil {
		return nil, fmt.Errorf("decode scene eligibility: invalid approved scene JSON")
	}

	scenesByID := make(map[string]sceneEligibility, len(value.Scenes))
	projection := make([]projectedScene, 0, len(value.Scenes))
	for _, scene := range value.Scenes {
		if _, exists := scenesByID[scene.ID]; exists {
			return nil, fmt.Errorf("scene eligibility contains duplicate scene ID %q", scene.ID)
		}
		eligibility := sceneEligibility{
			sourceID:    scene.SourceRef.SourceID,
			startUnitID: scene.SourceRef.StartUnitID,
			endUnitID:   scene.SourceRef.EndUnitID,
			kind:        scene.Kind,
		}
		scenesByID[scene.ID] = eligibility
		projection = append(projection, projectedScene{
			ID:        scene.ID,
			SourceRef: source.SourceRef{SourceID: eligibility.sourceID, StartUnitID: eligibility.startUnitID, EndUnitID: eligibility.endUnitID},
			Kind:      eligibility.kind,
		})
	}

	content, err := eligibilityProjection(projection)
	if err != nil {
		return nil, fmt.Errorf("encode scene eligibility projection: %w", err)
	}
	return &Registry{
		bound:      true,
		digest:     semanticDigest(content),
		scenesByID: scenesByID,
	}, nil
}

// Bound reports whether an approved scene-description reference was supplied.
func (r *Registry) Bound() bool { return r != nil && r.bound }

// Count reports the number of scene records in the eligibility view.
func (r *Registry) Count() int {
	if r == nil {
		return 0
	}
	return len(r.scenesByID)
}

// EligibilityDigest returns the semantic SHA-256 digest of the canonical
// eligibility projection, including the canonical unbound projection.
func (r *Registry) EligibilityDigest() string {
	if r == nil {
		return ""
	}
	return r.digest
}

// Match classifies a chunk against its scene record. The kind is intentionally
// unavailable unless the ID and complete source range match exactly.
func (r *Registry) Match(chunk *source.Chunk) ChunkMatch {
	if r == nil || chunk == nil {
		return ChunkMatch{State: MatchMissing}
	}
	scene, ok := r.scenesByID[chunk.ID]
	if !ok {
		return ChunkMatch{State: MatchMissing}
	}
	if scene.sourceID != chunk.Ref.SourceID || scene.startUnitID != chunk.Ref.StartUnitID || scene.endUnitID != chunk.Ref.EndUnitID {
		return ChunkMatch{State: MatchMismatched}
	}
	return ChunkMatch{State: MatchExact, Kind: scene.kind}
}

type projectedScene struct {
	ID        string           `json:"id"`
	SourceRef source.SourceRef `json:"source_ref"`
	Kind      dnd.SceneKind    `json:"kind"`
}

type projectedSceneList struct {
	Scenes []projectedScene `json:"scenes"`
}

func eligibilityProjection(scenes []projectedScene) ([]byte, error) {
	sort.Slice(scenes, func(i, j int) bool { return scenes[i].ID < scenes[j].ID })
	return json.Marshal(projectedSceneList{Scenes: scenes})
}

func semanticDigest(content []byte) string {
	sum := sha256.Sum256(content)
	return "sha256:" + hex.EncodeToString(sum[:])
}
