package memory

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/kami/maven/internal/llm"
)

// LocativeCompleter is the narrow resident-model seam the verifier needs.
// *llm.Client satisfies it. Keeping the interface here lets the safety and
// malformed-output paths run without a server in unit tests.
type LocativeCompleter interface {
	Complete(context.Context, llm.Req) (string, error)
}

// LocativeAnswerVerifier is the model-backed second opinion for candidates the
// deterministic locative identity gate rejected. It never replaces that gate:
// exact structural accepts do not call it, and a missing model, timeout, error,
// or malformed verdict remains an abstention in the daemon.
type LocativeAnswerVerifier struct {
	c LocativeCompleter
}

// LocativeVerdict keeps the two extracted referents as auditable evidence.
// The daemon consumes only Answerable; the live eval records all three fields
// so a yes/no score cannot hide what the model thought it was comparing.
type LocativeVerdict struct {
	Target        string `json:"target"`
	MemorySubject string `json:"memory_subject"`
	Answerable    bool   `json:"-"`
	Raw           string `json:"-"`
}

// NewLocativeAnswerVerifier returns nil when no resident completion seam exists.
// That is the ordinary no-model deployment and deliberately means abstain.
func NewLocativeAnswerVerifier(c LocativeCompleter) *LocativeAnswerVerifier {
	if c == nil {
		return nil
	}
	return &LocativeAnswerVerifier{c: c}
}

// locativeVerifierGrammar fixes both the shape and every variable-width field.
// target and memory_subject come before answer deliberately: the small resident
// model must identify the two referents before choosing the verdict instead of
// emitting an unconstrained first-token hunch.
const locativeVerifierGrammar = `
root   ::= "{" ws "\"target\"" ws ":" ws string "," ws "\"memory_subject\"" ws ":" ws string "," ws "\"answer\"" ws ":" ws answer ws "}"
answer ::= "\"no\"" | "\"yes\""
string ::= "\"" ([^"\\\x00-\x1F] | "\\" ["\\/bfnrt] | "\\u" [0-9a-fA-F]{4}){1,80} "\""
ws     ::= [ \t\n]{0,2}
`

// locativeVerifierSystem teaches a relation, not the held-out fixture. The
// examples use different entities from the measured nginx/token/disk/key and
// adversarial passport/shirt/box cases. Two negative examples pin the dangerous
// distinctions: a location object is not the proposition subject, and a shared
// generic noun with a conflicting complement is not the same referent.
const locativeVerifierSystem = `Ты — строгий классификатор логического следования для личной памяти. Вход — JSON с одним question и одним memory.
Сначала выдели target: конкретный предмет/событие, чьё место или источник спрашивают. Затем memory_subject: предмет/событие, МЕСТО КОТОРОГО сообщает память. Предмет после слов места (в, на, под, рядом с, inside, at, under) — это место/контейнер, а НЕ memory_subject.
answer=yes только если target и memory_subject — один и тот же конкретный референт и память прямо сообщает запрошенное место/источник. Настоящие синонимы и контекстные названия допустимы. Совпадение цвета, свойства, общего слова, контейнера, места или действия недостаточно. Уточнения принадлежности/состава не должны конфликтовать. Не используй внешние знания. Сомнение => no.

Примеры:
input: {"question":"где красная тетрадь?","memory":"зарядка лежит на красной тетради"}
output: {"target":"красная тетрадь","memory_subject":"зарядка","answer":"no"}
input: {"question":"где ключ от гаража?","memory":"ключ от офиса лежит под ковриком"}
output: {"target":"ключ от гаража","memory_subject":"ключ от офиса","answer":"no"}
input: {"question":"где дубликат ключа от мастерской?","memory":"запасной ключ мастерской лежит в ящике"}
output: {"target":"дубликат ключа от мастерской","memory_subject":"запасной ключ мастерской","answer":"yes"}
input: {"question":"where are the database settings?","memory":"the database configuration is in /etc/db"}
output: {"target":"database settings","memory_subject":"database configuration","answer":"yes"}

Верни только JSON требуемой формы.`

const (
	locativeVerifierTimeout   = 8 * time.Second
	locativeVerifierMaxTokens = 128
	locativeVerifierMaxField  = 80
)

// Answerable implements the daemon's deliberately tiny verifier interface.
func (v *LocativeAnswerVerifier) Answerable(ctx context.Context, question, candidate string) (bool, error) {
	verdict, err := v.Evaluate(ctx, question, candidate)
	return verdict.Answerable, err
}

// Evaluate returns the bounded model verdict and its extracted referents.
// Callers must treat every error as false; it never manufactures a fallback.
func (v *LocativeAnswerVerifier) Evaluate(ctx context.Context, question, candidate string) (LocativeVerdict, error) {
	if v == nil || v.c == nil {
		return LocativeVerdict{}, errors.New("locative verifier: resident model unavailable")
	}
	input, err := json.Marshal(struct {
		Question string `json:"question"`
		Memory   string `json:"memory"`
	}{Question: question, Memory: candidate})
	if err != nil {
		return LocativeVerdict{}, fmt.Errorf("locative verifier: encode input: %w", err)
	}

	callCtx, cancel := context.WithTimeout(ctx, locativeVerifierTimeout)
	defer cancel()
	raw, err := v.c.Complete(callCtx, llm.Req{
		System:        locativeVerifierSystem,
		User:          string(input),
		Grammar:       locativeVerifierGrammar,
		MaxTokens:     locativeVerifierMaxTokens,
		RepeatPenalty: 1.1,
	})
	if err != nil {
		return LocativeVerdict{}, fmt.Errorf("locative verifier: complete: %w", err)
	}
	return parseLocativeVerdict(raw)
}

func parseLocativeVerdict(raw string) (LocativeVerdict, error) {
	trimmed := strings.TrimSpace(raw)
	var wire struct {
		Target        string `json:"target"`
		MemorySubject string `json:"memory_subject"`
		Answer        string `json:"answer"`
	}
	dec := json.NewDecoder(strings.NewReader(trimmed))
	dec.DisallowUnknownFields()
	if err := dec.Decode(&wire); err != nil {
		return LocativeVerdict{}, fmt.Errorf("locative verifier: parse %q: %w", boundedRaw(trimmed), err)
	}
	var trailing any
	if err := dec.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			return LocativeVerdict{}, fmt.Errorf("locative verifier: trailing JSON in %q", boundedRaw(trimmed))
		}
		return LocativeVerdict{}, fmt.Errorf("locative verifier: trailing data in %q: %w", boundedRaw(trimmed), err)
	}
	wire.Target = strings.TrimSpace(wire.Target)
	wire.MemorySubject = strings.TrimSpace(wire.MemorySubject)
	if wire.Target == "" || wire.MemorySubject == "" ||
		utf8.RuneCountInString(wire.Target) > locativeVerifierMaxField ||
		utf8.RuneCountInString(wire.MemorySubject) > locativeVerifierMaxField {
		return LocativeVerdict{}, fmt.Errorf("locative verifier: empty or oversized referent in %q", boundedRaw(trimmed))
	}
	var answerable bool
	switch wire.Answer {
	case "yes":
		answerable = true
	case "no":
		answerable = false
	default:
		return LocativeVerdict{}, fmt.Errorf("locative verifier: invalid answer %q", wire.Answer)
	}
	return LocativeVerdict{
		Target:        wire.Target,
		MemorySubject: wire.MemorySubject,
		Answerable:    answerable,
		Raw:           trimmed,
	}, nil
}

func boundedRaw(s string) string {
	const max = 240
	if len(s) <= max {
		return s
	}
	return s[:max] + "…"
}
