package router

import (
	"strings"

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

// A spoken status change over the task list (Vikunja #512, step 4 of
// docs/plans/15-board-surface.md).
//
// Capture and the list read were built; moving a task was only possible in the
// two turns after she read one out, through resolveCandidate — "первую сделал"
// against the bound list. Naming the task instead of its position reached
// nothing: "закрой задачу купить молоко" routed act, found no allowlisted fn,
// and the gate asked "Что сделать?".
//
// Same shape TaskCaptureGrammar uses, for the same reason: no eighth intent, so
// the grammar matches broadly and a deterministic parser inside Build decides.
// The task's own warning applies — every such grammar runs its parser ahead of
// the resident model on every turn, so this is the last one that is free.
//
// TaskStatusFn is the fn slot the daemon dispatches on. Not a Hexis capability
// and not a Praxis one: the board is Maven's own store, so actionAct intercepts
// this name before either ecosystem client sees it.
const TaskStatusFn = "task_status"

// TaskStatus — a parsed status change. Status is a store task status, and Text
// is the task he named, empty when he named none ("закрой задачу"), which is a
// turn the daemon claims and answers by asking which.
type TaskStatus struct {
	Status string
	Text   string
}

// taskStatusNouns — the noun that makes this a board turn rather than ordinary
// speech. Required, and it is the whole reason this rule is safe to run on every
// utterance: "готово" alone is him reporting his day, "убери" alone is a request
// about the room, and neither names the list.
//
// "дело" is deliberately absent. "в чём дело" and "дело в том" are ordinary
// speech, and "список дел" is already a list query.
var taskStatusNouns = []string{"task", "tasks", "todo", "todos"}

// taskStatusTrailingFrame — grammar words that may remain after the named task.
// Words before the board noun are excluded by bounds; this set is used only at
// the trailing edge, so a title such as "сходить в банк" keeps its preposition.
var taskStatusTrailingFrame = []string{"из", "в", "во", "с", "со", "мои", "моих", "моё", "мой", "моя", "мою", "my", "the", "from", "off", "as", "как"}

// taskStatusTopicFrame reuses the closed possessive/topic grammar already used
// to identify a committed reminder's subject. It describes the noun phrase,
// not the reminder itself, so "задача про бэкапы" has the same boundary.
var taskStatusTopicFrame = lexicon.ReminderCancelFrame()

// ParseTaskStatus reads a status change over the board: which transition, and
// which task.
//
// Four conditions, all required. A task noun, so no ordinary sentence claims
// the turn. Exactly one status class, because two transitions mean asking beats
// picking. Exact command vocabulary at command position, OR a result state by
// lemma inside an independently authorised "mark task as state" frame. The
// split is load-bearing: "закрой" and "закрыл" share a lemma, while only one is
// addressed to Maven. Finally, questions and direct prohibitions decline before
// the stage-0 decision can expose a write slot.
func ParseTaskStatus(text string) (TaskStatus, bool) {
	toks := praxisTokens(strings.ToLower(strings.TrimSpace(text)))
	if len(toks) == 0 || !taskStatusNamesBoard(toks) || IsCommandProhibition(text) {
		return TaskStatus{}, false
	}
	status := ""
	statusAt := len(toks)
	for _, c := range []struct {
		status   string
		commands []string
		states   []string
	}{
		{store.TaskDone, lexicon.TaskDoneCommands(), lexicon.TaskDoneStates()},
		{store.TaskDropped, lexicon.TaskDropCommands(), lexicon.TaskDropStates()},
	} {
		at, ok := taskStatusTransitionIndex(toks, c.commands, c.states)
		if !ok {
			continue
		}
		if status != "" {
			// Two transitions in one sentence. They are different rows on the
			// page, so this declines and the cascade answers.
			return TaskStatus{}, false
		}
		status = c.status
		statusAt = at
	}
	if status == "" {
		return TaskStatus{}, false
	}
	// An explicit result from the other transition still makes the sentence
	// contradictory even when it is not, by itself, mutation authority:
	// "задача готова, убери" says both completed and dropped. Decline instead
	// of silently privileging the one imperative. Result vocabulary is used
	// here only as conflict evidence, never to authorize a write.
	if (status == store.TaskDone && taskStatusHasState(toks, lexicon.TaskDropStates())) ||
		(status == store.TaskDropped && taskStatusHasState(toks, lexicon.TaskDoneStates())) {
		return TaskStatus{}, false
	}
	if taskStatusQuestionShaped(text, toks, statusAt, status) {
		return TaskStatus{}, false
	}
	return TaskStatus{Status: status, Text: taskStatusReferent(toks, statusAt)}, true
}

// taskStatusQuestionShaped keeps questions out of the mutating stage-0 rule.
// The general IsQuestionShaped predicate lets an explicit capture verb win —
// "запиши что я пил воду" is a write, not a query. That precedence cannot be
// reused here: TaskStatusGrammar runs before capture, and
// "запиши как отменить задачу" must remain a capture/query rather than become
// a task deletion merely because its later infinitive is in TaskDropWords.
//
// A question mark is conclusive. Without punctuation, a closed interrogative
// or narrative token before the status word makes the status word the subject
// of a question ("как отменить задачу", "объясни как закрыть задачу"). A token
// after an already stated change may belong to the stored task's name —
// "отмени задачу узнать когда рейс" — so it is not enough to reverse a clear
// command. Hyphenated indefinite pronouns remain one token under praxisTokens,
// hence "отмени задачу купить что-нибудь" is not mistaken for a question.
func taskStatusQuestionShaped(text string, toks []string, statusAt int, status string) bool {
	if strings.Contains(text, "?") && !taskStatusPoliteModalAt(toks, statusAt) {
		return true
	}
	for i, tok := range toks {
		if i >= statusAt || !taskStatusQuestionToken(tok) {
			continue
		}
		// "отметь задачу как сделанную" uses как as a state marker, not
		// an interrogative. The marker verb and board noun must both precede
		// it, and a stative resolve word must follow it; this deliberately
		// does not excuse "отметь как отменить задачу".
		if tok == "как" && status == store.TaskDone && taskStatusDoneMarker(toks, i, statusAt) {
			continue
		}
		return true
	}
	return false
}

func taskStatusQuestionToken(tok string) bool {
	return taskStatusIn(tok, interrogatives) || taskStatusIn(tok, narrativeRequests)
}

func taskStatusDoneMarker(toks []string, at, statusAt int) bool {
	markerAt, joinAt, ok := taskStatusMarkerFrame(toks, statusAt)
	return ok && joinAt == at && markerAt < joinAt
}

// taskStatusNamesBoard reports whether the sentence names the task list. The
// Russian noun is matched by lemma, because a noun means the same thing in every
// case and he says "из задач", "задачу", "задача" for one list.
func taskStatusNamesBoard(toks []string) bool {
	for _, t := range toks {
		if taskStatusIsBoardNoun(t) {
			return true
		}
	}
	return false
}

// taskStatusTransitionIndex separates authority from state. A command form is
// exact and must occupy the command head; a result word may use morphology only
// after an explicit marker command has already supplied authority.
func taskStatusTransitionIndex(toks, commands, states []string) (int, bool) {
	for i, tok := range toks {
		if taskStatusIn(tok, commands) && taskStatusCommandHead(toks, i) {
			return i, true
		}
	}
	for i, tok := range toks {
		for _, state := range states {
			if (tok == state || morph.SameWord(tok, state)) && taskStatusStateFrame(toks, i) {
				return i, true
			}
		}
	}
	return 0, false
}

func taskStatusHasState(toks, states []string) bool {
	for _, tok := range toks {
		for _, state := range states {
			if tok == state || morph.SameWord(tok, state) {
				return true
			}
		}
	}
	return false
}

// taskStatusCommandHead proves that the transition is addressed rather than a
// plan/report containing an infinitive. Filler and Maven's address may lead a
// command. Russian also permits the board noun first ("задачу X закрой") and
// the bounded negative-polarity politeness frame "не мог бы ты закрыть".
func taskStatusCommandHead(toks []string, at int) bool {
	if at < 0 || at >= len(toks) {
		return false
	}
	start := 0
	for start < at && commandLead(toks[start]) {
		start++
	}
	if start == at {
		return true
	}
	if taskStatusPoliteModalPrefix(toks[start:at]) {
		return true
	}
	return start < at && taskStatusIsBoardNoun(toks[start])
}

func taskStatusPoliteModalPrefix(prefix []string) bool {
	return len(prefix) == 4 && prefix[0] == "не" && morph.SameWord(prefix[1], "мочь") &&
		prefix[2] == "бы" && (prefix[3] == "ты" || prefix[3] == "вы")
}

func taskStatusPoliteModalAt(toks []string, at int) bool {
	start := 0
	for start < at && commandLead(toks[start]) {
		start++
	}
	return at >= start && taskStatusPoliteModalPrefix(toks[start:at])
}

// taskStatusStateFrame proves the result word is subordinate to an exact
// marker command. A bare "задача готова" or "я сделал задачу" is a report and
// carries no mutation authority, even though it names both board and state.
func taskStatusStateFrame(toks []string, statusAt int) bool {
	_, _, ok := taskStatusMarkerFrame(toks, statusAt)
	return ok
}

func taskStatusMarkerFrame(toks []string, statusAt int) (markerAt, joinAt int, ok bool) {
	for join := statusAt - 1; join >= 0; join-- {
		if toks[join] != "как" && toks[join] != "as" {
			continue
		}
		for marker := 0; marker < join; marker++ {
			if !taskStatusIn(toks[marker], praxisMarkerVerbs) || !taskStatusCommandHead(toks, marker) {
				continue
			}
			if taskStatusNamesBoard(toks[marker+1:join]) || taskStatusNamesBoard(toks[:marker]) {
				return marker, join, true
			}
		}
	}
	return 0, 0, false
}

// taskStatusReferent reads the task out of the command frame, rather than
// subtracting every word that can occur in that frame. Subtraction mangles a
// real title such as "сходить в банк", and in the marker shape
//
//	отметь задачу про бэкапы как сделанную
//
// it left the framing verb in the identity. The board noun and resolved status
// word give this grammar real boundaries. The dedicated marker frame handles
// both word orders around "отметь"; everything else keeps the words between
// the board noun and status (or after the noun when the imperative leads).
func taskStatusReferent(toks []string, statusAt int) string {
	if ref, ok := taskStatusMarkerReferent(toks, statusAt); ok {
		return strings.Join(taskStatusTrimReferent(ref), " ")
	}

	boardAt, ok := taskStatusBoardIndex(toks, statusAt)
	if !ok {
		return ""
	}
	lo, hi := boardAt+1, len(toks)
	if statusAt > boardAt {
		hi = statusAt
	}
	if lo > hi {
		return ""
	}
	return strings.Join(taskStatusTrimReferent(toks[lo:hi]), " ")
}

// taskStatusMarkerReferent recognises the already-validated
// "mark task X as done" frame and returns X. A leading marker wins over any
// marker-shaped verb inside X ("отметь задачу отметить выходные ...").
// With postposed Russian word order, the last marker before "как" closes X.
func taskStatusMarkerReferent(toks []string, statusAt int) ([]string, bool) {
	for joinAt := statusAt - 1; joinAt >= 0; joinAt-- {
		if toks[joinAt] != "как" && toks[joinAt] != "as" {
			continue
		}
		if !taskStatusDoneMarker(toks, joinAt, statusAt) {
			continue
		}

		// Canonical order: marker, board noun, referent, state joiner.
		for markerAt := 0; markerAt < joinAt; markerAt++ {
			if !taskStatusIn(toks[markerAt], praxisMarkerVerbs) {
				continue
			}
			for boardAt := markerAt + 1; boardAt < joinAt; boardAt++ {
				if taskStatusIsBoardNoun(toks[boardAt]) {
					return toks[boardAt+1 : joinAt], true
				}
			}
		}

		// Postposed order: board noun, referent, marker, state joiner.
		for markerAt := joinAt - 1; markerAt >= 0; markerAt-- {
			if !taskStatusIn(toks[markerAt], praxisMarkerVerbs) {
				continue
			}
			for boardAt := markerAt - 1; boardAt >= 0; boardAt-- {
				if taskStatusIsBoardNoun(toks[boardAt]) {
					return toks[boardAt+1 : markerAt], true
				}
			}
		}
	}
	return nil, false
}

func taskStatusBoardIndex(toks []string, statusAt int) (int, bool) {
	// A leading imperative owns the first board noun after it. Looking there
	// first avoids treating a later "задача" inside the title as the frame.
	for i := statusAt + 1; i < len(toks); i++ {
		if taskStatusIsBoardNoun(toks[i]) {
			return i, true
		}
	}
	for i := 0; i < statusAt && i < len(toks); i++ {
		if taskStatusIsBoardNoun(toks[i]) {
			return i, true
		}
	}
	return 0, false
}

func taskStatusIsBoardNoun(tok string) bool {
	if morph.SameWord(tok, "задача") {
		return true
	}
	return taskStatusIn(tok, taskStatusNouns)
}

// taskStatusTrimReferent removes only words at the identity's edges. The topic
// and possessive words are the same closed noun/subject frame reminder
// cancellation already uses. Keeping interior words is load-bearing: Russian
// task titles routinely contain prepositions.
func taskStatusTrimReferent(toks []string) []string {
	for len(toks) > 0 && taskStatusIn(toks[0], taskStatusTopicFrame) {
		toks = toks[1:]
	}
	for len(toks) > 0 && (taskStatusIn(toks[len(toks)-1], taskStatusTrailingFrame) ||
		taskStatusIn(toks[len(toks)-1], taskStatusTopicFrame) || lexicon.IsFillerParticle(toks[len(toks)-1])) {
		toks = toks[:len(toks)-1]
	}
	return toks
}

func taskStatusIn(tok string, words []string) bool {
	for _, w := range words {
		if tok == w {
			return true
		}
	}
	return false
}

// TaskStatusGrammar — stage 0 for a spoken status change. Wired after the Praxis
// rules and before the capture marker: Praxis claims a bare "закрой" and this
// rule requires the board noun, so the two cannot collide, and the capture
// marker must not read "убери из задач купить молоко" as a new task.
func TaskStatusGrammar() Grammar {
	return Grammar{
		Name: "task-status",
		Decide: func(utterance string) (Decision, bool) {
			c, ok := ParseTaskStatus(utterance)
			if !ok {
				return Decision{}, false
			}
			return Decision{
				Stage:      0,
				Intent:     IntentAct,
				Confidence: 1.0,
				// Value carries the transition and Text the task he named,
				// which is the pairing handlePraxisAct uses for an item and its
				// reference. Empty Text is a claim, not a refusal: the daemon
				// asks which task, having the list she does not.
				Slots: Slots{Fn: TaskStatusFn, HasFn: true, Value: c.Status, Text: c.Text},
			}, true
		},
	}
}
