"""Permission enforcement service for CleverAgents.

Provides permission checking and an ``enforce_permission`` decorator
that can be applied at CLI/service boundaries.  In local mode every
action is permitted; server-side enforcement evaluates role bindings
against the permission policy.  Actual wiring of the decorator into
CLI and service call sites is deferred to a future integration pass.

Based on issue #344 — feat(security): add permission system.
"""

from __future__ import annotations

import os
from collections.abc import Callable
from functools import wraps
from typing import TypeVar

import structlog

from cleveragents.domain.models.core.permission import (
    DEFAULT_LOCAL_ROLE_MAPPING,
    ROLE_PERMISSIONS,
    PermissionAction,
    PermissionCheck,
    PermissionPolicy,
    PermissionRole,
    PermissionScope,
    RoleBinding,
)

logger = structlog.get_logger(__name__)

F = TypeVar("F", bound=Callable[..., object])


def _validate_identifier(value: str, name: str) -> str:
    """Strip whitespace and reject empty identifiers.

    Raises ``ValueError`` when *value* is empty or whitespace-only after
    stripping, matching the ``RoleBinding`` Pydantic constraint
    (``min_length=1``, ``str_strip_whitespace=True``).
    """
    stripped = value.strip()
    if not stripped:
        msg = f"{name} must be a non-empty string (got {value!r})"
        raise ValueError(msg)
    return stripped


__all__ = [
    "PermissionService",
    "enforce_permission",
    "get_default_permission_service",
    "set_default_permission_service",
]

# ── Environment helpers ───────────────────────────────────────────

_SERVER_MODE_ENV = "CLEVERAGENTS_SERVER_MODE"


def _is_server_mode() -> bool:
    """Return ``True`` when the process is running in server mode."""
    return os.environ.get(_SERVER_MODE_ENV, "").lower() in {"1", "true", "yes"}


# ── Binding index key ─────────────────────────────────────────────

_BindingKey = tuple[str, PermissionScope, str]


def _binding_key(binding: RoleBinding) -> _BindingKey:
    """Return the lookup key for a role binding."""
    return (binding.principal, binding.scope, binding.scope_id)


# ── Service ───────────────────────────────────────────────────────


class PermissionService:
    """Central permission check and enforcement service.

    * **Local mode** (default): all checks return *allowed*.
    * **Server mode**: evaluates role bindings against the permission
      policy.  If no server-side store is available the check raises
      ``NotImplementedError``.
    """

    def __init__(
        self,
        *,
        policy: PermissionPolicy | None = None,
        bindings: list[RoleBinding] | None = None,
    ) -> None:
        self._policy = policy or PermissionPolicy()
        self._bindings_index: dict[_BindingKey, list[RoleBinding]] = {}
        self._logger = logger.bind(service="permission")
        for b in bindings or []:
            self._bindings_index.setdefault(_binding_key(b), []).append(b)

    # ── public helpers ────────────────────────────────────────────

    @staticmethod
    def is_local_mode() -> bool:
        """Return ``True`` when running in local (non-server) mode."""
        return not _is_server_mode()

    # ── role binding queries ──────────────────────────────────────

    def get_role_bindings(
        self,
        principal: str,
        scope: PermissionScope,
        scope_id: str,
    ) -> list[RoleBinding]:
        """Return all role bindings for *principal* at *scope*/*scope_id*.

        In local mode a synthetic OWNER binding is returned for every
        query so that all actions are implicitly permitted.

        Raises ``ValueError`` if *principal* or *scope_id* is empty or
        whitespace-only after stripping.
        """
        principal = _validate_identifier(principal, "principal")
        scope_id = _validate_identifier(scope_id, "scope_id")

        if self.is_local_mode():
            default_role = DEFAULT_LOCAL_ROLE_MAPPING.get(
                principal,
                DEFAULT_LOCAL_ROLE_MAPPING.get("*", PermissionRole.OWNER),
            )
            return [
                RoleBinding(
                    principal=principal,
                    role=default_role,
                    scope=scope,
                    scope_id=scope_id,
                )
            ]

        key: _BindingKey = (principal, scope, scope_id)
        return list(self._bindings_index.get(key, []))

    # ── permission check ──────────────────────────────────────────

    def check_permission(
        self,
        principal: str,
        action: PermissionAction,
        scope: PermissionScope,
        scope_id: str,
    ) -> PermissionCheck:
        """Check whether *principal* may perform *action* on *scope*/*scope_id*.

        Returns a ``PermissionCheck`` with the result and a human-readable
        reason.

        Raises ``ValueError`` if *principal* or *scope_id* is empty or
        whitespace-only after stripping.
        """
        principal = _validate_identifier(principal, "principal")
        scope_id = _validate_identifier(scope_id, "scope_id")

        local_mode = self.is_local_mode()

        # Fast-path: local mode is always permissive
        if local_mode:
            return PermissionCheck(
                principal=principal,
                action=action,
                scope=scope,
                scope_id=scope_id,
                result=True,
                reason="local mode — all actions permitted",
            )

        # Server mode: evaluate role bindings
        bindings = self.get_role_bindings(principal, scope, scope_id)

        # Check allow-overrides first
        for override in self._policy.allow_overrides:
            if (
                override.principal == principal
                and override.scope == scope
                and override.scope_id == scope_id
            ):
                allowed_actions = ROLE_PERMISSIONS.get(override.role, frozenset())
                if action in allowed_actions:
                    return PermissionCheck(
                        principal=principal,
                        action=action,
                        scope=scope,
                        scope_id=scope_id,
                        result=True,
                        reason=f"allowed by override ({override.role})",
                    )

        # Check regular bindings
        for binding in bindings:
            allowed_actions = ROLE_PERMISSIONS.get(binding.role, frozenset())
            if action in allowed_actions:
                return PermissionCheck(
                    principal=principal,
                    action=action,
                    scope=scope,
                    scope_id=scope_id,
                    result=True,
                    reason=f"allowed by role {binding.role}",
                )

        # Default deny
        if self._policy.default_deny:
            result = PermissionCheck(
                principal=principal,
                action=action,
                scope=scope,
                scope_id=scope_id,
                result=False,
                reason="denied by default-deny policy",
            )
            self._logger.warning(
                "permission_denied",
                principal=principal,
                action=str(action),
                scope=str(scope),
                scope_id=scope_id,
                reason=result.reason,
            )
            return result

        # If default_deny is False and no explicit binding exists,
        # permit the action.
        return PermissionCheck(
            principal=principal,
            action=action,
            scope=scope,
            scope_id=scope_id,
            result=True,
            reason="no binding found, default-deny disabled",
        )

    # ── add / remove bindings ─────────────────────────────────────

    def add_binding(self, binding: RoleBinding) -> bool:
        """Register a new role binding.

        Returns ``True`` if the binding was added, ``False`` if an
        identical binding already exists (duplicates are prevented).
        """
        key = _binding_key(binding)
        bucket = self._bindings_index.setdefault(key, [])
        for existing in bucket:
            if existing.role == binding.role:
                return False
        bucket.append(binding)
        self._logger.info(
            "binding_added",
            principal=binding.principal,
            role=str(binding.role),
            scope=str(binding.scope),
            scope_id=binding.scope_id,
        )
        return True

    def remove_binding(self, binding: RoleBinding) -> bool:
        """Remove all matching role bindings.

        Returns ``True`` if at least one binding was found and removed.
        """
        key = _binding_key(binding)
        bucket = self._bindings_index.get(key)
        if bucket is None:
            return False
        original_len = len(bucket)
        self._bindings_index[key] = [b for b in bucket if b.role != binding.role]
        removed = original_len - len(self._bindings_index[key])
        if not self._bindings_index[key]:
            del self._bindings_index[key]
        if removed > 0:
            self._logger.info(
                "binding_removed",
                principal=binding.principal,
                role=str(binding.role),
                scope=str(binding.scope),
                scope_id=binding.scope_id,
                count=removed,
            )
            return True
        return False


# ── Module-level default service ──────────────────────────────────

_default_service: PermissionService | None = None


def get_default_permission_service() -> PermissionService:
    """Return the module-level default ``PermissionService``.

    If no default has been configured via
    ``set_default_permission_service``, a new instance is created on
    first access and cached for the lifetime of the process.
    """
    global _default_service
    if _default_service is None:
        _default_service = PermissionService()
    return _default_service


def set_default_permission_service(service: PermissionService | None) -> None:
    """Replace (or clear) the module-level default ``PermissionService``."""
    global _default_service
    _default_service = service


# ── Decorator for enforcement at CLI/service boundaries ───────────


def enforce_permission(
    action: PermissionAction,
    scope: PermissionScope,
    scope_id: str = "*",
    *,
    principal: str = "local",
    service: PermissionService | None = None,
) -> Callable[[F], F]:
    """Decorator that enforces a permission check before function execution.

    In local mode the check is a no-op (always allowed).  In server
    mode a ``PermissionError`` is raised when the check fails.

    When *service* is ``None`` the module-level default service
    (see ``get_default_permission_service``) is used so that bindings
    registered at application startup are visible to every decorated
    call site.
    """

    def decorator(fn: F) -> F:
        @wraps(fn)
        def wrapper(*args: object, **kwargs: object) -> object:
            svc = service or get_default_permission_service()
            check = svc.check_permission(principal, action, scope, scope_id)
            if not check.result:
                raise PermissionError(
                    f"Permission denied: {check.reason} "
                    f"(principal={principal}, action={action}, "
                    f"scope={scope}, scope_id={scope_id})"
                )
            return fn(*args, **kwargs)

        return wrapper  # type: ignore[return-value]

    return decorator
