"""
Phase 46: Role-Based Access Control (RBAC) for the Brynq Platform.

Enterprise-grade authorization layer. Define roles with fine-grained
permissions, assign roles to users and agents, check authorization,
and audit every access decision.

Features:
  - ROLE DEFINITION: Create custom roles with permission sets and inheritance
  - BUILT-IN ROLES: viewer, member, moderator, admin, owner
  - ROLE ASSIGNMENT: Assign/remove roles to any entity (user or agent)
  - PERMISSION CHECK: Resolve all roles (including inherited), check access
  - PERMISSION GROUPS: List all known permissions grouped by category
  - AUDIT LOG: Every check_permission call is logged for compliance
  - ORGANIZATION SUPPORT: Org-level roles, membership management

Storage:
  state/broker/rbac/roles.json                     -- role definitions
  state/broker/rbac/assignments/{entity_id}.json   -- per-entity role list
  state/broker/rbac/orgs/{org_id}.json             -- organization data
  state/broker/rbac/access_log.jsonl               -- permission audit trail
"""

import json
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    _write_json_sync,
    _read_jsonl_sync,
    _append_jsonl_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

RBAC_DIR = BROKER_DIR / "rbac"
ROLES_FILE = RBAC_DIR / "roles.json"
ASSIGNMENTS_DIR = RBAC_DIR / "assignments"
ORGS_DIR = RBAC_DIR / "orgs"
ACCESS_LOG_FILE = RBAC_DIR / "access_log.jsonl"

for _d in (RBAC_DIR, ASSIGNMENTS_DIR, ORGS_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# ── Known Permissions ──────────────────────────────────────────────────────

PERMISSION_GROUPS = {
    "task": [
        "task.view",
        "task.create",
        "task.accept",
        "task.assign",
        "task.delete",
        "task.manage",
    ],
    "channel": [
        "channel.read",
        "channel.post",
        "channel.manage",
        "channel.delete",
    ],
    "agent": [
        "agent.view",
        "agent.manage",
        "agent.sandbox",
    ],
    "dm": [
        "dm.send",
        "dm.read",
    ],
    "billing": [
        "billing.view",
        "billing.manage",
    ],
    "platform": [
        "platform.configure",
        "platform.monitor",
    ],
    "role": [
        "role.view",
        "role.manage",
    ],
}

ALL_PERMISSIONS: set[str] = set()
for _perms in PERMISSION_GROUPS.values():
    ALL_PERMISSIONS.update(_perms)

# ── Built-in Role Definitions ─────────────────────────────────────────────

BUILTIN_ROLES: dict[str, dict] = {
    "viewer": {
        "name": "viewer",
        "description": "Read-only access to tasks, channels, and agents.",
        "permissions": ["task.view", "channel.read", "agent.view"],
        "inherits": None,
        "builtin": True,
    },
    "member": {
        "name": "member",
        "description": "Standard member: can create tasks, post to channels, send DMs.",
        "permissions": ["task.create", "task.accept", "channel.post", "dm.send"],
        "inherits": "viewer",
        "builtin": True,
    },
    "moderator": {
        "name": "moderator",
        "description": "Can manage agents, channels, and assign tasks.",
        "permissions": ["agent.manage", "channel.manage", "task.assign"],
        "inherits": "member",
        "builtin": True,
    },
    "admin": {
        "name": "admin",
        "description": "Full access to all permissions.",
        "permissions": ["*"],
        "inherits": None,
        "builtin": True,
    },
    "owner": {
        "name": "owner",
        "description": "Admin plus billing, role management, and platform configuration.",
        "permissions": ["billing.manage", "role.manage", "platform.configure"],
        "inherits": "admin",
        "builtin": True,
    },
}

# ── Internal Helpers ───────────────────────────────────────────────────────


def _load_roles() -> dict[str, dict]:
    """Load custom roles from disk, merging with built-in roles."""
    custom: dict[str, dict] = {}
    try:
        if ROLES_FILE.exists():
            custom = json.loads(ROLES_FILE.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        custom = {}
    merged = dict(BUILTIN_ROLES)
    merged.update(custom)
    return merged


def _save_custom_roles(roles: dict[str, dict]) -> None:
    """Persist only custom (non-builtin) roles."""
    custom = {k: v for k, v in roles.items() if not v.get("builtin")}
    _write_json_sync(ROLES_FILE, custom)


def _load_assignments(entity_id: str) -> dict:
    path = ASSIGNMENTS_DIR / f"{entity_id}.json"
    try:
        if path.exists():
            return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return {"entity_id": entity_id, "roles": [], "entity_type": "agent"}


def _save_assignments(entity_id: str, data: dict) -> None:
    path = ASSIGNMENTS_DIR / f"{entity_id}.json"
    _write_json_sync(path, data)


def _load_org(org_id: str) -> Optional[dict]:
    path = ORGS_DIR / f"{org_id}.json"
    try:
        if path.exists():
            return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return None


def _save_org(org_id: str, data: dict) -> None:
    path = ORGS_DIR / f"{org_id}.json"
    _write_json_sync(path, data)


def _log_access(entity_id: str, permission: str, allowed: bool, reason: str) -> None:
    entry = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "entity_id": entity_id,
        "permission": permission,
        "allowed": allowed,
        "reason": reason,
    }
    _append_jsonl_sync(ACCESS_LOG_FILE, entry)


# ── Role Definition ────────────────────────────────────────────────────────


def create_role(
    name: str,
    permissions: list[str],
    description: str = "",
    inherits: Optional[str] = None,
) -> dict:
    """Create or update a custom role.

    Args:
        name: Unique role name.
        permissions: List of permission strings (e.g. "task.create").
        description: Human-readable description of the role.
        inherits: Optional parent role whose permissions are included.

    Returns:
        The created role dict, or an error dict.
    """
    if not name or not isinstance(name, str):
        return {"error": "Role name must be a non-empty string."}
    if not isinstance(permissions, list):
        return {"error": "Permissions must be a list of strings."}

    roles = _load_roles()

    # Validate inheritance target exists
    if inherits is not None and inherits not in roles and inherits != name:
        return {"error": f"Inherited role '{inherits}' does not exist."}

    role = {
        "name": name,
        "description": description,
        "permissions": permissions,
        "inherits": inherits,
        "builtin": False,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    roles[name] = role
    _save_custom_roles(roles)
    return role


def delete_role(name: str) -> dict:
    """Delete a custom role. Built-in roles cannot be deleted."""
    if name in BUILTIN_ROLES:
        return {"error": f"Cannot delete built-in role '{name}'."}
    roles = _load_roles()
    if name not in roles:
        return {"error": f"Role '{name}' not found."}
    del roles[name]
    _save_custom_roles(roles)
    return {"deleted": name}


def get_role(name: str) -> Optional[dict]:
    """Return a single role definition, or None if not found."""
    roles = _load_roles()
    return roles.get(name)


def list_roles() -> list[dict]:
    """Return all defined roles (built-in + custom)."""
    roles = _load_roles()
    return list(roles.values())


# ── Permission Resolution ─────────────────────────────────────────────────


def _resolve_role_permissions(role_name: str, roles: dict[str, dict],
                              visited: Optional[set] = None) -> set[str]:
    """Recursively resolve all permissions for a role, including inherited."""
    if visited is None:
        visited = set()
    if role_name in visited:
        return set()  # circular inheritance guard
    visited.add(role_name)

    role = roles.get(role_name)
    if role is None:
        return set()

    perms = set(role.get("permissions", []))

    parent = role.get("inherits")
    if parent:
        perms |= _resolve_role_permissions(parent, roles, visited)

    return perms


def get_role_permissions(role_name: str) -> list[str]:
    """Return the fully resolved permission set for a role (including inherited)."""
    roles = _load_roles()
    perms = _resolve_role_permissions(role_name, roles)
    if "*" in perms:
        return sorted(ALL_PERMISSIONS | {"*"})
    return sorted(perms)


# ── Role Assignment ────────────────────────────────────────────────────────


def assign_role(entity_id: str, role_name: str, entity_type: str = "agent") -> dict:
    """Assign a role to an entity (user or agent).

    Returns:
        The updated assignment dict, or an error dict.
    """
    roles = _load_roles()
    if role_name not in roles:
        return {"error": f"Role '{role_name}' does not exist."}

    data = _load_assignments(entity_id)
    data["entity_type"] = entity_type
    if role_name not in data["roles"]:
        data["roles"].append(role_name)
    _save_assignments(entity_id, data)
    return data


def remove_role(entity_id: str, role_name: str) -> dict:
    """Remove a role from an entity.

    Returns:
        The updated assignment dict, or an error dict.
    """
    data = _load_assignments(entity_id)
    if role_name not in data["roles"]:
        return {"error": f"Entity '{entity_id}' does not have role '{role_name}'."}
    data["roles"].remove(role_name)
    _save_assignments(entity_id, data)
    return data


def get_roles(entity_id: str) -> list[str]:
    """Return the list of role names assigned to an entity."""
    data = _load_assignments(entity_id)
    return list(data["roles"])


# ── Permission Checking ───────────────────────────────────────────────────


def _collect_entity_permissions(entity_id: str) -> set[str]:
    """Gather all permissions for an entity from direct roles and org memberships."""
    roles_map = _load_roles()
    perms: set[str] = set()

    # Direct role assignments
    direct_roles = get_roles(entity_id)
    for rn in direct_roles:
        perms |= _resolve_role_permissions(rn, roles_map)

    # Org-inherited roles
    for org_file in ORGS_DIR.glob("*.json"):
        try:
            org = json.loads(org_file.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        for member in org.get("members", []):
            if member["entity_id"] == entity_id:
                member_role = member.get("role", "member")
                perms |= _resolve_role_permissions(member_role, roles_map)

    return perms


def has_permission(entity_id: str, permission: str) -> bool:
    """Check whether an entity has a specific permission. Returns bool."""
    perms = _collect_entity_permissions(entity_id)
    return "*" in perms or permission in perms


def check_permission(entity_id: str, permission: str) -> tuple[bool, str]:
    """Check permission and return (allowed, reason). Also logs the check."""
    perms = _collect_entity_permissions(entity_id)

    if "*" in perms:
        allowed, reason = True, "Wildcard permission granted."
    elif permission in perms:
        allowed, reason = True, f"Permission '{permission}' granted via assigned roles."
    else:
        assigned = get_roles(entity_id)
        if not assigned:
            allowed, reason = False, f"Entity '{entity_id}' has no assigned roles."
        else:
            allowed, reason = (
                False,
                f"Permission '{permission}' not found in roles: {', '.join(assigned)}.",
            )

    _log_access(entity_id, permission, allowed, reason)
    return allowed, reason


# ── Permission Listing ─────────────────────────────────────────────────────


def list_permissions() -> dict[str, list[str]]:
    """Return all known permissions grouped by category."""
    return {k: list(v) for k, v in PERMISSION_GROUPS.items()}


# ── Audit / Access Log ────────────────────────────────────────────────────


def get_access_log(
    entity_id: Optional[str] = None,
    permission: Optional[str] = None,
    limit: int = 50,
) -> list[dict]:
    """Return permission check history, optionally filtered.

    Args:
        entity_id: If set, only return entries for this entity.
        permission: If set, only return entries for this permission.
        limit: Maximum number of entries to return (most recent first).
    """
    entries = _read_jsonl_sync(ACCESS_LOG_FILE)
    if entity_id:
        entries = [e for e in entries if e.get("entity_id") == entity_id]
    if permission:
        entries = [e for e in entries if e.get("permission") == permission]
    # Most recent first
    entries.reverse()
    return entries[:limit]


# ── Organization Support ──────────────────────────────────────────────────


def create_org(name: str, owner_id: str) -> dict:
    """Create an organization owned by the given entity.

    The owner is automatically assigned the 'owner' role within the org.

    Returns:
        The org dict, or an error dict.
    """
    if not name or not isinstance(name, str):
        return {"error": "Organization name must be a non-empty string."}

    org_id = uuid.uuid4().hex[:12]
    org = {
        "org_id": org_id,
        "name": name,
        "owner_id": owner_id,
        "members": [
            {"entity_id": owner_id, "role": "owner", "joined_at": datetime.now(timezone.utc).isoformat()},
        ],
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    _save_org(org_id, org)
    return org


def add_member(org_id: str, entity_id: str, role: str = "member") -> dict:
    """Add a member to an organization with the given role.

    Returns:
        The updated org dict, or an error dict.
    """
    roles = _load_roles()
    if role not in roles:
        return {"error": f"Role '{role}' does not exist."}

    org = _load_org(org_id)
    if org is None:
        return {"error": f"Organization '{org_id}' not found."}

    # Check if already a member
    for m in org["members"]:
        if m["entity_id"] == entity_id:
            m["role"] = role
            _save_org(org_id, org)
            return org

    org["members"].append({
        "entity_id": entity_id,
        "role": role,
        "joined_at": datetime.now(timezone.utc).isoformat(),
    })
    _save_org(org_id, org)
    return org


def remove_member(org_id: str, entity_id: str) -> dict:
    """Remove a member from an organization.

    The owner cannot be removed. Returns the updated org dict or an error.
    """
    org = _load_org(org_id)
    if org is None:
        return {"error": f"Organization '{org_id}' not found."}

    if org["owner_id"] == entity_id:
        return {"error": "Cannot remove the organization owner."}

    original_len = len(org["members"])
    org["members"] = [m for m in org["members"] if m["entity_id"] != entity_id]
    if len(org["members"]) == original_len:
        return {"error": f"Entity '{entity_id}' is not a member of org '{org_id}'."}

    _save_org(org_id, org)
    return org


def list_members(org_id: str) -> list[dict]:
    """List all members of an organization."""
    org = _load_org(org_id)
    if org is None:
        return []
    return org.get("members", [])


def get_org(org_id: str) -> Optional[dict]:
    """Return the full org dict, or None if not found."""
    return _load_org(org_id)
