package app

import (
	"strconv"
	"strings"

	"github.com/correx/tui-go/internal/protocol"
	"github.com/correx/tui-go/internal/ws"
)

// DisplayState is the top-level screen the UI renders.
type DisplayState int

const (
	StateIdle DisplayState = iota
	StateInSession
	StateApproval
	StateClarification
	StateWorkflowPropose
)

// InputMode toggles what a submitted line targets.
type InputMode int

const (
	ModeRouter InputMode = iota // chat / steering input
	ModeFilter                  // session-list filter
	ModeIntent                  // freeform request for a workflow being started
)

// EditMode is the vim-style modality: Normal = bare-key commands, Insert = typing.
type EditMode int

const (
	ModeNormal EditMode = iota
	ModeInsert
)

// ChatMode selects how a turn is sent to the router.
const (
	ChatModeChat     = "CHAT"
	ChatModeSteering = "STEERING"
)

// OverlayKind is the active modal (immediate-mode: drawn on top when set).
type OverlayKind int

const (
	OverlayNone OverlayKind = iota
	OverlayPalette
	OverlayEventInspector
	OverlayDiff
	OverlayToolPalette
	OverlayModels
	OverlayArtifacts
	OverlayConfig
	OverlayStats
	OverlayIdeas
	OverlayHealth
	OverlaySessions
	OverlayTasks
	OverlayFiles
	OverlayStatusbar
	OverlayGrants
	OverlayGrantScope
	OverlayHelp
	OverlayProjectProfile
	OverlayOperatorProfile
	OverlayPlan
)

// RouterEntry is one line in a session's conversation transcript.
type RouterEntry struct {
	Role      string // user | router | tool | narration | narration_llm | action
	Content   string
	Icon      string // action role only: the gutter glyph (✓ ✎ ✗ ⌘ ✕ ⊞ ⊟)
	Metrics   *TurnMetrics
	Reasoning string // model reasoning/CoT that preceded this entry (tool/artifact turns)
}

// TurnMetrics carries optional latency + token cost for a ROUTER chat turn.
type TurnMetrics struct {
	LatencyMs   int64
	TotalTokens int
}

// EventEntry is a row in the event stream.
type EventEntry struct {
	Time   string
	Type   string
	Detail string
}

// PlanStageStatus tracks a stage's lifecycle in a locked execution plan.
type PlanStageStatus int

const (
	PlanPending   PlanStageStatus = iota
	PlanRunning
	PlanCompleted
	PlanFailed
)

// PlanStage is one stage in a locked execution plan, with live execution status.
type PlanStage struct {
	ID     string
	Status PlanStageStatus
}

// ToolStatus mirrors the Kotlin ToolDisplayStatus.
type ToolStatus int

const (
	ToolStarted ToolStatus = iota
	ToolCompleted
	ToolFailed
	ToolRejected
)

type ToolRecord struct {
	Name   string
	Tier   int
	Status ToolStatus
	Params []string // pretty "key=value" call args, from tool.started
}

// ManifestTool is a declared (not-yet-run) tool from a stage manifest.
type ManifestTool struct {
	Name string
	Tier int
}

// ProfileField is one editable row in the project/operator profile editors — a flat
// key/value text field, mirroring the config editor's STRING-field editing model.
type ProfileField struct {
	Key   string
	Value string
}

// Approval is a pending approval gate for a session.
type Approval struct {
	RequestID string
	SessionID string
	Tier      string
	Risk      string
	ToolName  string
	Preview   string
	// Rationale holds the plane-2 verified preconditions ("[PATH_OUTSIDE_WORKSPACE] …")
	// that justify the gate — shown in the approval band instead of an opaque tier.
	Rationale []string
}

// tierNum parses the numeric part of a tier label ("T3" → 3). Unparseable or
// empty tiers return -1, which is treated as low-risk (never requires confirm).
func tierNum(tier string) int {
	t := strings.TrimSpace(strings.ToUpper(tier))
	t = strings.TrimPrefix(t, "T")
	if t == "" {
		return -1
	}
	n, err := strconv.Atoi(t)
	if err != nil {
		return -1
	}
	return n
}

// HighTier reports whether this gate is destructive/high-risk (T3+), so an approve
// must be confirmed with a second keypress rather than acting on one keystroke.
func (a *Approval) HighTier() bool {
	return tierNum(a.Tier) >= 3
}

// ClarQuestion is one open question a stage raised (mirrors the wire DTO).
type ClarQuestion struct {
	ID          string
	Prompt      string
	Header      string
	Options     []string
	MultiSelect bool
}

// Clarification is a stage's pending set of open questions for the operator.
type Clarification struct {
	RequestID string
	SessionID string
	StageID   string
	Questions []ClarQuestion
}

// ProposeCandidate is one workflow the router suggests (mirrors the wire DTO).
type ProposeCandidate struct {
	WorkflowID string
	Reason     string
}

// Proposal is the router's triage suggestion of candidate workflows for a chat
// request, rendered as a choice panel with a manual-answer slot. Picking a candidate
// launches that workflow; the custom slot continues the conversation.
type Proposal struct {
	ProposalID      string
	SessionID       string
	Prompt          string
	OriginalRequest string
	Candidates      []ProposeCandidate
}

// Session is the UI's view of one server session.
type Session struct {
	ID                 string
	Status             string
	WorkflowID         string
	Name               string
	named              bool // Name came from a session.renamed (intent-derived title); don't clobber with workflowId
	LastEventAt        int64
	CurrentStage       string
	PlanGoal           string
	PlanStages         []PlanStage
	WorkspaceRoot      string // bound cwd, from session.workspace_bound
	LastOutput         string
	LastResponse       string
	Tools              []ToolRecord
	ToolsByStage       map[string][]ManifestTool
	TokenBudgetByStage map[string]int // stage -> ceiling, from stage.tool_manifest
	StageTokensUsed    int            // tokens used by the most recent inference in CurrentStage
	Events             []EventEntry
	// Pending is the *current* approval gate shown in the band. It always mirrors
	// PendingQueue[PendingIdx] (kept in sync by syncPending) so the render code can
	// keep reading a single pointer while multiple gates queue up behind it.
	Pending      *Approval
	PendingQueue []*Approval    // all pending gates, oldest first; navigated with ↑/↓
	PendingIdx   int            // selected index into PendingQueue
	Clar         *Clarification // open questions awaiting answers (clarification view)
	Propose      *Proposal      // router workflow suggestion awaiting a pick (propose view)
	Active       bool           // an inference/tool is in flight (drives the spinner)

	// LastReview is the most recent semantic reviewer (Gate 3) verdict for this
	// session, from review.findings. Nil until the first review lands.
	LastReview *ReviewResult
}

// ReviewResult is the semantic reviewer's verdict + findings over a stage's
// produced files (mirrors ReviewFindingsRaisedEvent).
type ReviewResult struct {
	StageID  string
	Verdict  string // PASS | WARN | FAIL
	Findings []ReviewFinding
	Blocked  bool
}

type ReviewFinding struct {
	Severity     string
	Confidence   float64
	Category     string
	Target       string
	Message      string
	SuggestedFix string
	Correctness  bool
}

// Workflow is a launchable workflow advertised by the server.
type Workflow struct {
	ID          string
	Description string
}

// Model is the whole TUI state. View is a pure function of it (immediate-mode),
// which is what keeps overlays from desyncing.
type Model struct {
	width, height int
	client        *ws.Client
	theme         Theme
	quitting      bool

	// connection
	connected    bool
	reconnecting bool

	// sessions
	sessions      []Session
	selectedID    string
	filter        string
	workflows     []Workflow
	wfIndex       int // -1 = not in workflow picker
	wfVisible     bool
	wfPendingID   string // workflow chosen, awaiting an intent line before StartSession
	wfPendingName string
	// launcher (idle screen): the active "what to launch" selection — 0 = chat, 1..N =
	// workflows[i-1] — cycled with Tab and shown at the input's lower-right. railHidden
	// folds away the idle status/quick-keys rail.
	launcherWf int
	railHidden bool
	bgUpdates  int

	// input
	editMode     EditMode
	inputMode    InputMode
	inputBuffer  string
	inputCursor  int
	inputHistory []string // submitted lines across all contexts (chat, intent, in-session), newest last
	historyIndex int      // -1 = editing the live buffer; else an index into inputHistory
	savedBuffer  string   // live buffer stashed while walking history

	// flow flags
	sessionEntered       bool
	approvalDismissed    bool
	pendingWorkflowFocus bool

	// router transcript
	routerMessages  map[string][]RouterEntry
	routerConnected bool
	chatMode        string
	// transcriptSel is the index (into the selected session's transcript) of the message
	// the operator has navigated to with ctrl+↑/↓; -1 = no selection (tail-follow). A
	// selected message is highlighted, scrolled into view, and is what `y` copies.
	transcriptSel int
	copiedFlash   int // frame at which a copy happened, for a brief "copied" footer note

	// provider
	currentModel string
	providerType string // LOCAL | REMOTE

	// managed-model swap (nil resource fields = unavailable)
	availableModels []string
	modelsIndex     int
	gpuUsedMB       *int64
	gpuTotalMB      *int64
	gpuUtil         *int
	ramMB           *int64
	sysRamUsedMB    *int64
	sysRamTotalMB   *int64

	// diff / overlay
	overlay          OverlayKind
	overlayEventIdx  int
	diffScrollOffset int
	modalScroll      int // body scroll for tall fixed-content modals (help, stats)
	eventStripShown  bool
	// in-session right panel: 0 = events, 1 = changes (token usage + changed files),
	// 2 = off (output full-width). Cycled with `d`.
	rightPanel int

	// artifact viewer (OverlayArtifacts) — populated by the artifact.list response
	artifacts        []protocol.ArtifactDto
	artifactsFor     string // sessionId the current listing belongs to
	artifactsIndex   int
	artifactScroll   int
	artifactsLoading bool

	// config editor (OverlayConfig) — populated by the config.snapshot response
	configFields  []protocol.ConfigFieldDto
	configIndex   int
	configStaged  map[string]string // key -> edited value, pending save
	configEditing bool              // true while typing a value into configEditBuf
	configEditBuf string
	configError   string
	configRestart []string // keys from the last save that need a restart
	configLoading bool

	// project profile editor (OverlayProjectProfile) — populated by project_profile.snapshot;
	// mirrors the config editor's field-list/staged-edit pattern. fields[0] is "about",
	// fields[1] is "conventions" (semicolon-joined), the rest are one row per command.
	projectProfileFields  []ProfileField
	projectProfileIndex   int
	projectProfileStaged  map[string]string
	projectProfileEditing bool
	projectProfileEditBuf string
	projectProfileError   string
	projectProfileLoading bool

	// operator profile editor (OverlayOperatorProfile) — populated by operator_profile.snapshot.
	// fields are "about", "approvalMode", "preferredModels" (comma-joined), "conventions"
	// (semicolon-joined). proposedAdaptation is read-only (from ProfileAdaptationService).
	operatorProfileFields   []ProfileField
	operatorProfileIndex    int
	operatorProfileStaged   map[string]string
	operatorProfileEditing  bool
	operatorProfileEditBuf  string
	operatorProfileError    string
	operatorProfileLoading  bool
	operatorProfileProposed string

	// session stats (OverlayStats) — populated by the session.stats reply
	stats        *protocol.StatsDto
	statsFor     string // sessionId the current stats belong to
	statsLoading bool

	// idea board (OverlayIdeas) — cross-session, populated by the idea.list reply
	ideas        []protocol.IdeaDto
	ideasIndex   int
	ideasLoading bool

	// health checks (OverlayHealth) — system-scoped, populated by the health.checks reply
	health        *protocol.HealthDto
	healthLoading bool

	// session browser (OverlaySessions) — recent sessions fetched over HTTP from
	// GET /sessions, so prior runs are reachable after a server restart + TUI reopen
	// (the WS stream only carries live sessions, not the historical roster).
	sessionList        []SessionSummary
	sessionListIndex   int
	sessionListLoading bool
	sessionListErr     string

	// task board (OverlayTasks) — all tasks across projects, fetched over HTTP from
	// GET /tasks. taskDetail toggles the per-task detail pane (built from the list payload).
	taskList         []TaskSummary
	taskListIndex    int
	taskListLoading  bool
	taskListErr      string
	taskDetail       bool
	taskDetailScroll int
	taskFilter       string // `/` substring narrow over id/title/goal
	taskFilterTyping bool

	// command palette
	paletteFilter string
	paletteIndex  int

	// @ file picker (OverlayFiles) — workspace paths from the file.list reply
	files        []string // all workspace-relative paths for filesFor
	filesFor     string   // sessionId the file list belongs to
	filesLoading bool
	fileFilter   string // the query typed after @ (narrows the list)
	fileIndex    int

	// status-bar segment visibility (OverlayStatusbar) — persisted TUI-local in tui-prefs.json.
	// A segment id present in sbHidden is hidden; absent = shown. sbIndex is the toggle cursor.
	sbHidden map[string]bool
	sbIndex  int

	// actionsHidden mutes the inline action rows (tool calls / writes / approvals / grants) in
	// the OUTPUT transcript; they always stay in the EVENTS panel. Persisted in tui-prefs.json.
	actionsHidden bool

	// thinkingShown reveals the model's reasoning/thinking blocks in the OUTPUT transcript.
	// Off by default (collapsed to a one-line summary) so the trace doesn't drown the answer;
	// toggled from the palette ("thinking"). Persisted in tui-prefs.json.
	thinkingShown bool

	// outputScroll is how many rows up from the bottom the OUTPUT transcript is scrolled
	// (0 = tail-follow the newest output). PgUp/PgDn + ctrl+u/d move it; esc snaps back.
	outputScroll int

	// lastReasoning holds the model's reasoning/CoT trace from the most recent
	// inference.completed, surfaced on the following tool-call and artifact rows.
	lastReasoning string

	// event-inspector filter (OverlayEventInspector): narrows the event list by a substring
	// of type/detail. eventFilterTyping is true while the operator is editing the query after /.
	eventFilter       string
	eventFilterTyping bool

	// standing grants (OverlayGrants) — active PROJECT/GLOBAL grants from the grant.list reply.
	grants        []protocol.GrantDto
	grantsLoading bool
	grantIndex    int
	// grant scope picker (OverlayGrantScope) — chosen when the operator presses A on an approval.
	// grantScopeIndex selects session/project/global; grantFor holds the approval being widened.
	grantScopeIndex int
	grantFor        *Approval

	// animation
	frame   int  // tick counter; drives spinner + caret blink
	ticking bool // true while a tick loop is scheduled. The loop is gated on animating()
	// so an idle screen stops redrawing — which lets native terminal selection survive
	// (the 120ms redraw used to wipe a mouse drag every frame).

	// snapshot phase
	snapshotPhase bool
	pendingEvents []protocol.ServerMessage

	// lastBase is the full-screen render behind the active modal, stashed by View each
	// frame so center() can composite a modal over a dimmed copy of it (transparent
	// backdrop) instead of an opaque scrim. Transient render scratch — not real state.
	lastBase string

	// approval steering input buffer
	steerBuffer string
	steering    bool
	// approvalArmed is set while a high-tier (T3+) approve is awaiting its second
	// confirming keypress — the destructive-action safety. Any non-confirm key
	// disarms it; a confirming `y`/`a`/enter sends the decision.
	approvalArmed bool

	// clarification view (the interactive question form)
	clarFocus     int            // focused question index
	clarCursor    int            // option cursor in the focused question (== len(opts) → custom slot)
	clarChosen    []map[int]bool // per-question selected option indices
	clarText      []string       // per-question free-text / custom answer
	clarTyping    bool           // typing into the custom buffer for the focused question
	clarDismissed bool           // peeked away from the form (it can be reopened)

	// workflow-propose view (the router's candidate-workflow picker)
	proposeCursor    int    // cursor over candidates (== len(candidates) → custom slot)
	proposeText      string // free-text answer typed into the custom slot
	proposeTyping    bool   // typing into the custom buffer
	proposeDismissed bool   // peeked away from the picker (it can be reopened)
}

// NewModel builds the initial idle state.
func NewModel(client *ws.Client) Model {
	return Model{
		client:                client,
		theme:                 NewTheme(SoftBlue),
		wfIndex:               -1,
		inputMode:             ModeRouter,
		historyIndex:          -1,
		routerMessages:        map[string][]RouterEntry{},
		configStaged:          map[string]string{},
		projectProfileStaged:  map[string]string{},
		operatorProfileStaged: map[string]string{},
		chatMode:              ChatModeChat,
		providerType:          "LOCAL",
		snapshotPhase:         true,
		eventStripShown:       true,
		transcriptSel:         -1,
		sbHidden:              loadStatusbarHidden(),
		actionsHidden:         loadPrefs().InlineActionsHidden,
		thinkingShown:         loadPrefs().ThinkingShown,
	}
}

// displayState derives the active screen. A session must be *entered*
// (sessionEntered) before its in-session or approval surfaces show — otherwise
// merely moving the list cursor onto a session with a pending gate would yank
// you into the approval, and `l` back-to-list couldn't escape it.
func (m Model) displayState() DisplayState {
	if m.selectedID == "" || !m.sessionEntered {
		return StateIdle
	}
	s := m.session(m.selectedID)
	// A stage's open questions take precedence: the run is parked on them, and they
	// must be answered (or peeked away) before anything else makes sense.
	if s != nil && s.Clar != nil && !m.clarDismissed {
		return StateClarification
	}
	// A router workflow proposal is the operator's call to make before chatting on.
	if s != nil && s.Propose != nil && !m.proposeDismissed {
		return StateWorkflowPropose
	}
	if m.approvalDismissed {
		return StateInSession
	}
	if s != nil && s.Pending != nil {
		return StateApproval
	}
	return StateInSession
}

func (m Model) session(id string) *Session {
	for i := range m.sessions {
		if m.sessions[i].ID == id {
			return &m.sessions[i]
		}
	}
	return nil
}

// syncPending re-points Pending at the selected queue entry, clamping the index
// so it stays in range as the queue grows and shrinks. Called after any queue
// mutation; Pending is nil exactly when the queue is empty.
func (s *Session) syncPending() {
	if len(s.PendingQueue) == 0 {
		s.PendingQueue = nil
		s.PendingIdx = 0
		s.Pending = nil
		return
	}
	if s.PendingIdx < 0 {
		s.PendingIdx = 0
	}
	if s.PendingIdx >= len(s.PendingQueue) {
		s.PendingIdx = len(s.PendingQueue) - 1
	}
	s.Pending = s.PendingQueue[s.PendingIdx]
}

// enqueueApproval adds a gate to the queue (replacing one with the same request
// id, so a re-sent gate doesn't duplicate). The freshly-added gate is left where
// it is in the order; the current selection is preserved.
func (s *Session) enqueueApproval(a *Approval) {
	for i, p := range s.PendingQueue {
		if p.RequestID == a.RequestID {
			s.PendingQueue[i] = a
			s.syncPending()
			return
		}
	}
	s.PendingQueue = append(s.PendingQueue, a)
	s.syncPending()
}

// removeApproval drops the gate with requestID and advances the selection. If the
// removed entry was before the cursor the index shifts down to stay on the same
// gate; if it was the selected one the cursor stays put (now showing the next gate).
func (s *Session) removeApproval(requestID string) {
	idx := -1
	for i, p := range s.PendingQueue {
		if p.RequestID == requestID {
			idx = i
			break
		}
	}
	if idx < 0 {
		return
	}
	s.PendingQueue = append(s.PendingQueue[:idx], s.PendingQueue[idx+1:]...)
	if idx < s.PendingIdx {
		s.PendingIdx--
	}
	s.syncPending()
}

// clearApprovals empties the queue (used on resume/completion, where the server
// invalidates every gate at once).
func (s *Session) clearApprovals() {
	s.PendingQueue = nil
	s.PendingIdx = 0
	s.Pending = nil
}

// navApproval moves the queue selection by dir (wrapping) and re-syncs Pending.
func (s *Session) navApproval(dir int) {
	n := len(s.PendingQueue)
	if n <= 1 {
		return
	}
	s.PendingIdx = (s.PendingIdx + dir + n) % n
	s.syncPending()
}

// ensureSession returns the session with id, creating an ACTIVE entry if absent.
// Session existence is derived from the event stream (any event for an unknown
// session vivifies it) rather than a dedicated control frame.
func (m *Model) ensureSession(id string) *Session {
	if s := m.session(id); s != nil {
		return s
	}
	m.sessions = append(m.sessions, Session{
		ID: id, Status: "ACTIVE", LastEventAt: nowMillis(),
	})
	return &m.sessions[len(m.sessions)-1]
}

// filteredSessions applies the workflow-id filter.
func (m Model) filteredSessions() []Session {
	if m.filter == "" {
		return m.sessions
	}
	out := make([]Session, 0, len(m.sessions))
	for _, s := range m.sessions {
		if containsFold(s.WorkflowID, m.filter) {
			out = append(out, s)
		}
	}
	return out
}
