package router

import (
	"strings"
	"unicode"

	"github.com/kami/maven/internal/lexicon"
	"github.com/kami/maven/internal/morph"
)

// ProhibitedActFn is the stage-0 fn used for a command whose only authority is
// negative: the user explicitly told Maven not to perform it. It is not an
// executable tool name. The daemon consumes it as a deterministic no-op before
// any local or ecosystem executor is considered.
const ProhibitedActFn = "prohibited_act"

// CommandProhibition is the structural evidence carried by a direct negative
// command. Body begins with the verb governed by the prohibition, with address
// and politeness framing removed. Keeping the body available lets tests and
// future policy distinguish the scope without recovering it from substrings.
type CommandProhibition struct {
	Body []string
}

// ParseCommandProhibition recognises an addressed prohibition at command
// position. It is deliberately a token grammar, not a search for "не"/"not":
// first-person reports ("я не закрыл"), questions and a negative clause after
// another request never grant or revoke execution authority.
//
// The small scope exceptions are semantic command frames of their own. "не
// забудь напомнить" / "don't forget to remind" is an affirmative reminder,
// and "не мог бы ты ..." is ordinary modal politeness. They must not be
// flattened into a refusal merely because their first surface token is
// negative. A forget frame is exempt only when it actually contains Maven's
// reminder verb; "не забудь закрыть задачу" remains an ambiguous action and is
// conservatively refused rather than allowed to mutate the board.
func ParseCommandProhibition(text string) (CommandProhibition, bool) {
	tokens := commandFrameTokens(text)
	for len(tokens) > 0 && commandLead(tokens[0]) {
		tokens = tokens[1:]
	}
	if len(tokens) < 2 {
		return CommandProhibition{}, false
	}

	body := tokens
	switch {
	case len(tokens) >= 3 && tokens[0] == "только" && tokens[1] == "не":
		body = tokens[2:]
	case len(tokens) >= 6 && tokens[0] == "ни" && tokens[1] == "в" && tokens[2] == "коем" && tokens[3] == "случае" && tokens[4] == "не":
		body = tokens[5:]
	case len(tokens) >= 5 && tokens[0] == "ни" && tokens[1] == "за" && tokens[2] == "что" && tokens[3] == "не":
		body = tokens[4:]
	case tokens[0] == "не":
		body = tokens[1:]
	case tokens[0] == "никогда":
		body = tokens[1:]
		if len(body) > 0 && body[0] == "не" {
			body = body[1:]
		}
	case tokens[0] == "don't" || tokens[0] == "dont":
		body = tokens[1:]
	case len(tokens) >= 3 && tokens[0] == "do" && tokens[1] == "not":
		body = tokens[2:]
	case tokens[0] == "never":
		body = tokens[1:]
	default:
		return CommandProhibition{}, false
	}
	if len(body) == 0 || affirmativeNegativeFrame(body) || selfStateReport(body) {
		return CommandProhibition{}, false
	}
	return CommandProhibition{Body: append([]string(nil), body...)}, true
}

// IsCommandProhibition is the execution-belt predicate. Callers use the same
// structural evidence at routing, dialogue and executor boundaries so a model
// cannot recover authority by changing the intent or rewriting the text slot.
func IsCommandProhibition(text string) bool {
	_, ok := ParseCommandProhibition(text)
	return ok
}

// CommandProhibitionGrammar gives a direct prohibition a deterministic stage-0
// route. The sentinel is intentionally an Act: it reaches the same daemon
// policy as a model-routed action, but can never collide with an enabled tool.
func CommandProhibitionGrammar() Grammar {
	return Grammar{
		Name: "command-prohibition",
		Decide: func(utterance string) (Decision, bool) {
			if !IsCommandProhibition(utterance) {
				return Decision{}, false
			}
			return Decision{
				Stage:      0,
				Intent:     IntentAct,
				Confidence: 1,
				Slots: Slots{
					Fn:    ProhibitedActFn,
					HasFn: true,
				},
			}, true
		},
	}
}

// commandFrameTokens keeps apostrophes inside a word so don't is one grammar
// token. Every other punctuation rune is a boundary. This is intentionally
// local rather than reusing praxisTokens, whose underscore/hyphen policy is for
// item ids, not spoken command mood.
func commandFrameTokens(text string) []string {
	var out []string
	var word []rune
	flush := func() {
		for len(word) > 0 && word[len(word)-1] == '\'' {
			word = word[:len(word)-1]
		}
		if len(word) > 0 {
			out = append(out, string(word))
		}
		word = word[:0]
	}
	for _, r := range strings.ToLower(text) {
		switch {
		case unicode.IsLetter(r) || unicode.IsDigit(r):
			word = append(word, r)
		case (r == '\'' || r == '’') && len(word) > 0:
			word = append(word, '\'')
		default:
			flush()
		}
	}
	flush()
	return out
}

func commandLead(token string) bool {
	if lexicon.IsFillerParticle(token) {
		return true
	}
	switch token {
	case "maven", "мавен", "мавена", "мэйвен", "мейвен", "майвен", "мэвен":
		return true
	default:
		return false
	}
}

// selfStateReport declines a negation whose head describes the speaker rather
// than an action Maven could take. "ну не знаю" is an unclear answer to a parked
// question and must reach the clarify ladder; consuming it as a prohibition
// ended the dialogue with "хорошо, не буду" and dropped the pending reminder.
func selfStateReport(body []string) bool {
	for _, verb := range lexicon.SelfStateVerbs() {
		if body[0] == verb || morph.SameWord(body[0], verb) {
			return true
		}
	}
	return false
}

func affirmativeNegativeFrame(body []string) bool {
	if len(body) == 0 {
		return false
	}
	// Negative-polarity modal politeness: the complete bounded frame "не мог
	// бы ты ..." / "не могли бы вы ..." asks for the nested action; a bare
	// "не мог перезапустить" is instead a report and must not erase the safety
	// belt merely because its modal has the same lemma.
	if len(body) >= 4 && morph.SameWord(body[0], "мочь") && body[1] == "бы" &&
		(body[2] == "ты" || body[2] == "вы") {
		return true
	}
	if body[0] != "forget" && !morph.SameWord(body[0], "забыть") {
		return false
	}
	// The exception is the nested reminder request itself, not a reminder word
	// somewhere later in an unrelated sentence. English may carry infinitival
	// "to" between the two verbs; Russian does not need a joiner.
	at := 1
	if at < len(body) && body[at] == "to" {
		at++
	}
	if at >= len(body) {
		return false
	}
	for _, reminderVerb := range lexicon.ReminderVerbs() {
		if body[at] == reminderVerb || morph.SameWord(body[at], reminderVerb) {
			return true
		}
	}
	return false
}
