from __future__ import annotations

from dataclasses import dataclass
from enum import Enum, auto
from time import monotonic
from typing import Callable

from .config import AppConfig, TitleConfig


class TitleState(Enum):
    IDLE = auto()
    STANDBY = auto()  # step 1: active border shown, waiting for confirm press
    COOLDOWN = auto()  # step 2: timer running


@dataclass
class TitleRuntime:
    config: TitleConfig
    state: TitleState = TitleState.IDLE
    cooldown_end: float | None = None
    standby_since: float | None = None

    def remaining_fraction(self, now: float) -> float:
        """1.0 = just started, 0.0 = about to expire. 0 outside COOLDOWN."""
        if self.state != TitleState.COOLDOWN or self.cooldown_end is None:
            return 0.0
        total = self.config.cooldown_seconds
        if total <= 0:
            return 0.0
        remaining = self.cooldown_end - now
        return max(0.0, min(1.0, remaining / total))

    def remaining_seconds(self, now: float) -> float:
        if self.state != TitleState.COOLDOWN or self.cooldown_end is None:
            return 0.0
        return max(0.0, self.cooldown_end - now)

    def standby_remaining_seconds(self, now: float) -> float | None:
        """Seconds left to press step 2 before STANDBY auto-reverts, or
        None if this title is either not in STANDBY or has no timeout
        configured (stays in STANDBY indefinitely)."""
        timeout = self.config.standby_timeout_seconds
        if self.state != TitleState.STANDBY or self.standby_since is None or timeout is None:
            return None
        return max(0.0, timeout - (now - self.standby_since))


class TitleTimerEngine:
    """Title switching with an optional pre-activation step and Dogma mutex.

    A title with no pre_hotkey configured: the main hotkey starts the
    cooldown directly on a single press.

    A title *with* a pre_hotkey configured: that combo is required for step
    1 (IDLE -> STANDBY, shown as a pulsing border) - the main hotkey is
    step-2-only then and does nothing while IDLE, so there's no way to
    shortcut around the pre-activation combo by pressing the main hotkey
    twice. The optional standby_timeout_seconds only applies to this
    pre-activation flow, auto-reverting a forgotten STANDBY back to IDLE.

    Dogma mode: while any title is COOLDOWN, hotkeys for *other* titles are
    ignored outright, so a running timer can't be interfered with.
    """

    def __init__(self, config: AppConfig, on_change: Callable[[], None]):
        self.config = config
        self.on_change = on_change
        self.runtimes: dict[str, TitleRuntime] = {
            t.id: TitleRuntime(config=t) for t in config.titles
        }
        self._by_hotkey: dict[frozenset[str], str] = {
            frozenset(t.hotkey): t.id for t in config.titles
        }
        self._by_pre_hotkey: dict[frozenset[str], str] = {
            frozenset(t.pre_hotkey): t.id for t in config.titles if t.pre_hotkey
        }

    def handle_hotkey(self, held: frozenset[str], pressed_key: str) -> None:
        """`held` is every key currently down (e.g. a movement key the
        player is holding plus the key they just pressed); `pressed_key` is
        specifically the one that just went down.

        A configured hotkey fires when its own keys are all a *subset* of
        `held` - not an exact match - so a hotkey still fires while an
        unrelated key (movement, usually) is also held. It only fires on
        the keydown that actually completes it (`pressed_key` must be one
        of its keys), so already-held extra keys don't cause spurious
        re-fires when some other, unrelated key is pressed afterwards. If
        more than one configured combo would match, the most specific one
        (most keys) wins - e.g. Ctrl+C beats a plain C bound elsewhere.
        """
        match = self._best_match(self._by_hotkey, held, pressed_key)
        if match is not None:
            self._advance(match)
            return

        pre_match = self._best_match(self._by_pre_hotkey, held, pressed_key)
        if pre_match is not None:
            self._enter_standby(pre_match)

    @staticmethod
    def _best_match(
        table: dict[frozenset[str], str], held: frozenset[str], pressed_key: str
    ) -> str | None:
        candidates = [
            (combo, title_id)
            for combo, title_id in table.items()
            if pressed_key in combo and combo <= held
        ]
        if not candidates:
            return None
        return max(candidates, key=lambda c: len(c[0]))[1]

    def _advance(self, title_id: str) -> None:
        rt = self.runtimes[title_id]
        if rt.state is TitleState.IDLE:
            if rt.config.pre_hotkey:
                # A pre-activation combo is configured for this title, so
                # the main hotkey is step-2-only - it must never itself
                # trigger step 1, or the main hotkey pressed twice in a row
                # would work as an (unwanted) shortcut around the
                # pre-activation combo.
                return
            # No pre-activation configured: a single press starts the
            # cooldown directly, no standby step in between.
            self._start_cooldown(title_id)
        elif rt.state is TitleState.STANDBY:
            self._start_cooldown(title_id)
        elif rt.config.restart_on_repeat:
            self._start_cooldown(title_id)  # COOLDOWN, but re-triggering is allowed
        # else: COOLDOWN and re-triggering not enabled - ignore the press

    def _start_cooldown(self, title_id: str) -> None:
        rt = self.runtimes[title_id]
        if self.config.dogma_mode and self._any_cooldown_except(title_id):
            return
        rt.state = TitleState.COOLDOWN
        rt.cooldown_end = monotonic() + rt.config.cooldown_seconds
        rt.standby_since = None
        self.on_change()

    def _enter_standby(self, title_id: str) -> None:
        rt = self.runtimes[title_id]
        if rt.state is not TitleState.IDLE:
            return  # pre_hotkey only ever does step 1, never step 2
        if self.config.dogma_mode and self._any_cooldown_except(title_id):
            return
        self._clear_other_standbys(keep=title_id)
        rt.state = TitleState.STANDBY
        rt.standby_since = monotonic()
        self.on_change()

    def tick(self) -> None:
        now = monotonic()
        changed = False
        for rt in self.runtimes.values():
            if rt.state is TitleState.COOLDOWN and rt.cooldown_end is not None and now >= rt.cooldown_end:
                rt.state = TitleState.IDLE
                rt.cooldown_end = None
                changed = True
            elif rt.state is TitleState.STANDBY and rt.config.standby_timeout_seconds is not None:
                if rt.standby_since is not None and now - rt.standby_since >= rt.config.standby_timeout_seconds:
                    rt.state = TitleState.IDLE
                    rt.standby_since = None
                    changed = True
        if changed:
            self.on_change()

    def snapshot(self) -> list[TitleRuntime]:
        return list(self.runtimes.values())

    def _any_cooldown_except(self, title_id: str) -> bool:
        return any(
            rt.state is TitleState.COOLDOWN
            for tid, rt in self.runtimes.items()
            if tid != title_id
        )

    def _clear_other_standbys(self, keep: str) -> None:
        for tid, rt in self.runtimes.items():
            if tid != keep and rt.state is TitleState.STANDBY:
                rt.state = TitleState.IDLE
                rt.standby_since = None
