package tui

import (
	"regexp"
	"strings"
	"unicode"
)

// Unicode markers used by the Claude Code TUI.
const (
	markerUser       = '❯' // U+276F — user prompt
	markerAssistant  = '●' // U+25CF — assistant response / tool call
	markerToolResult = '⎿' // U+23BF — tool result
	markerProgress   = '✻' // U+273B — progress indicator (ignore)
	markerBoxTL      = '╭' // U+256D — top-left box corner (banner start)
	markerBoxBL      = '╰' // U+2570 — bottom-left box corner (banner end)
	markerBoxBR      = '╯' // U+256F — bottom-right box corner (banner end)
	markerHRule      = '─' // U+2500 — horizontal rule
)

// reToolUse matches "Identifier(anything)" — a tool_use line.
var reToolUse = regexp.MustCompile(`^([A-Za-z_][A-Za-z0-9_]*)\((.*)\)\s*$`)

// reProgress matches Claude's generation status/spinner line by its stable
// signature: "(Ns … tokens" or "esc to interrupt". Used when the line still
// carries that suffix, e.g. "✽ Whatchamacalliting… (2s · ↓ 1 tokens · esc to interrupt)".
var reProgress = regexp.MustCompile(`\(\d+s\b[^)]*tokens?\b|esc to interrupt`)

// reSpinner matches the spinner line by STRUCTURE rather than by its (infinite,
// ever-changing) gerund word: a non-alphanumeric glyph (✻ ✽ ✢ ✶ ✺ …) followed by
// a single word and a horizontal ellipsis, e.g. "✽ Forging…" or "✶ Puzzling…".
// This catches early frames that don't yet show the "(Ns · tokens)" suffix. The
// caller guards known turn markers (●/❯/⎿) so a legitimate answer ending in "…"
// is not misclassified.
var reSpinner = regexp.MustCompile(`^\s*[^\p{L}\p{N}\s]\s+\p{L}[\p{L}'’\-]*…`)

// ClaudeTurnRole classifies each turn/block extracted from the screen.
type ClaudeTurnRole string

const (
	// ClaudeTurnUser is a message typed by the user (line starting with "❯ ").
	ClaudeTurnUser ClaudeTurnRole = "user"
	// ClaudeTurnAssistant is a response block from the assistant.
	ClaudeTurnAssistant ClaudeTurnRole = "assistant"
	// ClaudeTurnToolUse is a tool invocation "● ToolName(args)".
	ClaudeTurnToolUse ClaudeTurnRole = "tool_use"
	// ClaudeTurnToolResult is a result line "⎿ ..." following a tool_use.
	ClaudeTurnToolResult ClaudeTurnRole = "tool_result"
)

// ClaudeTurn is a single conversation block extracted from the rendered screen.
type ClaudeTurn struct {
	Role     ClaudeTurnRole `json:"role"`
	Text     string         `json:"text"`                  // textual content (multiline joined with \n)
	ToolName string         `json:"tool_name,omitempty"`   // only for tool_use
}

// ClaudeTUIParse is the result of parsing one captured Claude TUI screen.
type ClaudeTUIParse struct {
	Turns  []ClaudeTurn `json:"turns"`  // all visible turns in order
	Answer string       `json:"answer"` // assistant reply to the last user turn (like `claude -p`)
}

// ParseClaudeTUI parses the rendered text of a Claude Code TUI screen and
// extracts the conversation turns and the final assistant answer.
//
// The screen is expected to be the output of VTRender applied to a PTY
// capture of the claude CLI. Heuristics handle the welcome banner, status
// bar, progress lines and multi-line continuations.
func ParseClaudeTUI(screen string) ClaudeTUIParse {
	lines := strings.Split(screen, "\n")

	// --- Step 1: strip the welcome banner (box drawn with ╭...╰/╯) ---
	lines = stripBanner(lines)

	// --- Step 2: strip the status bar at the bottom ---
	lines = stripStatusBar(lines)

	// --- Step 3: collect turns from the remaining lines ---
	turns := extractTurns(lines)

	// --- Step 4: compute Answer from turns ---
	answer := computeAnswer(turns)

	return ClaudeTUIParse{Turns: turns, Answer: answer}
}

// stripBanner removes the welcome banner block from the top of the lines
// slice. The banner is a Unicode box that starts with a line containing ╭
// and ends with a line containing ╰ or ╯.
func stripBanner(lines []string) []string {
	// Find a banner start (╭) in the first ~15 lines.
	startIdx := -1
	for i := 0; i < len(lines) && i < 15; i++ {
		if strings.ContainsRune(lines[i], markerBoxTL) {
			startIdx = i
			break
		}
	}
	if startIdx < 0 {
		return lines
	}

	// Find the matching close (╰ or ╯) after the start.
	for i := startIdx; i < len(lines); i++ {
		if strings.ContainsRune(lines[i], markerBoxBL) || strings.ContainsRune(lines[i], markerBoxBR) {
			return lines[i+1:]
		}
	}
	return lines
}

// isHRule returns true when the line consists mostly of ─ (U+2500) characters
// — at least 40 of them and the line has no other significant content.
func isHRule(line string) bool {
	count := 0
	for _, r := range line {
		if r == markerHRule {
			count++
		}
	}
	return count >= 40
}

// isStatusBarLine returns true for lines that belong to the Claude status bar
// (CTX:, IN:, OUT:, Total:, Limits:, $, "← for agents", etc.).
func isStatusBarLine(line string) bool {
	trimmed := strings.TrimSpace(line)
	if trimmed == "" {
		return false
	}
	prefixes := []string{
		"CTX:", "IN:", "OUT:", "Total:", "Limits:", "$", "← for agents",
	}
	for _, p := range prefixes {
		if strings.Contains(trimmed, p) {
			return true
		}
	}
	return false
}

// stripStatusBar removes the status bar at the bottom of the lines slice.
// Strategy: scan from the bottom upward. The footer looks like:
//
//	<hrule>
//	❯                   (empty prompt)
//	<hrule>
//	<status lines with CTX: / $0.xxx / ← for agents ...>
//
// We look for the LAST hrule that is followed by an empty-prompt line and
// another hrule, and discard everything from that hrule onward.
// Additionally, any trailing status-bar-flavored lines are dropped first.
func stripStatusBar(lines []string) []string {
	if len(lines) == 0 {
		return lines
	}

	// Trim trailing blank lines first.
	end := len(lines)
	for end > 0 && strings.TrimSpace(lines[end-1]) == "" {
		end--
	}
	lines = lines[:end]

	// Remove explicit status-bar lines from the bottom.
	for len(lines) > 0 && isStatusBarLine(lines[len(lines)-1]) {
		lines = lines[:len(lines)-1]
	}

	// Now find the pattern: hrule / empty-❯ / hrule and cut there.
	// Scan from the bottom upward.
	for i := len(lines) - 1; i >= 2; i-- {
		if !isHRule(lines[i]) {
			continue
		}
		// Check that lines[i-1] is the empty prompt "❯" (optional surrounding spaces).
		mid := strings.TrimSpace(lines[i-1])
		if mid != string([]rune{markerUser}) && mid != string([]rune{markerUser, ' '}) {
			// Also allow a completely empty line (prompt area can be blank).
			if mid != "" {
				continue
			}
		}
		// Check lines[i-2] is also an hrule.
		if isHRule(lines[i-2]) {
			// Cut from lines[i-2] onward.
			lines = lines[:i-2]
			break
		}
	}

	// Trim trailing blank lines again after stripping.
	for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
		lines = lines[:len(lines)-1]
	}

	return lines
}

// firstRune returns the first non-space rune in s, or 0 if s is blank.
func firstRune(s string) rune {
	for _, r := range s {
		if !unicode.IsSpace(r) {
			return r
		}
	}
	return 0
}

// isMarkerLine returns true when the line starts with one of the recognised
// turn markers (❯, ●, ⎿, ✻).
func isMarkerLine(line string) bool {
	r := firstRune(line)
	return r == markerUser || r == markerAssistant || r == markerToolResult || r == markerProgress
}

// isProgressLine reports whether the line is a Claude generation status/spinner
// line (the animated "✻/✽ Word… (Ns · ↓ N tokens · esc to interrupt)" indicator).
// The glyph and the gerund word change on every frame, so it is detected by
// structure/signature, never by the specific word. These lines are noise and must
// never be folded into an assistant answer — critical when capturing frames
// mid-generation (streaming), where a different "loading" word appears each tick.
func isProgressLine(line string) bool {
	r := firstRune(line)
	if r == markerProgress {
		return true
	}
	// Known turn markers are never progress, even if they end in "…".
	if r == markerUser || r == markerAssistant || r == markerToolResult {
		return false
	}
	return reProgress.MatchString(line) || reSpinner.MatchString(line)
}

// isBreakLine reports whether the line should end an assistant/user/tool
// continuation: either a turn marker or a progress/spinner line.
func isBreakLine(line string) bool {
	return isMarkerLine(line) || isProgressLine(line)
}

// textAfterMarker returns the text that follows the first occurrence of
// marker in line, trimmed of leading spaces.
func textAfterMarker(line string, marker rune) string {
	idx := strings.IndexRune(line, marker)
	if idx < 0 {
		return ""
	}
	rest := line[idx+len(string(marker)):]
	return strings.TrimLeft(rest, " ")
}

// extractTurns scans lines and groups them into ClaudeTurn slices.
func extractTurns(lines []string) []ClaudeTurn {
	var turns []ClaudeTurn

	i := 0
	for i < len(lines) {
		line := lines[i]

		// Progress/spinner lines are noise in any position — skip early so they
		// are never folded into an assistant continuation (matters for streaming).
		if isProgressLine(line) {
			i++
			continue
		}

		r := firstRune(line)

		switch r {
		case markerProgress:
			// ✻ lines are noise — skip (also covered by isProgressLine above).
			i++

		case markerUser:
			text := textAfterMarker(line, markerUser)
			if text == "" {
				// Empty prompt — skip.
				i++
				continue
			}
			// Collect continuation lines (indented, non-marker, non-empty).
			i++
			for i < len(lines) {
				cont := lines[i]
				if isBreakLine(cont) || strings.TrimSpace(cont) == "" {
					break
				}
				text += "\n" + strings.TrimRight(cont, " ")
				i++
			}
			turns = append(turns, ClaudeTurn{Role: ClaudeTurnUser, Text: strings.TrimRight(text, " ")})

		case markerAssistant:
			body := textAfterMarker(line, markerAssistant)
			i++

			// Determine if this is a tool_use or assistant text.
			if m := reToolUse.FindStringSubmatch(body); m != nil {
				// tool_use — do NOT collect continuation lines.
				turns = append(turns, ClaudeTurn{
					Role:     ClaudeTurnToolUse,
					Text:     body,
					ToolName: m[1],
				})
			} else {
				// assistant text — collect continuation lines.
				for i < len(lines) {
					cont := lines[i]
					if isBreakLine(cont) {
						break
					}
					trimmed := strings.TrimSpace(cont)
					if trimmed == "" {
						// A single blank line may separate paragraphs; peek ahead.
						// If the next non-blank line is also a continuation, keep it.
						j := i + 1
						for j < len(lines) && strings.TrimSpace(lines[j]) == "" {
							j++
						}
						if j < len(lines) && !isBreakLine(lines[j]) {
							// Include the blank line(s) as paragraph separator.
							body += "\n"
							i = j
							continue
						}
						break
					}
					body += "\n" + strings.TrimRight(cont, " ")
					i++
				}
				turns = append(turns, ClaudeTurn{
					Role: ClaudeTurnAssistant,
					Text: strings.TrimRight(body, " \n"),
				})
			}

		case markerToolResult:
			text := textAfterMarker(line, markerToolResult)
			// Also accept └ as alias (some terminals substitute).
			if text == "" {
				text = textAfterMarker(line, '└')
			}
			i++
			// Collect continuation lines for the tool result.
			for i < len(lines) {
				cont := lines[i]
				if isBreakLine(cont) || strings.TrimSpace(cont) == "" {
					break
				}
				text += "\n" + strings.TrimRight(cont, " ")
				i++
			}
			turns = append(turns, ClaudeTurn{
				Role: ClaudeTurnToolResult,
				Text: strings.TrimRight(text, " "),
			})

		default:
			// Blank or unrecognised line — skip.
			i++
		}
	}

	return turns
}

// computeAnswer finds the last user turn and concatenates all assistant
// (non-tool_use, non-tool_result) turns that follow it.
// If there is no user turn, concatenates all assistant turns.
func computeAnswer(turns []ClaudeTurn) string {
	lastUserIdx := -1
	for i, t := range turns {
		if t.Role == ClaudeTurnUser {
			lastUserIdx = i
		}
	}

	var parts []string
	start := 0
	if lastUserIdx >= 0 {
		start = lastUserIdx + 1
	}
	for _, t := range turns[start:] {
		if t.Role == ClaudeTurnAssistant {
			parts = append(parts, t.Text)
		}
	}
	return strings.TrimSpace(strings.Join(parts, "\n"))
}
