package main

import (
	"context"
	"log"
	"regexp"
	"strings"
	"sync"

	"github.com/kami/maven/internal/delivery"
	"github.com/kami/maven/internal/loop"
	"github.com/kami/maven/internal/phraser"
	"github.com/kami/maven/internal/phraser/eval"
)

// The persona checks, run before she speaks (Vikunja #399).
//
// RunChecks and RunTalkChecks only ever ran from the eval package, so
// everything the fixtures measured was offline knowledge: we could say "about
// one reply in three is broken" and still ship every one of them. This runs the
// cheap half of that on the live path, and replaces a failing message with the
// deterministic floor.
//
// Which checks: the unambiguous string tests only — feminine self-reference,
// how she addresses him, and a leaked-reasoning test. Not length, which is
// path-specific, and not ontopic, which compares against fragments the fixture
// supplies and runtime does not have. Not hisgender either — see guardSpoken.
//
// No retry. A retry doubles the latency on the exact turn that is already going
// badly, and on the nudge path the moment has passed.
//
// The known cost, written down because it is real: a wrongly flagged good reply
// is replaced by a flatter stub one. That is the right trade — a stub sentence
// is dull, a leaked reasoning trace is broken — but it means these checks can
// no longer be tuned for sensitivity alone.

// checkLeak — the name reported when the model's scaffolding reaches the text.
const checkLeak = "leak"

// leakPatterns — reasoning and protocol that belongs to the model, not to him.
// The resident model is a Thinking variant, so an unclosed reasoning block is
// the failure mode, not a hypothetical (Vikunja #398).
var leakPatterns = []*regexp.Regexp{
	regexp.MustCompile(`(?i)<\s*/?\s*think`),
	regexp.MustCompile(`(?i)thinking\s*(process|:)`),
	regexp.MustCompile(`(?i)^\s*(assistant|user|system)\s*:`),
	// Raw contract JSON: the parser already unwraps a good one, so a body that
	// still carries the keys is one it could not read.
	regexp.MustCompile(`"(response|mood|body|summary)"\s*:`),
	// The persona block quoted back at him.
	regexp.MustCompile(`(?i)(ты\s+—?\s*мэйвен|системный промпт|system prompt)`),
}

// checkPersonaLeak reports whether the model's own scaffolding is in the text.
func checkPersonaLeak(body string) (string, bool) {
	for _, re := range leakPatterns {
		if m := re.FindString(body); m != "" {
			return "leaked " + strings.TrimSpace(m), false
		}
	}
	return "", true
}

// personaRejects counts what the guard caught, by check name, so the real
// production rate is knowable rather than inferred from the fixture.
var personaRejects = struct {
	mu sync.Mutex
	by map[string]int
}{by: map[string]int{}}

func personaRejectCounts() map[string]int {
	personaRejects.mu.Lock()
	defer personaRejects.mu.Unlock()
	out := make(map[string]int, len(personaRejects.by))
	for k, v := range personaRejects.by {
		out[k] = v
	}
	return out
}

// guardSpoken checks a phrased message. It returns the failed check and false
// when the message must not be said; path names the caller, for the log.
//
// An empty message passes: the caller already treats that as a failure and
// falls back on its own, and reporting it as a persona breach would put a
// misleading line in the count.
func guardSpoken(path, body string) (string, bool) {
	if strings.TrimSpace(body) == "" {
		return "", true
	}
	if detail, ok := checkPersonaLeak(body); !ok {
		return rejectSpoken(path, checkLeak, detail, body), false
	}
	// Feminine and address only. HisGender is not run here: it reads a
	// sentence-initial feminine verb with no pronoun — "записала, что ты выпил
	// воды" — as a woman being addressed, when it is her own correct
	// self-reference. Offline that is a point of score; on this path it would
	// replace a good reply with a stub one on every fact she confirms.
	for _, r := range []eval.Result{eval.Feminine(body), eval.Address(body)} {
		if !r.Pass {
			return rejectSpoken(path, r.Name, r.Detail, body), false
		}
	}
	return "", true
}

// rejectSpoken logs what she nearly said and counts it. The whole text, not a
// prefix: the point of the log line is that the failure can be read back later
// and argued with.
func rejectSpoken(path, check, detail, body string) string {
	personaRejects.mu.Lock()
	personaRejects.by[check]++
	personaRejects.mu.Unlock()
	log.Printf("persona: %s rejected on %s (%s): %q", path, check, detail, body)
	return check
}

// guardNudge checks a phrased nudge and falls back to the deterministic floor
// when it fails. The nudge path, unlike the reply path, cannot ask again: the
// tick has already decided she speaks, so the choice is the floor's wording or
// a broken sentence.
func guardNudge(pn delivery.PhrasedNudge, cand loop.Candidate) delivery.PhrasedNudge {
	if _, ok := guardSpoken("nudge", pn.Body); ok {
		return pn
	}
	stub, err := phraser.NewStub().PhraseNudge(context.Background(), cand)
	if err != nil {
		// The Stub is templates over the candidate and does not fail. If it
		// somehow does, the model's text is still what the rule decided to
		// say, and saying nothing is the worse outcome.
		return pn
	}
	return stub
}
