"""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 unicurses import (  # type: ignore[import-untyped]
	A_REVERSE,
	KEY_BACKSPACE,
	KEY_DOWN,
	KEY_ENTER,
	KEY_LEFT,
	KEY_RESIZE,
	KEY_RIGHT,
	KEY_UP,
)

from pygittools.tui.commit_draft import save_commit_draft
from pygittools.tui.draw import draw_line
from pygittools.tui.worktree import (
	categorize_status,
	commit_staged,
	stage_path,
	stage_paths,
	unstage_path,
	unstage_paths,
)

_COMMIT_PREFIX = "Commit: "


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


@dataclass(frozen=True, slots=True)
class InputKeyResult:
	layout_changed: bool = False
	relinquish: 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._cursor = 0
		self._scroll_offset = 0
		self._rows: list[_Row] = []
		self._commit_message = ""
		self._error: str | None = None
		self._staged_count = 0

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

	def set_commit_message(self, message: str) -> None:
		self._commit_message = 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._cursor == 0

	def refresh(self, repo: Repository) -> None:
		staged, unstaged, untracked = categorize_status(repo)
		self._staged_count = len(staged)
		files_by_section = {
			SectionId.STAGED: staged,
			SectionId.UNSTAGED: unstaged,
			SectionId.UNTRACKED: untracked,
		}
		rows: list[_Row] = [
			_Row(
				kind="commit",
				section=None,
				path=None,
				label=self._commit_label(width=120, focused=False),
			),
		]
		for section_id, title in _SECTION_ORDER:
			files = files_by_section[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
		self._cursor = min(self._cursor, max(0, len(self._rows) - 1))

	def draw(self, top_row: int, height: int, width: int, *, highlight: bool = True) -> None:
		if height <= 0:
			return
		self._ensure_cursor_visible(height)
		for view_row in range(height):
			row_index = self._scroll_offset + view_row
			if row_index >= len(self._rows):
				break
			row = self._rows[row_index]
			focused = row_index == self._cursor
			label = self._commit_label(width, focused and highlight) if row.kind == "commit" else row.label
			attr = A_REVERSE if focused and highlight else 0
			draw_line(top_row + view_row, 0, label, width, attr)

	def handle_nav_key(self, key: int, repo: Repository) -> NavKeyResult:
		"""Handle navigation keys. May begin commit input when typing on the commit row."""
		if key == KEY_RESIZE:
			return NavKeyResult()
		if self._error is not None:
			self._error = None

		if key in (KEY_UP, ord("k"), ord("K")):
			self._cursor = max(0, self._cursor - 1)
			return NavKeyResult()
		if key in (KEY_DOWN, ord("j"), ord("J")):
			self._cursor = min(max(0, len(self._rows) - 1), self._cursor + 1)
			return NavKeyResult()
		if not self._rows:
			return NavKeyResult()

		row = self._rows[self._cursor]
		if row.kind == "commit":
			if self._try_begin_input(key):
				return NavKeyResult(enter_input=True)
			layout_changed = self._handle_commit_nav_key(key, repo)
			return NavKeyResult(layout_changed=layout_changed)

		if key in (KEY_LEFT, KEY_RIGHT, ord(" ")) and row.kind == "header" and row.section is not None:
			self._collapsed[row.section] = not self._collapsed[row.section]
			self.refresh(repo)
			return NavKeyResult(layout_changed=True)
		if key in (KEY_ENTER, 10, 13) 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 key in (KEY_ENTER, 10, 13) 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: int, repo: Repository) -> InputKeyResult:
		"""Handle keys while commit input context is active. Esc is handled by InputContext."""
		if key == KEY_RESIZE:
			return InputKeyResult()
		if self._error is not None:
			self._error = None
		if key in (KEY_ENTER, 10, 13):
			ok, error = commit_staged(repo, self._commit_message)
			if ok:
				self._commit_message = ""
				self.persist_commit_draft(repo)
				self._error = None
				self.refresh(repo)
				return InputKeyResult(layout_changed=True, relinquish=True)
			self._error = error
			return InputKeyResult()
		if key in (KEY_BACKSPACE, 127, 8):
			if self._commit_message:
				self._commit_message = self._commit_message[:-1]
			return InputKeyResult()
		if 32 <= key <= 126:
			self._commit_message += chr(key)
			return InputKeyResult()
		return InputKeyResult()

	def status_hint(self) -> str:
		if self._error:
			return self._error
		if not self._rows:
			return "Working tree clean"
		row = self._rows[self._cursor]
		if row.kind == "commit":
			return "Type message — Enter commit — j/k move"
		if row.kind == "header":
			if row.section == SectionId.STAGED:
				if self._staged_count == 0:
					return "Enter stage all — Space collapse — j/k move"
				return "Enter unstage all — Space collapse — j/k move"
			return "Enter stage all — Space collapse — j/k move"
		if row.section == SectionId.STAGED:
			return "Enter unstage — j/k move"
		return "Enter stage — j/k move"

	def _try_begin_input(self, key: int) -> bool:
		if key in (KEY_BACKSPACE, 127, 8):
			if not self._commit_message:
				return False
			self._commit_message = self._commit_message[:-1]
			return True
		if 32 <= key <= 126:
			self._commit_message += chr(key)
			return True
		return False

	def _handle_commit_nav_key(self, key: int, repo: Repository) -> bool:
		if key in (KEY_ENTER, 10, 13):
			ok, error = commit_staged(repo, self._commit_message)
			if ok:
				self._commit_message = ""
				self.persist_commit_draft(repo)
				self._error = None
				self.refresh(repo)
				return True
			self._error = error
		return False

	def _commit_label(self, width: int, focused: bool) -> str:
		max_message = max(0, width - len(_COMMIT_PREFIX) - (1 if focused else 0))
		message = self._commit_message
		if len(message) > max_message:
			message = message[-max_message:]
		label = f"{_COMMIT_PREFIX}{message}"
		if focused:
			label += "_"
		return label[:width]

	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:
		staged, unstaged, untracked = categorize_status(repo)
		paths_by_section = {
			SectionId.STAGED: staged,
			SectionId.UNSTAGED: unstaged,
			SectionId.UNTRACKED: untracked,
		}
		paths = paths_by_section[section]
		if section == SectionId.STAGED:
			if paths:
				unstage_paths(repo, paths)
			else:
				stage_paths(repo, unstaged + untracked)
		else:
			stage_paths(repo, paths)

	def _ensure_cursor_visible(self, height: int) -> None:
		if self._cursor < self._scroll_offset:
			self._scroll_offset = self._cursor
		elif self._cursor >= self._scroll_offset + height:
			self._scroll_offset = self._cursor - height + 1