"""Role-based access control + dual-auth policy for the AR Suite.

The brief originally specified three RBAC tiers (Operator / Technician /
Integrator). The product evolved in Sprint 2.5 to four roles with stricter
guarantees:

- ``SUPER_ADMIN`` -- the integrator (Álvaro). Unrestricted.
- ``ENGINEER`` -- an authorised technician of the integrator. May edit
  ESP32 firmware and flash boards, but flashing requires a Super Admin
  PIN as a second factor (2-of-N approval).
- ``OWNER`` -- the vessel owner. May manage Users on their own vessel and
  edit operational preferences. May NOT touch engineering parameters.
- ``USER`` -- a crew member. May operate the pilot (engage, acknowledge
  alarms, change setpoints) but cannot create users or change config.

Every gateable action is enumerated in :class:`Capability`. The matrix
``_CAPABILITIES_BY_ROLE`` is the **only** place where the role -> capability
mapping lives; everything else queries via :func:`has`.
"""

from __future__ import annotations

from enum import StrEnum


class Role(StrEnum):
    """Top-level role of an authenticated user."""

    SUPER_ADMIN = "super_admin"
    """The integrator (Álvaro). No restrictions."""

    ENGINEER = "engineer"
    """Authorised technician of the integrator. Flashing needs SA approval."""

    OWNER = "owner"
    """Vessel owner. Manages users on their vessel + operational preferences."""

    USER = "user"
    """Crew member. Operates the pilot, no config rights."""


class Capability(StrEnum):
    """Every action that requires a permission check.

    Adding an entry here is a deliberate, reviewed change -- the UI and
    every call site must opt into the new gate.
    """

    # ----- Code & firmware (integrator IP) ---------------------------------
    EDIT_PYTHON_PROJECT = "edit_python_project"
    """Edit the arautopilot package, tools, scripts. Super Admin only."""

    EDIT_FIRMWARE_SOURCE = "edit_firmware_source"
    """Edit ESP32 C++ sources under firmware/**. SA + Engineer."""

    FLASH_FIRMWARE = "flash_firmware"
    """Flash a .bin to an ESP32. SA direct; Engineer with SA PIN."""

    BUILD_FIRMWARE = "build_firmware"
    """Compile firmware locally via pio. SA + Engineer."""

    # ----- Tuning ----------------------------------------------------------
    EDIT_BASE_GAINS = "edit_base_gains"
    """Edit the integrator's base PID gains (IP). Super Admin only."""

    EDIT_COMMISSIONING = "edit_commissioning"
    """Edit field-commissioning parameters (rudder limits, calibration).
    SA + Engineer."""

    EDIT_OPERATIONAL = "edit_operational"
    """Edit operational preferences (favourite headings, alarm volume,
    profile Soft/Normal/Sport). SA + Engineer + Owner."""

    # ----- User management -------------------------------------------------
    MANAGE_USERS = "manage_users"
    """Create / delete / edit Users on this vessel. SA + Owner."""

    # ----- Runtime operation ----------------------------------------------
    ENGAGE_PILOT = "engage_pilot"
    """Engage or disengage the autopilot. All roles."""

    READ_TELEMETRY = "read_telemetry"
    """Read live telemetry from the firmware. All roles."""

    ACK_ALARMS = "ack_alarms"
    """Acknowledge active alarms. All roles."""

    # ----- Audit -----------------------------------------------------------
    VIEW_AUDIT_LOG_FULL = "view_audit_log_full"
    """Read the full immutable audit log (all vessels). SA + Engineer."""

    VIEW_AUDIT_LOG_VESSEL = "view_audit_log_vessel"
    """Read the audit log for this vessel only. SA + Engineer + Owner."""


# The single source of truth for who can do what. Order inside each set
# is irrelevant; we use frozenset so accidental mutation is impossible.
_CAPABILITIES_BY_ROLE: dict[Role, frozenset[Capability]] = {
    Role.SUPER_ADMIN: frozenset(Capability),  # everything
    Role.ENGINEER: frozenset(
        {
            Capability.EDIT_FIRMWARE_SOURCE,
            Capability.FLASH_FIRMWARE,
            Capability.BUILD_FIRMWARE,
            Capability.EDIT_COMMISSIONING,
            Capability.EDIT_OPERATIONAL,
            Capability.ENGAGE_PILOT,
            Capability.READ_TELEMETRY,
            Capability.ACK_ALARMS,
            Capability.VIEW_AUDIT_LOG_FULL,
            Capability.VIEW_AUDIT_LOG_VESSEL,
        }
    ),
    Role.OWNER: frozenset(
        {
            Capability.EDIT_OPERATIONAL,
            Capability.MANAGE_USERS,
            Capability.ENGAGE_PILOT,
            Capability.READ_TELEMETRY,
            Capability.ACK_ALARMS,
            Capability.VIEW_AUDIT_LOG_VESSEL,
        }
    ),
    Role.USER: frozenset(
        {
            Capability.ENGAGE_PILOT,
            Capability.READ_TELEMETRY,
            Capability.ACK_ALARMS,
        }
    ),
}


def has(role: Role, capability: Capability) -> bool:
    """Return True iff ``role`` has ``capability``."""
    return capability in _CAPABILITIES_BY_ROLE[role]


def capabilities_of(role: Role) -> frozenset[Capability]:
    """Return the full capability set granted to ``role``."""
    return _CAPABILITIES_BY_ROLE[role]


def requires_dual_auth(actor_role: Role, capability: Capability) -> bool:
    """Return True if the given (actor, capability) pair requires a Super
    Admin approval in addition to the actor's own credentials.

    Sprint 2.5 policy: an ``ENGINEER`` flashing firmware needs the Super
    Admin's PIN as a second factor. Everything else is single-factor.

    Reasoning: flashing the wrong firmware to a customer board is a
    high-impact action with real-world consequences (rudder mis-driven,
    safety interlocks bypassed). The Super Admin retains a checkpoint.
    """
    if actor_role == Role.ENGINEER and capability == Capability.FLASH_FIRMWARE:
        return True
    return False


class PermissionError(Exception):
    """Raised when a capability check fails or a dual-auth second factor is missing."""


def require(role: Role, capability: Capability) -> None:
    """Raise :class:`PermissionError` if ``role`` lacks ``capability``.

    Use this at the entry point of every gated function. Combine with the
    audit log to record both grants and denials.
    """
    if not has(role, capability):
        raise PermissionError(
            f"role {role.value!r} does not have capability {capability.value!r}"
        )
