"""
Permission Service

Implements the access control model:
- Channel read/write permissions
- Task permissions by role
- Notification permissions (who can notify whom)
- Communication matrix (who can communicate with whom)

Permission Levels:
- L0: CEO (full access)
- L1: Board (cross-org access)
- L2: Main PM (all cells access)
- L3: Cell PM (own cell + PM channel)
- L4: Cell Members (own cell only)
- SPECIAL: Auditor (silent read all)

Architecture:
- agents_config.py is the SINGLE SOURCE OF TRUTH for permission configuration
- This service provides runtime enforcement using AgentContext (role + team)
- No duplicate permission definitions - all derived from agents_config
"""

from typing import TYPE_CHECKING, Any, ClassVar
from uuid import UUID

from sqlalchemy import select

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession

from roboco.agents_config import (
    AGENT_ROLE_MAP,
    AGENT_TEAM_MAP,
    CHANNEL_ACCESS,
)
from roboco.foundation.identity import Role as _FoundationRole
from roboco.foundation.policy.communications import NOTIFY_SENDER_ROLES
from roboco.models import AgentRole, Team
from roboco.models.permissions import (
    KB_PERMISSIONS,
    ROLE_LEVELS,
    TASK_PERMISSIONS,
    AgentContext,
    PermissionLevel,
    TaskAction,
)
from roboco.services.base import SingletonService

# =============================================================================
# CHANNEL PERMISSIONS (derived from agents_config.CHANNEL_ACCESS)
# =============================================================================

# Build role→team mapping from agents_config for efficient lookups
_ROLE_TEAM_LOOKUP: dict[tuple[str, str | None], list[str]] = {}
for agent_slug, role in AGENT_ROLE_MAP.items():
    team = AGENT_TEAM_MAP.get(agent_slug)
    key = (role, team)
    if key not in _ROLE_TEAM_LOOKUP:
        _ROLE_TEAM_LOOKUP[key] = []
    _ROLE_TEAM_LOOKUP[key].append(agent_slug)


def _get_agents_for_role_team(role: AgentRole, team: Team | None) -> list[str]:
    """Get all agent slugs that match a role and optional team."""
    role_str = role.value
    team_str = team.value if team else None
    return _ROLE_TEAM_LOOKUP.get((role_str, team_str), [])


# =============================================================================
# NOTIFICATION PERMISSIONS (derived from foundation.NOTIFY_SENDER_ROLES)
# =============================================================================
#
# Foundation owns the sender allowlist. Scope semantics (who each sender may
# reach) live here because they depend on the recipient's role + team — not
# pure identity data. The mapping below preserves the legacy
# NOTIFICATION_PERMISSIONS behaviour:
#   - main_pm / ceo            -> "all"   (no recipient filter)
#   - cell_pm                  -> "cell"  (own team members + any PM)
#   - product_owner            -> list    (management chain only)
#   - head_marketing           -> list    (management chain only)
# Auditor is intentionally NOT a sender (silent observer per spec §5.5).

# Board members notify the management chain only. Roles, not slugs — matched
# against recipient.role in can_notify(). Each list is the set of recipient
# roles the sender may notify.
_BOARD_NOTIFY_TARGETS: dict[AgentRole, frozenset[AgentRole]] = {
    AgentRole.PRODUCT_OWNER: frozenset(
        {AgentRole.MAIN_PM, AgentRole.HEAD_MARKETING, AgentRole.AUDITOR, AgentRole.CEO}
    ),
    AgentRole.HEAD_MARKETING: frozenset(
        {AgentRole.MAIN_PM, AgentRole.PRODUCT_OWNER, AgentRole.AUDITOR, AgentRole.CEO}
    ),
}


def _can_role_send_notifications(role: AgentRole) -> bool:
    """Whether a role may call notify(). Canonical in foundation."""
    try:
        return _FoundationRole(role.value) in NOTIFY_SENDER_ROLES
    except ValueError:
        return False


def _get_notification_scope(role: AgentRole) -> str | list[AgentRole]:
    """Scope of recipients a sender role may notify.

    Returns:
      - ``"all"`` for main_pm / ceo (no recipient filter)
      - ``"cell"`` for cell_pm (own team + any PM)
      - ``list[AgentRole]`` for board members (management chain only)
      - ``[]`` for roles that cannot send notifications
    """
    if role in (AgentRole.MAIN_PM, AgentRole.CEO):
        return "all"
    if role is AgentRole.CELL_PM:
        return "cell"
    targets = _BOARD_NOTIFY_TARGETS.get(role)
    if targets is not None:
        return list(targets)
    return []


# =============================================================================
# PERMISSION SERVICE
# =============================================================================


class PermissionService(SingletonService):
    """
    Service for checking and enforcing permissions.

    Implements the access control model.
    Uses agents_config.py as the SINGLE SOURCE OF TRUTH.

    Usage:
        service = PermissionService()

        # Check channel access
        if service.can_read_channel(agent_ctx, channel_name):
            messages = await get_messages(channel_name)

        # Check notification permission
        if service.can_notify(sender_ctx, recipient_ctx):
            await send_notification(...)
    """

    service_name: ClassVar[str] = "permissions"
    # No duplicate storage - uses agents_config.CHANNEL_ACCESS directly

    # =========================================================================
    # CHANNEL PERMISSIONS (uses agents_config.CHANNEL_ACCESS)
    # =========================================================================

    def _check_channel_access_for_agent(
        self,
        agent: AgentContext,
        channel_name: str,
        access_type: str,
    ) -> bool:
        """
        Check channel access using agents_config.CHANNEL_ACCESS.

        Converts AgentContext (role+team) to potential agent slugs,
        then checks if any of them have access.
        """
        channel = CHANNEL_ACCESS.get(channel_name)
        if not channel:
            self.log.warning("Unknown channel", channel=channel_name)
            return False

        # Get list of agent slugs that match this role+team
        agent_slugs = _get_agents_for_role_team(agent.role, agent.team)

        # Check if any matching agent has the requested access
        access_list = channel.get(access_type, [])
        silent_list = channel.get("silent", [])

        for slug in agent_slugs:
            if slug in access_list:
                return True
            if access_type == "read" and slug in silent_list:
                return True

        return False

    def can_read_channel(
        self,
        agent: AgentContext,
        channel_name: str,
    ) -> bool:
        """Check if agent can read from a channel."""
        # Auditor has silent read access to everything
        if agent.role == AgentRole.AUDITOR:
            return True

        # CEO has full access
        if agent.role == AgentRole.CEO:
            return True

        # Main PM has access to all channels
        if agent.role == AgentRole.MAIN_PM:
            return True

        return self._check_channel_access_for_agent(agent, channel_name, "read")

    def can_write_channel(
        self,
        agent: AgentContext,
        channel_name: str,
    ) -> bool:
        """Check if agent can write to a channel."""
        # CEO has full access
        if agent.role == AgentRole.CEO:
            return True

        # Auditor is a silent, read-only observer (no say/dm in its verb surface);
        # deny channel writes so this layer matches the role's actual capabilities.
        if agent.role == AgentRole.AUDITOR:
            return False

        # Main PM has access to all channels
        if agent.role == AgentRole.MAIN_PM:
            return True

        return self._check_channel_access_for_agent(agent, channel_name, "write")

    def get_accessible_channels(
        self,
        agent: AgentContext,
    ) -> list[str]:
        """Get list of channels an agent can read."""
        channels = []
        for channel_name in CHANNEL_ACCESS:
            if self.can_read_channel(agent, channel_name):
                channels.append(channel_name)
        return channels

    def get_writable_channels(
        self,
        agent: AgentContext,
    ) -> list[str]:
        """Get list of channels an agent can write to."""
        channels = []
        for channel_name in CHANNEL_ACCESS:
            if self.can_write_channel(agent, channel_name):
                channels.append(channel_name)
        return channels

    # =========================================================================
    # NOTIFICATION PERMISSIONS (foundation.NOTIFY_SENDER_ROLES + local scope)
    # =========================================================================

    def can_send_notifications(self, agent: AgentContext) -> bool:
        """Check if agent can send notifications at all."""
        return _can_role_send_notifications(agent.role)

    def can_notify(
        self,
        sender: AgentContext,
        recipient: AgentContext,
    ) -> bool:
        """Check if sender can notify recipient.

        Sender allowlist comes from foundation.NOTIFY_SENDER_ROLES.
        Scope rules are encoded in _get_notification_scope.
        """
        if not self.can_send_notifications(sender):
            return False

        scope = _get_notification_scope(sender.role)

        # "all" scope means can notify anyone
        if scope == "all":
            return True

        # "cell" scope means can only notify own cell members
        if scope == "cell":
            # Cell PM can notify other PMs (Cell PMs or Main PM) for coordination
            if recipient.role in (AgentRole.CELL_PM, AgentRole.MAIN_PM):
                return True
            # Otherwise must be same team
            return sender.team == recipient.team

        # List scope - check if recipient.role is in the allowed role list
        if isinstance(scope, list):
            return recipient.role in scope

        return False

    # =========================================================================
    # TASK PERMISSIONS
    # =========================================================================

    def can_perform_task_action(
        self,
        agent: AgentContext,
        action: str,
        task_team: Team | None = None,
    ) -> bool:
        """Check if agent can perform a task action."""
        # The CEO is the ultimate authority and may perform any task action on
        # any task — unblock, reassign, cancel, override status, etc. Every
        # route that gates a write through this helper therefore lets the CEO
        # through (the panel operates as the CEO).
        if agent.role == AgentRole.CEO:
            return True
        allowed_actions = TASK_PERMISSIONS.get(agent.role, set())

        if action in allowed_actions:
            # VIEW_OWN means only own cell
            is_view_own = action == TaskAction.VIEW_OWN and task_team
            wrong_team = agent.team and agent.team != task_team
            return not (is_view_own and wrong_team)

        # Check VIEW_ALL permission for VIEW_OWN requests
        return bool(
            action == TaskAction.VIEW_OWN and TaskAction.VIEW_ALL in allowed_actions
        )

    def get_task_actions(
        self,
        agent: AgentContext,
    ) -> set[str]:
        """Get all task actions an agent can perform."""
        return TASK_PERMISSIONS.get(agent.role, set())

    # =========================================================================
    # KNOWLEDGE BASE PERMISSIONS
    # =========================================================================

    def can_perform_kb_action(
        self,
        agent: AgentContext,
        action: str,
    ) -> bool:
        """Check if agent can perform a knowledge base action."""
        allowed_actions = KB_PERMISSIONS.get(agent.role, set())
        return action in allowed_actions

    def get_kb_actions(
        self,
        agent: AgentContext,
    ) -> set[str]:
        """Get all KB actions an agent can perform."""
        return KB_PERMISSIONS.get(agent.role, set())

    # =========================================================================
    # UTILITY
    # =========================================================================

    def get_permission_level(self, role: AgentRole) -> PermissionLevel:
        """Get the permission level for a role."""
        return ROLE_LEVELS.get(role, PermissionLevel.CELL_MEMBER)

    def check_all(
        self,
        agent: AgentContext,
    ) -> dict[str, Any]:
        """Get comprehensive permission summary for an agent."""
        return {
            "agent_id": str(agent.agent_id),
            "role": agent.role.value,
            "team": agent.team.value if agent.team else None,
            "level": self.get_permission_level(agent.role).name,
            "readable_channels": self.get_accessible_channels(agent),
            "writable_channels": self.get_writable_channels(agent),
            "can_send_notifications": self.can_send_notifications(agent),
            "task_actions": list(self.get_task_actions(agent)),
        }


# =============================================================================
# ASYNC DATABASE LOOKUPS
# =============================================================================

# Roles with full access to all channels (bypass membership checks)
PRIVILEGED_ROLES = frozenset({AgentRole.CEO, AgentRole.AUDITOR, AgentRole.MAIN_PM})


async def has_privileged_access(db: "AsyncSession", agent_id: UUID) -> bool:
    """
    Check if agent has a privileged role (CEO, Auditor, Main PM).

    These roles have full access to all channels regardless of membership.
    Queries by both id and slug since agent_id could be either
    (CEO uses UUID-style slug, others use short slugs like "be-dev-1").
    """
    from roboco.db.tables import AgentTable

    result = await db.execute(
        select(AgentTable.role).where(
            (AgentTable.id == agent_id) | (AgentTable.slug == str(agent_id))
        )
    )
    role = result.scalar_one_or_none()
    return role in PRIVILEGED_ROLES if role else False


# PM_ROLES is canonical in foundation.identity. Re-export for backwards
# compatibility; new consumers import from foundation directly.
from roboco.foundation.identity import PM_ROLES  # noqa: F401, E402

MANAGEMENT_ROLES = frozenset(
    {AgentRole.CEO, AgentRole.PRODUCT_OWNER, AgentRole.CELL_PM, AgentRole.MAIN_PM}
)


async def is_pm_role(db: "AsyncSession", agent_id: UUID) -> bool:
    """
    Check if agent has a PM or management role.

    PM roles (Cell PM, Main PM) and management roles (CEO, Product Owner)
    can create task-linked sessions and assign work.
    """
    from roboco.db.tables import AgentTable

    result = await db.execute(
        select(AgentTable.role).where(
            (AgentTable.id == agent_id) | (AgentTable.slug == str(agent_id))
        )
    )
    role = result.scalar_one_or_none()
    return role in MANAGEMENT_ROLES if role else False
