"""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"