"""Collapsible staged / unstaged / untracked file list with keyboard control."""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Literal

from pygit2 import Repository
from pyratatui import (
	Constraint,
	Direction,
	Frame,
	Layout,
	Line,
	List,
	ListItem,
	ListState,
	Paragraph,
	Rect,
	Span,
	Style,
	Text,
)

from pygittools.tui.commit_draft import save_commit_draft
from pygittools.tui.git_colors import current_theme, focus_style, row_style_for_section
from pygittools.tui.keys import (
	KeyLike,
	is_backtab,
	is_down,
	is_enter,
	is_left,
	is_right,
	is_space,
	is_tab,
	is_up,
	move_hint,
	vim_enabled,
)
from pygittools.tui.multiline_edit import MultilineEdit
from pygittools.tui.worktree import (
	categorize_status,
	commit_staged,
	stage_path,
	stage_paths,
	unstage_path,
	unstage_paths,
)

_InputFocus = Literal["message", "commit", "amend"]
_EDITOR_MIN_HEIGHT = 3
_EDITOR_MAX_HEIGHT = 8


class SectionId(Enum):
	STAGED = "staged"
	UNSTAGED = "unstaged"
	UNTRACKED = "untracked"


@dataclass(frozen=True, slots=True)
class _Row:
	kind: Literal["commit", "header", "file"]
	section: SectionId | None
	path: str | None
	label: str


@dataclass(frozen=True, slots=True)
class NavKeyResult:
	enter_input: bool = False
	layout_changed: bool = False
	committed: bool = False


@dataclass(frozen=True, slots=True)
class InputKeyResult:
	layout_changed: bool = False
	relinquish: bool = False
	committed: bool = False


_SECTION_ORDER: tuple[tuple[SectionId, str], ...] = (
	(SectionId.STAGED, "Staged"),
	(SectionId.UNSTAGED, "Unstaged"),
	(SectionId.UNTRACKED, "Untracked"),
)


class ChangesList:
	def __init__(self) -> None:
		self._collapsed: dict[SectionId, bool] = dict.fromkeys(SectionId, False)
		self._files: dict[SectionId, list[str]] = {section: [] for section in SectionId}
		self._rows: list[_Row] = []
		self._list_state = ListState()
		self._commit_edit = MultilineEdit()
		self._input_focus: _InputFocus = "message"
		self._error: str | None = None
		self._staged_count = 0
		self._can_amend = False

	@property
	def commit_message(self) -> str:
		return self._commit_edit.value()

	def set_commit_message(self, message: str) -> None:
		self._commit_edit.set_value(message)

	def persist_commit_draft(self, repo: Repository) -> None:
		save_commit_draft(Path(repo.path), self.commit_message)

	def is_at_top(self) -> bool:
		return (self._list_state.selected or 0) == 0

	def select_commit_row(self) -> None:
		self._list_state.select(0)

	def begin_input(self) -> None:
		self._input_focus = "message"
		self.select_commit_row()

	def refresh(self, repo: Repository) -> None:
		staged, unstaged, untracked = categorize_status(repo)
		self._files = {
			SectionId.STAGED: staged,
			SectionId.UNSTAGED: unstaged,
			SectionId.UNTRACKED: untracked,
		}
		self._staged_count = len(staged)
		self._can_amend = not repo.head_is_unborn
		self._rebuild_rows()

	def _rebuild_rows(self) -> None:
		rows: list[_Row] = [
			_Row(kind="commit", section=None, path=None, label=self._commit_label()),
		]
		for section_id, title in _SECTION_ORDER:
			files = self._files[section_id]
			marker = ">" if self._collapsed[section_id] else "v"
			rows.append(
				_Row(
					kind="header",
					section=section_id,
					path=None,
					label=f"[{marker}] {title} ({len(files)})",
				),
			)
			if not self._collapsed[section_id]:
				for path in files:
					rows.append(_Row(kind="file", section=section_id, path=path, label=f"  {path}"))
		self._rows = rows
		selected = self._list_state.selected or 0
		self._list_state.select(min(selected, max(0, len(self._rows) - 1)))

	def draw(
		self,
		frame: Frame,
		area: Rect,
		*,
		highlight: bool = True,
		commit_input: bool = False,
	) -> None:
		if area.height <= 0 or area.width <= 0:
			return
		theme = current_theme()
		list_area = area
		if commit_input:
			editor_height = max(_EDITOR_MIN_HEIGHT, min(_EDITOR_MAX_HEIGHT, max(1, area.height // 3)))
			chunks = (
				Layout()
				.direction(Direction.Vertical)
				.constraints([
					Constraint.length(editor_height),
					Constraint.length(1),
					Constraint.fill(1),
				])
				.split(area)
			)
			self._draw_editor(frame, chunks[0], focused=self._input_focus == "message")
			self._draw_buttons(frame, chunks[1])
			list_area = chunks[2]

		items: list[ListItem] = []
		selected = self._list_state.selected or 0
		for index, row in enumerate(self._rows):
			if commit_input and row.kind == "commit":
				continue
			focused = highlight and index == selected and not commit_input
			if row.kind == "commit":
				style = focus_style(theme.commit) if focused else theme.commit
				items.append(ListItem(self._commit_label(), style))
				continue
			if row.kind == "header" and row.section is not None:
				style = row_style_for_section(
					row.section.value,
					focused=focused,
					highlight=highlight,
					is_header=True,
				)
			elif row.section is not None:
				style = row_style_for_section(
					row.section.value,
					focused=focused,
					highlight=highlight,
				)
			else:
				style = Style()
			items.append(ListItem(row.label, style))

		if not items:
			items.append(ListItem(" ", theme.normal))
		list_state = self._list_state
		if commit_input:
			list_state = ListState()
			list_state.select(None)
		frame.render_stateful_list(
			List(items).highlight_style(theme.focus).highlight_symbol(""),
			list_area,
			list_state,
		)

	def _draw_editor(self, frame: Frame, area: Rect, *, focused: bool) -> None:
		theme = current_theme()
		style = theme.commit
		cursor_style = theme.focus if focused else theme.commit
		text = self._commit_edit.render(
			max(1, area.width),
			max(1, area.height),
			focused=focused,
			style=style,
			cursor_style=cursor_style,
		)
		frame.render_widget(Paragraph(text), area)

	def _draw_buttons(self, frame: Frame, area: Rect) -> None:
		theme = current_theme()
		commit_style = theme.focus if self._input_focus == "commit" else theme.header
		amend_style = theme.focus if self._input_focus == "amend" else theme.header
		if not self._can_amend and self._input_focus != "amend":
			amend_style = theme.log_date.dim()
		text = Text([
			Line([
				Span("[ Commit ]", commit_style),
				Span("  ", theme.normal),
				Span("[ Commit (amend) ]", amend_style),
			]),
		])
		frame.render_widget(Paragraph(text), area)

	def handle_nav_key(self, key: KeyLike, repo: Repository) -> NavKeyResult:
		if self._error is not None:
			self._error = None

		if is_up(key):
			selected = self._list_state.selected or 0
			self._list_state.select(max(0, selected - 1))
			return NavKeyResult()
		if is_down(key):
			selected = self._list_state.selected or 0
			self._list_state.select(min(max(0, len(self._rows) - 1), selected + 1))
			return NavKeyResult()
		if not self._rows:
			return NavKeyResult()

		row = self._rows[self._list_state.selected or 0]
		if row.kind == "commit":
			if is_enter(key) or (vim_enabled() and key.code in {"i", "I"}):
				self.begin_input()
				return NavKeyResult(enter_input=True)
			return NavKeyResult()

		if (is_left(key) or is_right(key) or is_space(key)) and row.kind == "header" and row.section is not None:
			self._collapsed[row.section] = not self._collapsed[row.section]
			self._rebuild_rows()
			return NavKeyResult(layout_changed=True)
		if is_enter(key) and row.kind == "header" and row.section is not None:
			self._toggle_section(repo, row.section)
			self.refresh(repo)
			return NavKeyResult(layout_changed=True)
		if is_enter(key) and row.kind == "file" and row.path is not None and row.section is not None:
			self._toggle_stage(repo, row.section, row.path)
			self.refresh(repo)
			return NavKeyResult(layout_changed=True)
		return NavKeyResult()

	def handle_input_key(self, key: KeyLike, repo: Repository) -> InputKeyResult:
		if self._error is not None:
			self._error = None

		if is_tab(key):
			self._input_focus = _next_focus(self._input_focus, self._can_amend)
			return InputKeyResult()
		if is_backtab(key):
			self._input_focus = _prev_focus(self._input_focus, self._can_amend)
			return InputKeyResult()

		if self._input_focus == "message":
			if key.code == "Down" and self._commit_edit.at_last_visual_line():
				self._input_focus = "commit"
				return InputKeyResult()
			self._commit_edit.handle_key(key)
			return InputKeyResult()

		# Button row: arrows only so vim letters cannot steal focus while editing.
		if key.code == "Up":
			self._input_focus = "message"
			return InputKeyResult()
		if key.code == "Left" and self._input_focus == "amend":
			self._input_focus = "commit"
			return InputKeyResult()
		if key.code == "Right" and self._input_focus == "commit" and self._can_amend:
			self._input_focus = "amend"
			return InputKeyResult()
		if is_enter(key):
			return self._run_commit(repo, amend=self._input_focus == "amend")
		return InputKeyResult()

	def status_hint(self) -> str:
		if self._error:
			return self._error
		if not self._rows:
			return "Working tree clean"
		move = move_hint()
		row = self._rows[self._list_state.selected or 0]
		if row.kind == "commit":
			insert = "Enter/i edit" if vim_enabled() else "Enter edit"
			return f"{insert} — {move} — / command"
		if row.kind == "header":
			if row.section == SectionId.STAGED:
				if self._staged_count == 0:
					return f"Enter stage all — Space collapse — {move}"
				return f"Enter unstage all — Space collapse — {move}"
			return f"Enter stage all — Space collapse — {move}"
		if row.section == SectionId.STAGED:
			return f"Enter unstage — {move}"
		return f"Enter stage — {move}"

	def input_status_hint(self) -> str:
		if self._error:
			return self._error
		if self._input_focus == "message":
			return "Esc back — Enter newline — Tab buttons — ↓ Commit"
		if self._input_focus == "commit":
			return "Enter commit — ←/→ amend — Esc back"
		if self._can_amend:
			return "Enter amend — ←/→ commit — Esc back"
		return "Nothing to amend — ← commit — Esc back"

	def _run_commit(self, repo: Repository, *, amend: bool) -> InputKeyResult:
		ok, error = commit_staged(repo, self.commit_message, amend=amend)
		if ok:
			self._commit_edit.set_value("")
			self.persist_commit_draft(repo)
			self._error = None
			self.refresh(repo)
			return InputKeyResult(layout_changed=True, relinquish=True, committed=True)
		self._error = error
		return InputKeyResult()

	def _commit_label(self) -> str:
		message = self.commit_message
		if not message.strip():
			return "Commit: (edit message)"
		lines = message.splitlines()
		first = lines[0]
		if len(lines) > 1:
			return f"Commit: {first}…"
		return f"Commit: {first}"

	def _toggle_stage(self, repo: Repository, section: SectionId, path: str) -> None:
		if section == SectionId.STAGED:
			unstage_path(repo, path)
		else:
			stage_path(repo, path)

	def _toggle_section(self, repo: Repository, section: SectionId) -> None:
		paths = self._files[section]
		if section == SectionId.STAGED:
			if paths:
				unstage_paths(repo, paths)
			else:
				stage_paths(repo, self._files[SectionId.UNSTAGED] + self._files[SectionId.UNTRACKED])
		else:
			stage_paths(repo, paths)


def _next_focus(current: _InputFocus, can_amend: bool) -> _InputFocus:
	if current == "message":
		return "commit"
	if current == "commit":
		return "amend" if can_amend else "message"
	return "message"


def _prev_focus(current: _InputFocus, can_amend: bool) -> _InputFocus:
	if current == "message":
		return "amend" if can_amend else "commit"
	if current == "amend":
		return "commit"
	return "message"