"""Kill switch coordinator (``docs/07-risk-controls.md``).

Encapsulates the three side effects required when arming/disarming:

1. Update the ``system_state`` row in SQLite.
2. Append a tamper-evident line to the audit log.
3. Make the new state observable via :meth:`is_armed` for the
   orchestrator.

The orchestrator is the only caller; the GUI signals the same intent
via the :class:`ManualAction` queue, which the orchestrator drains and
forwards here.
"""

from __future__ import annotations

import sqlite3
from collections.abc import Callable
from datetime import UTC, datetime

from cerbero_bite.safety.audit_log import AuditLog
from cerbero_bite.state import Repository, transaction

__all__ = ["KillSwitch", "KillSwitchError"]


class KillSwitchError(RuntimeError):
    """Raised when an arm/disarm transition is invalid."""


class KillSwitch:
    """Arm/disarm + status helper backed by ``system_state`` + audit log.

    All transitions must go through this class so the SQLite row and
    the audit chain stay in lock-step.
    """

    def __init__(
        self,
        *,
        connection_factory: Callable[[], sqlite3.Connection],
        repository: Repository,
        audit_log: AuditLog,
        clock: Callable[[], datetime] | None = None,
    ) -> None:
        self._connect = connection_factory
        self._repo = repository
        self._audit = audit_log
        self._clock = clock or (lambda: datetime.now(UTC))

    # ------------------------------------------------------------------
    # Status
    # ------------------------------------------------------------------

    def is_armed(self) -> bool:
        conn = self._connect()
        try:
            state = self._repo.get_system_state(conn)
        finally:
            conn.close()
        if state is None:
            return False
        return state.kill_switch == 1

    # ------------------------------------------------------------------
    # Transitions
    # ------------------------------------------------------------------

    def arm(self, *, reason: str, source: str) -> None:
        """Move the engine to the armed state.

        ``source`` is a short label (``"manual"``, ``"mcp_timeout"``,
        ``"hash_chain"``) that goes both into the audit payload and the
        log so we can attribute the trigger later.
        """
        if not reason:
            raise KillSwitchError("reason is required to arm the kill switch")
        now = self._clock()
        conn = self._connect()
        try:
            with transaction(conn):
                state = self._repo.get_system_state(conn)
                if state is None:
                    raise KillSwitchError(
                        "system_state singleton missing — call init_system_state first"
                    )
                if state.kill_switch == 1:
                    return  # idempotent
                self._repo.set_kill_switch(conn, armed=True, reason=reason, now=now)
        finally:
            conn.close()
        self._audit.append(
            event="KILL_SWITCH_ARMED",
            payload={"reason": reason, "source": source},
            now=now,
        )

    def disarm(self, *, reason: str, source: str) -> None:
        """Move the engine back to the disarmed state."""
        if not reason:
            raise KillSwitchError("reason is required to disarm the kill switch")
        now = self._clock()
        conn = self._connect()
        try:
            with transaction(conn):
                state = self._repo.get_system_state(conn)
                if state is None:
                    raise KillSwitchError(
                        "system_state singleton missing — call init_system_state first"
                    )
                if state.kill_switch == 0:
                    return  # idempotent
                self._repo.set_kill_switch(conn, armed=False, reason=None, now=now)
        finally:
            conn.close()
        self._audit.append(
            event="KILL_SWITCH_DISARMED",
            payload={"reason": reason, "source": source},
            now=now,
        )
