"""Project actions page: run scripts defined in ``pyproject.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 ActionSpec, ProjectActions, find_project_root, load_action_specs
from pygittools.tui.git_colors import current_theme, focus_style
from pygittools.tui.keys import KeyLike, is_down, is_enter, is_esc, is_quit, is_up
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._specs: list[ActionSpec] = []
		self._list_state = ListState()
		self._states: dict[str, _RunState] = {}
		self._messages: dict[str, str] = {}
		self._active: 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()
		workdir = Path(ctx.repo.workdir) if ctx.repo.workdir else _worktree_path(ctx.repo_path)
		self._project_root = find_project_root(workdir)
		self._specs = load_action_specs(self._project_root) if self._project_root is not None else []
		self._states = {spec.name: _RunState.IDLE for spec in self._specs}
		self._messages = {}
		self._active = None
		self._spinner_index = 0
		self._error = None
		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
			if active is not None:
				msg = self._messages.get(active, "")
				suffix = f" — {msg}" if msg else ""
				return f"Running {active}…{suffix}"
		if self._project_root is None:
			return "No pyproject.toml found — Esc back"
		if not self._specs:
			return "No [tool.pgt].actions configured — Esc back"
		return "Enter run — A run all — j/k move — Esc back"

	def draw(self, frame: Frame, area: Rect) -> None:
		if area.height <= 0:
			return
		theme = current_theme()
		chunks = (
			Layout().direction(Direction.Vertical).constraints([Constraint.length(1), Constraint.fill(1)]).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])

		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(""),
			chunks[1],
			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()

		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()
		if key.code.casefold() == "a" and self._specs:
			self._start_run([spec.name for spec in self._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 self._specs], stop_on_failure=True)
			else:
				self._start_run([row_key], stop_on_failure=False)
			return PageResult()
		return PageResult()

	def _rows(self) -> list[tuple[str, str]]:
		if not self._specs:
			return []
		rows: list[tuple[str, str]] = [("__all__", _RUN_ALL_LABEL)]
		for spec in self._specs:
			message = self._messages.get(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:
		with self._lock:
			if key == "__all__":
				if self._active is not None:
					return _SPINNER[self._spinner_index]
				return "▶"
			state = self._states.get(key, _RunState.IDLE)
			if state is _RunState.RUNNING or key == self._active:
				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()
		with self._lock:
			if key == "__all__":
				base = theme.header
			else:
				state = self._states.get(key, _RunState.IDLE)
				if state is _RunState.SUCCESS:
					base = theme.staged
				elif state is _RunState.FAILURE:
					base = theme.unstaged
				elif state is _RunState.RUNNING or key == self._active:
					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
		self._stop_worker()
		with self._lock:
			for name in names:
				self._states[name] = _RunState.IDLE
				self._messages.pop(name, None)
			self._active = names[0]
			self._states[names[0]] = _RunState.RUNNING
			self._spinner_index = 0
			self._last_spin_at = time.monotonic()
		root = self._project_root
		specs = list(self._specs)
		self._stop.clear()
		self._worker = threading.Thread(
			target=self._run_worker,
			args=(root, specs, names, stop_on_failure),
			daemon=True,
		)
		self._worker.start()

	def _run_worker(
		self,
		root: Path,
		specs: list[ActionSpec],
		names: list[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._states[name] = _RunState.RUNNING
			ok, message = runner.action(name)
			with self._lock:
				self._states[name] = _RunState.SUCCESS if ok else _RunState.FAILURE
				self._messages[name] = message
			if not ok and stop_on_failure:
				break
		with self._lock:
			self._active = 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


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