"""Runtime capability enforcement via a PEP-578 audit hook.

Given the set of capabilities a plugin declared (see :mod:`mixlar.permissions`),
this installs a ``sys.addaudithook`` that **blocks** sensitive operations the
plugin did *not* declare — turning the manifest ``permissions`` list into an
enforced least-privilege contract rather than a promise.

Two modes:

  * **Global** (the sandbox child process runs *only* the plugin) — every
    audited event is attributed to the plugin. Use :func:`install`.
  * **Attributed** (in-process in the app, where non-plugin code must not be
    blocked) — pass an ``origin`` predicate that returns ``True`` only when the
    offending call comes from the plugin's own code (e.g. its module is on the
    stack). The app's ``plugin_guard`` uses this shape.

An audit hook is process-wide and cannot be uninstalled, so the app installs
exactly one shared hook and consults per-plugin state; the sandbox installs one
per child. This is a real boundary for pure-Python plugins, but not absolute:
native code can bypass audit hooks, which is why ``native`` is itself a gated,
high-privilege capability and why signing + review still matter.
"""
from __future__ import annotations

import sys
from typing import Callable, Iterable, Optional, Set

from . import permissions as _perms


class CapabilityViolation(RuntimeError):
    """Raised when a plugin uses a capability it did not declare."""

    def __init__(self, capability: str, event: str, detail: str = ""):
        self.capability = capability
        self.event = event
        msg = (f"blocked: this plugin used '{capability}' but did not declare it "
               f"in its permissions (event: {event})")
        if detail:
            msg += f" [{detail}]"
        super().__init__(msg)


# Audit event name -> capability it implies. Prefix match on the event name.
_EVENT_CAP = {
    "subprocess.Popen": _perms.CAP_SUBPROCESS,
    "os.system": _perms.CAP_SUBPROCESS,
    "os.exec": _perms.CAP_SUBPROCESS,
    "os.spawn": _perms.CAP_SUBPROCESS,
    "os.posix_spawn": _perms.CAP_SUBPROCESS,
    "os.fork": _perms.CAP_SUBPROCESS,
    "socket.connect": _perms.CAP_NETWORK,
    "socket.bind": _perms.CAP_NETWORK,
    "socket.getaddrinfo": _perms.CAP_NETWORK,
    "urllib.Request": _perms.CAP_NETWORK,
    "ctypes.dlopen": _perms.CAP_NATIVE,
    "ctypes.dlsym": _perms.CAP_NATIVE,
    "ctypes.call_function": _perms.CAP_NATIVE,
    "ctypes.set_errno": _perms.CAP_NATIVE,
    "ctypes.get_errno": _perms.CAP_NATIVE,
    "winreg.": _perms.CAP_NATIVE,
}

# Modules whose import implies a capability (gates `import <x>`).
_IMPORT_CAP = {
    "subprocess": _perms.CAP_SUBPROCESS,
    "ctypes": _perms.CAP_NATIVE, "winreg": _perms.CAP_NATIVE,
    "comtypes": _perms.CAP_NATIVE, "pycaw": _perms.CAP_NATIVE,
    "win32api": _perms.CAP_NATIVE, "win32gui": _perms.CAP_NATIVE,
    "win32process": _perms.CAP_NATIVE, "win32com": _perms.CAP_NATIVE,
    "socket": _perms.CAP_NETWORK, "urllib": _perms.CAP_NETWORK,
    "http": _perms.CAP_NETWORK, "requests": _perms.CAP_NETWORK,
    "websocket": _perms.CAP_NETWORK, "websockets": _perms.CAP_NETWORK,
    "aiohttp": _perms.CAP_NETWORK, "ftplib": _perms.CAP_NETWORK,
    "httpx": _perms.CAP_NETWORK, "urllib3": _perms.CAP_NETWORK,
    "curl_cffi": _perms.CAP_NETWORK, "grpc": _perms.CAP_NETWORK,
    "paho": _perms.CAP_NETWORK, "yfinance": _perms.CAP_NETWORK,
    "openrgb": _perms.CAP_NETWORK, "pymodbus": _perms.CAP_NETWORK,
    "zeroconf": _perms.CAP_NETWORK,
    "pynput": _perms.CAP_INPUT, "keyboard": _perms.CAP_INPUT,
    "mouse": _perms.CAP_INPUT, "pyautogui": _perms.CAP_INPUT,
}


def _event_capability(event: str, args) -> Optional[str]:
    """Map an audit (event, args) to the capability it requires, or None."""
    if event == "import":
        module = (args[0] if args else "") or ""
        top = str(module).split(".")[0]
        return _IMPORT_CAP.get(top)
    if event == "open":
        # args = (path, mode, flags); writing outside is a filesystem cap.
        mode = args[1] if len(args) > 1 else "r"
        if isinstance(mode, str) and any(c in mode for c in ("w", "a", "x", "+")):
            return _perms.CAP_FS
        return None
    for prefix, cap in _EVENT_CAP.items():
        if event == prefix or event.startswith(prefix):
            return cap
    return None


def make_hook(allowed: Set[str],
              origin: Optional[Callable[[], bool]] = None,
              on_violation: Optional[Callable[[str, str], None]] = None):
    """Build an audit-hook callable enforcing ``allowed`` capabilities.

    ``origin`` — optional predicate; when given, the hook only enforces if it
    returns True (so the app can scope enforcement to a specific plugin).
    ``on_violation(capability, event)`` — optional; if it returns without
    raising, the operation is still blocked with :class:`CapabilityViolation`.
    """
    allowed = set(allowed)

    def hook(event: str, args):
        cap = _event_capability(event, args)
        if cap is None or cap in allowed:
            return
        if origin is not None and not origin():
            return
        if on_violation is not None:
            on_violation(cap, event)
        raise CapabilityViolation(cap, event)

    return hook


def install(allowed: Iterable[str],
            on_violation: Optional[Callable[[str, str], None]] = None) -> None:
    """Install a **global** enforcing audit hook (for a sandbox child process).

    Call once, as early as possible, before importing the plugin. Cannot be
    undone — that's by design for the isolated child.
    """
    sys.addaudithook(make_hook(set(allowed), origin=None, on_violation=on_violation))


__all__ = ["CapabilityViolation", "make_hook", "install",
           "_event_capability", "_IMPORT_CAP", "_EVENT_CAP"]
