// Package registryresolver provides the shared reference-selection and caching
// mechanics used by immutable D&D registry views.
package registryresolver

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

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

// ReferenceSpec describes one optional, single-item registry reference.
type ReferenceSpec struct {
	SlotName          string
	AcceptedMediaType string
	MaxBytes          int64
}

// Item is an owned reference payload. MediaType is the parsed, normalized
// media type without parameters. Reference provenance and caller digests are
// intentionally excluded.
type Item struct {
	MediaType string
	Content   []byte
}

// validatedItem borrows its content from the supplied reference set. Callers
// must copy content before passing it to a callback that may retain it.
type validatedItem struct {
	mediaType string
	content   []byte
}

// Config supplies the domain-owned operations needed to prepare immutable
// registry views. Absent and Load must return values whose mutable state is not
// exposed to callers. Load receives owned bytes and may retain them. Errors
// returned by either callback must be bounded and must not contain input data.
type Config[V any] struct {
	Reference        ReferenceSpec
	Absent           func() (V, error)
	Load             func([]byte) (V, error)
	SemanticIdentity func(V) string
}

type preparedView[V any] struct {
	value    V
	bound    bool
	identity string
}

type semanticKey struct {
	bound    bool
	identity string
}

// Resolver retains one immutable construction-time view and memoizes valid
// operation-time views by raw reference content and semantic identity.
type Resolver[V any] struct {
	config    Config[V]
	seeded    preparedView[V]
	mediaType string
	slotName  string

	mu            sync.Mutex
	rawCache      map[string]preparedView[V]
	semanticCache map[semanticKey]preparedView[V]
}

// New validates the configuration and construction-time reference. A present
// slot with no items is treated as an absent generated-reference placeholder
// only at this construction boundary.
func New[V any](config Config[V], references contracts.ReferenceSet) (*Resolver[V], error) {
	normalized, mediaType, err := normalizeConfig(config)
	if err != nil {
		return nil, err
	}
	resolver := &Resolver[V]{
		config:        normalized,
		mediaType:     mediaType,
		slotName:      normalized.Reference.SlotName,
		rawCache:      make(map[string]preparedView[V]),
		semanticCache: make(map[semanticKey]preparedView[V]),
	}

	if slot, present := references.Slots[resolver.slotName]; present && len(slot.Items) == 0 {
		resolver.seeded, err = resolver.absent()
	} else {
		resolver.seeded, err = resolver.resolveUncached(references)
	}
	if err != nil {
		return nil, err
	}
	return resolver, nil
}

// Seeded returns the immutable construction-time view.
func (r *Resolver[V]) Seeded() V {
	if r == nil {
		var zero V
		return zero
	}
	return r.seeded.value
}

// Resolve returns the construction-time view when the operation does not
// supply the configured slot. A present slot is validated and loaded as an
// operation-time override.
func (r *Resolver[V]) Resolve(references contracts.ReferenceSet) (V, error) {
	if r == nil {
		var zero V
		return zero, fmt.Errorf("registry resolver must not be nil")
	}
	if _, present := references.Slots[r.slotName]; !present {
		return r.seeded.value, nil
	}

	item, _, err := validateOptionalSingleItem(references, r.config.Reference, r.mediaType)
	if err != nil {
		var zero V
		return zero, err
	}
	rawKey := rawReferenceKey(item)

	r.mu.Lock()
	defer r.mu.Unlock()
	if cached, ok := r.rawCache[rawKey]; ok {
		return cached.value, nil
	}

	resolved, err := r.load(append([]byte(nil), item.content...))
	if err != nil {
		var zero V
		return zero, err
	}
	if sameIdentity(r.seeded, resolved) {
		r.rawCache[rawKey] = r.seeded
		return r.seeded.value, nil
	}

	key := semanticKey{bound: resolved.bound, identity: resolved.identity}
	if cached, ok := r.semanticCache[key]; ok {
		r.rawCache[rawKey] = cached
		return cached.value, nil
	}
	r.semanticCache[key] = resolved
	r.rawCache[rawKey] = resolved
	return resolved.value, nil
}

// ResolveOptionalSingleItem validates and copies one optional registry item.
// A missing slot returns present=false. A present slot must contain exactly one
// item, even when it represents an operation-time generated reference.
func ResolveOptionalSingleItem(references contracts.ReferenceSet, spec ReferenceSpec) (Item, bool, error) {
	normalized, mediaType, err := normalizeReferenceSpec(spec)
	if err != nil {
		return Item{}, false, err
	}
	item, present, err := validateOptionalSingleItem(references, normalized, mediaType)
	if err != nil || !present {
		return Item{}, present, err
	}
	return Item{
		MediaType: item.mediaType,
		Content:   append([]byte(nil), item.content...),
	}, true, nil
}

func (r *Resolver[V]) resolveUncached(references contracts.ReferenceSet) (preparedView[V], error) {
	item, present, err := validateOptionalSingleItem(references, r.config.Reference, r.mediaType)
	if err != nil {
		return preparedView[V]{}, err
	}
	if !present {
		return r.absent()
	}
	return r.load(append([]byte(nil), item.content...))
}

func (r *Resolver[V]) absent() (preparedView[V], error) {
	value, err := r.config.Absent()
	if err != nil {
		return preparedView[V]{}, err
	}
	return preparedView[V]{value: value, identity: r.config.SemanticIdentity(value)}, nil
}

func (r *Resolver[V]) load(content []byte) (preparedView[V], error) {
	value, err := r.config.Load(content)
	if err != nil {
		return preparedView[V]{}, err
	}
	identity := strings.TrimSpace(r.config.SemanticIdentity(value))
	if identity == "" {
		return preparedView[V]{}, fmt.Errorf("load reference slot %q: semantic identity must not be empty", r.slotName)
	}
	return preparedView[V]{value: value, bound: true, identity: identity}, nil
}

func normalizeConfig[V any](config Config[V]) (Config[V], string, error) {
	reference, mediaType, err := normalizeReferenceSpec(config.Reference)
	if err != nil {
		return Config[V]{}, "", err
	}
	if config.Absent == nil {
		return Config[V]{}, "", fmt.Errorf("registry resolver absent-view callback must not be nil")
	}
	if config.Load == nil {
		return Config[V]{}, "", fmt.Errorf("registry resolver loader must not be nil")
	}
	if config.SemanticIdentity == nil {
		return Config[V]{}, "", fmt.Errorf("registry resolver semantic-identity callback must not be nil")
	}
	config.Reference = reference
	return config, mediaType, nil
}

func normalizeReferenceSpec(spec ReferenceSpec) (ReferenceSpec, string, error) {
	spec.SlotName = strings.TrimSpace(spec.SlotName)
	if spec.SlotName == "" {
		return ReferenceSpec{}, "", fmt.Errorf("registry reference slot name must not be empty")
	}
	mediaType, _, err := mime.ParseMediaType(spec.AcceptedMediaType)
	if err != nil || strings.TrimSpace(mediaType) == "" {
		return ReferenceSpec{}, "", fmt.Errorf("registry reference slot %q accepted media type is invalid", spec.SlotName)
	}
	mediaType = strings.ToLower(mediaType)
	if spec.MaxBytes <= 0 {
		return ReferenceSpec{}, "", fmt.Errorf("registry reference slot %q maximum size must be positive", spec.SlotName)
	}
	spec.AcceptedMediaType = mediaType
	return spec, mediaType, nil
}

func validateOptionalSingleItem(references contracts.ReferenceSet, spec ReferenceSpec, acceptedMediaType string) (validatedItem, bool, error) {
	slot, present := references.Slots[spec.SlotName]
	if !present {
		return validatedItem{}, false, nil
	}
	if len(slot.Items) != 1 {
		return validatedItem{}, true, fmt.Errorf("reference slot %q must contain exactly one item", spec.SlotName)
	}
	item := slot.Items[0]
	mediaType, _, err := mime.ParseMediaType(item.MediaType)
	if err != nil {
		return validatedItem{}, true, fmt.Errorf("reference slot %q item media type is invalid", spec.SlotName)
	}
	mediaType = strings.ToLower(mediaType)
	if !strings.EqualFold(mediaType, acceptedMediaType) {
		return validatedItem{}, true, fmt.Errorf("reference slot %q item media type must be %s", spec.SlotName, acceptedMediaType)
	}
	if int64(len(item.Content)) > spec.MaxBytes {
		return validatedItem{}, true, fmt.Errorf("reference slot %q item is %d bytes, limit %d", spec.SlotName, len(item.Content), spec.MaxBytes)
	}
	return validatedItem{mediaType: mediaType, content: item.Content}, true, nil
}

func rawReferenceKey(item validatedItem) string {
	sum := sha256.Sum256(item.content)
	return item.mediaType + "\x00sha256:" + hex.EncodeToString(sum[:])
}

func sameIdentity[V any](first, second preparedView[V]) bool {
	return first.bound == second.bound && first.identity == second.identity
}
