"""Action middleware: the single gate every agent action passes through.

No action in the system executes without going through enforce().
This connects the contract enforcer to every service.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from agntspace.security.contract import ContractEnforcer

log = logging.getLogger(__name__)


class ActionDenied(Exception):
    """Raised when the contract blocks an action."""

    def __init__(self, action: str, reason: str) -> None:
        self.action = action
        self.reason = reason
        super().__init__(f"Contract denied: {action} — {reason}")


class ConfirmationRequired(Exception):
    """Raised when the contract requires user confirmation."""

    def __init__(self, action: str, reason: str) -> None:
        self.action = action
        self.reason = reason
        super().__init__(f"Confirmation needed: {action} — {reason}")


class AgentFrozen(Exception):
    """Raised when emergency stop is active."""

    def __init__(self) -> None:
        super().__init__("Agent is frozen — emergency stop active")


class ContractBypassError(Exception):
    """Raised when `VaultWriter` detects a write that isn't backed by a
    valid contract decision in the current correlation scope.

    This is the teeth behind Rule #3 (Contract is the law). A write
    raises this error when:

      - The call site is inside a correlation scope (an agent tool call,
        an API request, a subagent dispatch, a workflow step), AND
      - The write did not declare a `contract_action`, OR
      - The declared `contract_action` was never authorized by the
        `ContractEnforcer` in this scope.

    The error message names the offending path, the declared action,
    and the set of actions that WERE authorized, so the caller can
    diagnose the bypass quickly. High-importance `security_violation`
    events are emitted to `ActivityLogger` when this error fires, so
    the user sees it in the next agent turn's relationship context.

    Internal callers (init, migrations, daily cycle, memory consolidation,
    etc.) bypass the guard by passing `trust_reason="..."` to the writer,
    which records the reason in the activity log for audit.
    """

    def __init__(
        self,
        path: str,
        attempted_action: str | None,
        authorized: frozenset[str] | set[str] | None,
        correlation_id: str,
    ) -> None:
        self.path = path
        self.attempted_action = attempted_action
        self.authorized = frozenset(authorized or frozenset())
        self.correlation_id = correlation_id
        if attempted_action is None:
            detail = (
                f"no contract_action declared and no trust_reason passed; "
                f"scope={correlation_id!r} has authorized={sorted(self.authorized)}"
            )
        else:
            detail = (
                f"contract_action={attempted_action!r} not in authorized set "
                f"{sorted(self.authorized)} for scope={correlation_id!r}"
            )
        super().__init__(
            f"Contract bypass detected on vault write to {path!r}: {detail}"
        )


# Global frozen flag — set by emergency stop endpoint
_frozen = False


def set_frozen(value: bool) -> None:
    """Set the global frozen state."""
    global _frozen  # noqa: PLW0603
    _frozen = value


def is_frozen() -> bool:
    """Check if agent is frozen."""
    return _frozen


def enforce(
    contract: ContractEnforcer,
    action: str,
    integration: str | None = None,
    skip_confirmation: bool = False,
) -> None:
    """Enforce the contract for an action. Raises if denied.

    This is the ONE function every operation must call.

    Args:
        contract: The contract enforcer instance
        action: Action identifier (e.g., "send_email", "read_inbox")
        integration: Optional integration name for overrides
        skip_confirmation: If True, confirmation-required actions proceed
                          (use when user has already confirmed)

    Raises:
        AgentFrozen: If emergency stop is active
        ActionDenied: If the contract blocks this action
        ConfirmationRequired: If the action needs user confirmation
    """
    if _frozen:
        raise AgentFrozen

    result = contract.check(action, integration)

    if not result.allowed:
        log.warning("Contract DENIED: %s (integration=%s) — %s", action, integration, result.reason)
        raise ActionDenied(action, result.reason)

    if result.requires_confirmation and not skip_confirmation:
        log.info("Contract CONFIRM needed: %s (integration=%s)", action, integration)
        raise ConfirmationRequired(action, result.reason)

    log.debug("Contract OK: %s (integration=%s)", action, integration)
