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