"""Project actions page: run scripts from ``pyproject.toml`` or ``actions.toml``."""

from __future__ import annotations

import threading
import time
from enum import Enum, auto
from pathlib import Path

from pyratatui import (
    Constraint,
    Direction,
    Frame,
    Layout,
    List,
    ListItem,
    ListState,
    Paragraph,
    Rect,
    Style,
)

from pygittools.actions import ActionSet, ActionSpec, ProjectActions, find_project_root, load_action_sets
from pygittools.tui.git_colors import current_theme, focus_style
from pygittools.tui.keys import (
    KeyLike,
    is_backtab,
    is_down,
    is_enter,
    is_esc,
    is_left,
    is_quit,
    is_right,
    is_tab,
    is_up,
    move_hint,
    vim_enabled,
)
from pygittools.tui.tabs import render_tab_row
from pygittools.tui.types import PageAction, PageContext, PageResult

_SPINNER = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
_RUN_ALL_LABEL = "Run all"
_SPINNER_INTERVAL_S = 0.08


def _worktree_path(repo_path: Path) -> Path:
    resolved = repo_path.resolve()
    if resolved.name == ".git":
        return resolved.parent
    return resolved


class _RunState(Enum):
    IDLE = auto()
    RUNNING = auto()
    SUCCESS = auto()
    FAILURE = auto()


class ActionsPage:
    title = "actions"

    def __init__(self) -> None:
        self._ctx: PageContext | None = None
        self._project_root: Path | None = None
        self._sets: list[ActionSet] = []
        self._set_index = 0
        self._list_state = ListState()
        self._states: dict[tuple[str, str], _RunState] = {}
        self._messages: dict[tuple[str, str], str] = {}
        self._active: str | None = None
        self._active_set: str | None = None
        self._spinner_index = 0
        self._last_spin_at = 0.0
        self._error: str | None = None
        self._lock = threading.Lock()
        self._worker: threading.Thread | None = None
        self._stop = threading.Event()

    def on_enter(self, ctx: PageContext) -> None:
        self._ctx = ctx
        self._stop_worker()
        self._error = None
        workdir = Path(ctx.repo.workdir) if ctx.repo.workdir else _worktree_path(ctx.repo_path)
        self._project_root = find_project_root(workdir)
        self._sets = []
        self._set_index = 0
        if self._project_root is not None:
            try:
                self._sets = load_action_sets(self._project_root)
            except (OSError, ValueError) as exc:
                self._error = str(exc)
        self._states = {
            (action_set.name, spec.name): _RunState.IDLE for action_set in self._sets for spec in action_set.specs
        }
        self._messages = {}
        self._active = None
        self._active_set = None
        self._spinner_index = 0
        self._list_state.select(0)

    def on_idle(self) -> None:
        with self._lock:
            running = self._active is not None
        if not running:
            return
        now = time.monotonic()
        if now - self._last_spin_at >= _SPINNER_INTERVAL_S:
            self._spinner_index = (self._spinner_index + 1) % len(_SPINNER)
            self._last_spin_at = now

    def status_text(self) -> str:
        if self._error:
            return self._error
        with self._lock:
            active = self._active
            active_set = self._active_set
            if active is not None:
                msg = self._messages.get((active_set or "", active), "")
                suffix = f" — {msg}" if msg else ""
                return f"Running {active}…{suffix}"
        if self._project_root is None:
            return "No pyproject.toml or actions.toml found — Esc back"
        if not self._sets:
            return "No actions configured — Esc back"
        move = move_hint()
        hint = f"Enter run — A run all — {move} — Esc back"
        if len(self._sets) > 1:
            set_keys = "H/L or Tab/Shift-Tab" if vim_enabled() else "←/→ or Tab/Shift-Tab"
            return f"{set_keys} sets — {hint}"
        return hint

    def draw(self, frame: Frame, area: Rect) -> None:
        if area.height <= 0:
            return
        theme = current_theme()
        multi = len(self._sets) > 1
        constraints = [Constraint.length(1), Constraint.fill(1)]
        if multi:
            constraints = [Constraint.length(1), Constraint.length(1), Constraint.fill(1)]
        chunks = Layout().direction(Direction.Vertical).constraints(constraints).split(area)
        root_label = str(self._project_root) if self._project_root is not None else "(none)"
        frame.render_widget(Paragraph.from_string(f"Actions ({root_label})").style(theme.header), chunks[0])
        list_area = chunks[1]
        if multi:
            labels = tuple(action_set.name for action_set in self._sets)
            render_tab_row(frame, chunks[1], "", labels, self._set_index)
            list_area = chunks[2]

        rows = self._rows()
        selected = self._list_state.selected or 0
        items: list[ListItem] = []
        for index, (key, label) in enumerate(rows):
            marker = self._marker_for(key)
            style = self._style_for(key, focused=index == selected)
            items.append(ListItem(f"{marker} {label}", style))
        if not items:
            items.append(ListItem("No actions configured", theme.log_date))
        frame.render_stateful_list(
            List(items).highlight_style(theme.focus).highlight_symbol(""),
            list_area,
            self._list_state,
        )

    def handle_key(self, key: KeyLike) -> PageResult:
        if is_quit(key) or is_esc(key):
            self._stop_worker()
            return PageResult(action=PageAction.POP)
        if self._error is not None:
            self._error = None
        with self._lock:
            busy = self._active is not None
        if busy:
            return PageResult()

        if is_backtab(key) or is_left(key):
            self._cycle_set(-1)
            return PageResult()
        if is_tab(key) or is_right(key):
            self._cycle_set(1)
            return PageResult()

        rows = self._rows()
        if not rows:
            return PageResult()

        if is_up(key):
            selected = self._list_state.selected or 0
            self._list_state.select(max(0, selected - 1))
            return PageResult()
        if is_down(key):
            selected = self._list_state.selected or 0
            self._list_state.select(min(len(rows) - 1, selected + 1))
            return PageResult()
        specs = self._current_specs()
        if key.code.casefold() == "a" and specs:
            self._start_run([spec.name for spec in specs], stop_on_failure=True)
            return PageResult()
        if is_enter(key):
            selected = self._list_state.selected or 0
            row_key, _ = rows[selected]
            if row_key == "__all__":
                self._start_run([spec.name for spec in specs], stop_on_failure=True)
            else:
                self._start_run([row_key], stop_on_failure=False)
            return PageResult()
        return PageResult()

    def _current_specs(self) -> list[ActionSpec]:
        if not self._sets:
            return []
        return list(self._sets[self._set_index].specs)

    def _current_set_name(self) -> str:
        if not self._sets:
            return ""
        return self._sets[self._set_index].name

    def _cycle_set(self, delta: int) -> None:
        count = len(self._sets)
        if count < 2:
            return
        self._set_index = (self._set_index + delta) % count
        self._list_state.select(0)

    def _rows(self) -> list[tuple[str, str]]:
        specs = self._current_specs()
        if not specs:
            return []
        set_name = self._current_set_name()
        rows: list[tuple[str, str]] = [("__all__", _RUN_ALL_LABEL)]
        for spec in specs:
            message = self._messages.get((set_name, spec.name), "")
            label = spec.name if not message else f"{spec.name} — {message}"
            rows.append((spec.name, label))
        return rows

    def _marker_for(self, key: str) -> str:
        set_name = self._current_set_name()
        with self._lock:
            if key == "__all__":
                if self._active is not None and self._active_set == set_name:
                    return _SPINNER[self._spinner_index]
                return "▶"
            state = self._states.get((set_name, key), _RunState.IDLE)
            if state is _RunState.RUNNING or (key == self._active and self._active_set == set_name):
                return _SPINNER[self._spinner_index]
            if state is _RunState.SUCCESS:
                return "✓"
            if state is _RunState.FAILURE:
                return "✗"
            return "·"

    def _style_for(self, key: str, *, focused: bool) -> Style:
        theme = current_theme()
        set_name = self._current_set_name()
        with self._lock:
            if key == "__all__":
                base = theme.header
            else:
                state = self._states.get((set_name, key), _RunState.IDLE)
                running = state is _RunState.RUNNING or (key == self._active and self._active_set == set_name)
                if state is _RunState.SUCCESS:
                    base = theme.staged
                elif state is _RunState.FAILURE:
                    base = theme.unstaged
                elif running:
                    base = theme.log_hash
                else:
                    base = theme.normal
        return focus_style(base) if focused else base

    def _start_run(self, names: list[str], *, stop_on_failure: bool) -> None:
        if not names or self._project_root is None:
            return
        set_name = self._current_set_name()
        specs = self._current_specs()
        self._stop_worker()
        with self._lock:
            for name in names:
                self._states[(set_name, name)] = _RunState.IDLE
                self._messages.pop((set_name, name), None)
            self._active = names[0]
            self._active_set = set_name
            self._states[(set_name, names[0])] = _RunState.RUNNING
            self._spinner_index = 0
            self._last_spin_at = time.monotonic()
        root = self._project_root
        self._stop.clear()
        self._worker = threading.Thread(
            target=self._run_worker,
            args=(root, specs, names, set_name, stop_on_failure),
            daemon=True,
        )
        self._worker.start()

    def _run_worker(
        self,
        root: Path,
        specs: list[ActionSpec],
        names: list[str],
        set_name: str,
        stop_on_failure: bool,
    ) -> None:
        runner = ProjectActions(root, specs)
        for name in names:
            if self._stop.is_set():
                break
            with self._lock:
                self._active = name
                self._active_set = set_name
                self._states[(set_name, name)] = _RunState.RUNNING
            ok, message = runner.action(name)
            with self._lock:
                self._states[(set_name, name)] = _RunState.SUCCESS if ok else _RunState.FAILURE
                self._messages[(set_name, name)] = message
            if not ok and stop_on_failure:
                break
        with self._lock:
            self._active = None
            self._active_set = None

    def _stop_worker(self) -> None:
        self._stop.set()
        worker = self._worker
        if worker is not None and worker.is_alive():
            worker.join(timeout=0.2)
        self._worker = None
        with self._lock:
            self._active = None
            self._active_set = None


def actions_page() -> ActionsPage:
    return ActionsPage()