"""Central Interceptor class and @intercept decorator."""

from __future__ import annotations

import functools
from pathlib import Path
from typing import Any, Callable

from interceptor.approval import OverrideToken, RoleApproval
from interceptor.logging import InterceptorLogger
from interceptor.policy import PolicyEngine
from interceptor.risk import RiskClassifier
from interceptor.throttle import RetryThrottle
from interceptor.types import Decision, DecisionVerdict, Mode, RiskLevel


class Interceptor:
    """Runtime enforcement guard for AI agent tool calls.

    Parameters
    ----------
    mode:
        Enforcement mode — ``"strict"``, ``"balanced"``, or ``"observe"``.
    policy_path:
        Optional path to a YAML policy file.
    retry_limit:
        Max repeated calls to the same tool within the window before throttling.
    retry_window:
        Sliding window in seconds for retry throttling.
    confirmation_callback:
        Optional callable ``(Decision) -> bool`` invoked when confirmation is needed.
        Return ``True`` to approve, ``False`` to block.
    override_token:
        Optional pre-validated override token string for the *current* call.
    token_ttl:
        Lifetime in seconds for generated override tokens.
    """

    def __init__(
        self,
        mode: str | Mode = Mode.STRICT,
        policy_path: str | Path | None = None,
        retry_limit: int = 5,
        retry_window: float = 60.0,
        confirmation_callback: Callable[[Decision], bool] | None = None,
        override_token: str | None = None,
        token_ttl: float = 300.0,
    ) -> None:
        self.mode = Mode(mode) if isinstance(mode, str) else mode

        # Sub-systems
        self._policy = PolicyEngine()
        if policy_path is not None:
            self._policy.load(policy_path)

        self._classifier = RiskClassifier(
            extra_keywords=self._policy.extra_keywords,
            tool_overrides=self._policy.tool_overrides,
        )
        self._approval = RoleApproval()
        self._throttle = RetryThrottle(
            max_calls=retry_limit,
            window_seconds=retry_window,
        )
        self._tokens = OverrideToken(ttl_seconds=token_ttl)
        self._logger = InterceptorLogger()
        self._confirmation_callback = confirmation_callback
        self._override_token = override_token

    # -- public API -----------------------------------------------------------

    def generate_override_token(self) -> str:
        """Create a single-use override token."""
        return self._tokens.generate()

    def run(
        self,
        tool_name: str,
        args: dict[str, Any],
        user_role: str = "user",
        override_token: str | None = None,
    ) -> Decision:
        """Evaluate a tool call and return a structured :class:`Decision`.

        Parameters
        ----------
        tool_name:
            Name of the tool being invoked.
        args:
            Arguments for the tool call.
        user_role:
            Role of the calling user (``"user"``, ``"admin"``, ``"system"``).
        override_token:
            Optional one-time override token to bypass restrictions.
        """
        effective_token = override_token or self._override_token

        # 1. Intent summary
        intent = _build_intent(tool_name, args)

        # 2. Risk classification
        risk = self._classifier.classify(tool_name, args)

        # 3. Throttle check — escalate risk if throttled
        self._throttle.record(tool_name)
        if self._throttle.is_throttled(tool_name):
            risk = RiskLevel.HIGH

        # 4. Build human-readable explanation (MEDIUM/HIGH only)
        explanation = _build_explanation(tool_name, args, risk)

        # 5. Apply mode rules
        decision = self._apply_mode(risk, user_role, effective_token, intent, explanation)

        # 6. Print to console
        self._logger.print_decision(decision)

        return decision

    # -- private --------------------------------------------------------------

    def _apply_mode(
        self,
        risk: RiskLevel,
        user_role: str,
        override_token: str | None,
        intent: str,
        explanation: str | None,
    ) -> Decision:
        """Produce a :class:`Decision` based on mode, risk, role, and token."""

        # --- OBSERVE mode: never block ----------------------------------------
        if self.mode == Mode.OBSERVE:
            return Decision(
                allowed=True,
                risk_level=risk,
                mode=self.mode,
                decision=DecisionVerdict.ALLOWED,
                intent_summary=intent,
                reason="Observe mode — logging only.",
                explanation=explanation,
            )

        # --- Override token check (any mode) ----------------------------------
        if override_token is not None:
            try:
                self._tokens.validate_and_consume(override_token)
                return Decision(
                    allowed=True,
                    risk_level=risk,
                    mode=self.mode,
                    decision=DecisionVerdict.ALLOWED,
                    intent_summary=intent,
                    reason="Override token accepted.",
                    explanation=explanation,
                )
            except Exception:
                pass  # fall through to normal rules

        # --- STRICT mode -------------------------------------------------------
        if self.mode == Mode.STRICT:
            return self._strict(risk, user_role, intent, explanation)

        # --- BALANCED mode -----------------------------------------------------
        return self._balanced(risk, user_role, intent, explanation)

    def _strict(self, risk: RiskLevel, user_role: str, intent: str, explanation: str | None) -> Decision:
        if risk == RiskLevel.HIGH:
            if RoleApproval.is_authorized(user_role, risk):
                return Decision(
                    allowed=True,
                    risk_level=risk,
                    mode=self.mode,
                    decision=DecisionVerdict.ALLOWED,
                    intent_summary=intent,
                    reason="Admin role approved HIGH risk action.",
                    explanation=explanation,
                )
            return Decision(
                allowed=False,
                risk_level=risk,
                mode=self.mode,
                decision=DecisionVerdict.BLOCKED,
                intent_summary=intent,
                reason="HIGH risk action requires admin approval.",
                explanation=explanation,
            )
        if risk == RiskLevel.MEDIUM:
            return self._attempt_confirmation(risk, intent, "MEDIUM risk action requires confirmation.", explanation)

        return Decision(
            allowed=True,
            risk_level=risk,
            mode=self.mode,
            decision=DecisionVerdict.ALLOWED,
            intent_summary=intent,
            reason="LOW risk — allowed.",
        )

    def _balanced(self, risk: RiskLevel, user_role: str, intent: str, explanation: str | None) -> Decision:
        if risk == RiskLevel.HIGH:
            return self._attempt_confirmation(risk, intent, "HIGH risk action requires confirmation.", explanation)
        if risk == RiskLevel.MEDIUM:
            return Decision(
                allowed=True,
                risk_level=risk,
                mode=self.mode,
                decision=DecisionVerdict.ALLOWED,
                intent_summary=intent,
                reason="MEDIUM risk — allowed with warning.",
                explanation=explanation,
            )
        return Decision(
            allowed=True,
            risk_level=risk,
            mode=self.mode,
            decision=DecisionVerdict.ALLOWED,
            intent_summary=intent,
            reason="LOW risk — allowed.",
        )

    def _attempt_confirmation(
        self, risk: RiskLevel, intent: str, reason: str, explanation: str | None = None
    ) -> Decision:
        if self._confirmation_callback is not None:
            preliminary = Decision(
                allowed=False,
                risk_level=risk,
                mode=self.mode,
                decision=DecisionVerdict.CONFIRMATION_REQUIRED,
                intent_summary=intent,
                reason=reason,
                explanation=explanation,
            )
            if self._confirmation_callback(preliminary):
                return Decision(
                    allowed=True,
                    risk_level=risk,
                    mode=self.mode,
                    decision=DecisionVerdict.ALLOWED,
                    intent_summary=intent,
                    reason="Confirmed by callback.",
                    explanation=explanation,
                )
        return Decision(
            allowed=False,
            risk_level=risk,
            mode=self.mode,
            decision=DecisionVerdict.CONFIRMATION_REQUIRED,
            intent_summary=intent,
            reason=reason,
            explanation=explanation,
        )


# ---------------------------------------------------------------------------
# Decorator
# ---------------------------------------------------------------------------

def intercept(guard: Interceptor, user_role: str = "user"):
    """Decorator that intercepts a function call through *guard*.

    Usage::

        @intercept(guard)
        def delete_file(path: str) -> None:
            ...

    The decorated function's first positional argument is treated as
    ``tool_name`` (the function name), and keyword arguments as ``args``.
    """

    def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
        @functools.wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            decision = guard.run(fn.__name__, kwargs, user_role=user_role)
            if not decision.allowed:
                return decision
            result = fn(*args, **kwargs)
            return result

        return wrapper

    return decorator


# ---------------------------------------------------------------------------
# Intent builder
# ---------------------------------------------------------------------------

def _build_intent(tool_name: str, args: dict[str, Any]) -> str:
    """Generate a one-line intent summary from tool name and arguments."""
    parts: list[str] = [f"Agent intends to call {tool_name}"]

    if not args:
        return parts[0]

    # Summarise key arguments
    details: list[str] = []
    for key, value in args.items():
        if isinstance(value, list):
            details.append(f"{len(value)} {key}")
        elif isinstance(value, str) and len(value) < 120:
            details.append(f"{key}={value}")
        else:
            details.append(key)

    if details:
        parts.append("with " + ", ".join(details[:4]))

    return " ".join(parts)


# ---------------------------------------------------------------------------
# Explanation builder
# ---------------------------------------------------------------------------

_EXPLANATION_VERBS: dict[str, str] = {
    "delete": "permanently delete",
    "remove": "remove",
    "drop": "drop",
    "overwrite": "overwrite",
    "truncate": "truncate",
    "rewrite": "rewrite",
    "write": "write to",
    "send": "send",
    "execute": "execute",
    "run": "run",
    "install": "install",
    "create": "create",
}


def _build_explanation(
    tool_name: str, args: dict[str, Any], risk: RiskLevel
) -> str | None:
    """Build a plain-English explanation of the pending action.

    Returns ``None`` for LOW risk actions to keep output concise.
    """
    if risk == RiskLevel.LOW:
        return None

    name_lower = tool_name.lower()

    # Pick the most relevant verb from the tool name
    verb = "perform"
    for keyword, friendly_verb in _EXPLANATION_VERBS.items():
        if keyword in name_lower:
            verb = friendly_verb
            break

    # Extract a target from common arg names
    target = None
    for key in ("path", "file", "filename", "target", "url", "name", "destination"):
        if key in args:
            target = str(args[key])
            break

    # Count list-type args for richer context
    list_counts: list[str] = []
    for key, value in args.items():
        if isinstance(value, list):
            list_counts.append(f"{len(value)} {key}")

    # Assemble
    if target:
        summary = f"Heads up — this will {verb} {target}."
    elif list_counts:
        summary = f"Heads up — this will {verb} ({', '.join(list_counts)})."
    else:
        summary = f"Heads up — this will {verb} via {tool_name}."

    if risk == RiskLevel.HIGH:
        summary += " This is a destructive action that can't be easily undone."

    return summary

