"""Permission domain models for CleverAgents.

Namespace/project/plan/skill permission model with role bindings
(owner/admin/editor/viewer) and default-deny policy.  Server-side
enforcement is deferred; local mode returns permissive defaults.

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

from __future__ import annotations

from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field

__all__ = [
    "DEFAULT_LOCAL_ROLE_MAPPING",
    "ROLE_PERMISSIONS",
    "PermissionAction",
    "PermissionCheck",
    "PermissionPolicy",
    "PermissionRole",
    "PermissionScope",
    "RoleBinding",
]


# ── Enumerations ──────────────────────────────────────────────────


class PermissionRole(StrEnum):
    """Role hierarchy for the permission system.

    Independent of ``OrgRole`` in ``org.py``, which describes
    organisation membership.  These roles govern resource-level
    access control.
    """

    OWNER = "owner"
    ADMIN = "admin"
    EDITOR = "editor"
    VIEWER = "viewer"


class PermissionScope(StrEnum):
    """Scopes to which permissions can be applied."""

    NAMESPACE = "namespace"
    PROJECT = "project"
    PLAN = "plan"
    SKILL = "skill"


class PermissionAction(StrEnum):
    """Actions that can be gated by the permission system."""

    READ = "read"
    WRITE = "write"
    EXECUTE = "execute"
    DELETE = "delete"
    ADMIN = "admin"


# ── Data models ───────────────────────────────────────────────────


class RoleBinding(BaseModel):
    """Binds a principal to a role within a specific scope."""

    principal: str = Field(
        ...,
        min_length=1,
        description="User or service principal identifier.",
    )
    role: PermissionRole = Field(
        ...,
        description="Assigned permission role.",
    )
    scope: PermissionScope = Field(
        ...,
        description="Scope at which the role applies.",
    )
    scope_id: str = Field(
        ...,
        min_length=1,
        description="Identifier of the scoped resource.",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class PermissionCheck(BaseModel):
    """Result of a permission check."""

    principal: str = Field(..., description="Checked principal.")
    action: PermissionAction = Field(..., description="Action that was checked.")
    scope: PermissionScope = Field(..., description="Scope of the check.")
    scope_id: str = Field(..., description="Identifier of the scoped resource.")
    result: bool = Field(..., description="Whether the action is allowed.")
    reason: str = Field(..., description="Human-readable explanation.")

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class PermissionPolicy(BaseModel):
    """Top-level permission policy configuration."""

    default_deny: bool = Field(
        default=True,
        description="When True, actions are denied unless explicitly allowed.",
    )
    allow_overrides: list[RoleBinding] = Field(
        default_factory=list,
        description="Explicit allow-overrides that bypass default-deny.",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


# ── Constants ─────────────────────────────────────────────────────


ROLE_PERMISSIONS: dict[PermissionRole, frozenset[PermissionAction]] = {
    PermissionRole.OWNER: frozenset(PermissionAction),
    PermissionRole.ADMIN: frozenset(
        {
            PermissionAction.READ,
            PermissionAction.WRITE,
            PermissionAction.EXECUTE,
            PermissionAction.DELETE,
            PermissionAction.ADMIN,
        }
    ),
    PermissionRole.EDITOR: frozenset(
        {
            PermissionAction.READ,
            PermissionAction.WRITE,
            PermissionAction.EXECUTE,
        }
    ),
    PermissionRole.VIEWER: frozenset(
        {
            PermissionAction.READ,
        }
    ),
}
"""Mapping of each role to its set of allowed actions.

Note: OWNER and ADMIN currently grant the same set of actions.
The distinction is intentional — OWNER designates the resource
creator or namespace owner, while ADMIN is a delegated
administrative role.  Future server-mode enforcement may
differentiate them (e.g. ownership transfer, namespace deletion)
without requiring a schema change.
"""


DEFAULT_LOCAL_ROLE_MAPPING: dict[str, PermissionRole] = {
    "*": PermissionRole.OWNER,
}
"""In local mode every principal is treated as OWNER."""
