"""Discord access control — DM policies, guild allowlists, role permissions."""

from __future__ import annotations


class AccessController:
    """Multi-layered access control for Discord messages and slash commands.

    The controller is stateless — all required data (policies, paired user IDs,
    channel mappings) is passed by the caller (routes.py or gateway handlers).
    """

    def check_dm_access(
        self,
        user_id: str,
        dm_policy: str,
        paired_user_ids: list[str] | None,
    ) -> tuple[bool, str]:
        """Check whether a DM from user_id is permitted.

        Args:
            user_id: Discord user ID sending the DM.
            dm_policy: One of 'open', 'pairing', or 'disabled'.
            paired_user_ids: Approved Discord user IDs (used for 'pairing' policy).
                             Pass ``None`` when the pairing list could not be fetched
                             (fails open — the bot will still accept the message).

        Returns:
            (allowed, reason) — reason is empty string when allowed.
        """
        if dm_policy == "disabled":
            return False, "dm_policy is disabled"
        if dm_policy == "open":
            return True, ""
        if dm_policy == "pairing":
            if paired_user_ids is None:
                # Pairing data unavailable — fail open so users can still initiate
                return True, ""
            if user_id in paired_user_ids:
                return True, ""
            return False, "user not paired — send the bot a DM to request pairing"
        return False, f"unknown dm_policy: {dm_policy}"

    def check_guild_access(
        self,
        channel_id: str,
        user_id: str,
        member_roles: list[str],
        guild_policy: str,
        channel_mapping: dict | None,
    ) -> tuple[bool, str]:
        """Check whether a guild message or slash command interaction is permitted.

        Args:
            channel_id: Discord channel ID.
            user_id: Discord user ID.
            member_roles: Role IDs the user has in the guild.
            guild_policy: One of 'open', 'allowlist', or 'disabled'.
            channel_mapping: Row from discord_channel_mappings or None if unmapped.
                             Expected keys: enabled, allowed_user_ids, allowed_role_ids.

        Returns:
            (allowed, reason) — reason is empty string when allowed.
        """
        if guild_policy == "disabled":
            return False, "guild_policy is disabled"
        if guild_policy == "open":
            return True, ""
        if guild_policy == "allowlist":
            if channel_mapping is None or not channel_mapping.get("enabled", True):
                return False, "channel not in allowlist"
            allowed_users: list[str] = channel_mapping.get("allowed_user_ids", [])
            if allowed_users and not self.check_user_access(user_id, allowed_users):
                return False, "user not in channel user allowlist"
            allowed_roles: list[str] = channel_mapping.get("allowed_role_ids", [])
            if allowed_roles and not self.check_role_access(member_roles, allowed_roles):
                return False, "user lacks a required role for this channel"
            return True, ""
        return False, f"unknown guild_policy: {guild_policy}"

    def check_role_access(self, member_roles: list[str], allowed_roles: list[str]) -> bool:
        """Return True if the member has at least one of the allowed_roles.

        If allowed_roles is empty, all members are permitted.
        """
        if not allowed_roles:
            return True
        return bool(set(member_roles) & set(allowed_roles))

    def check_user_access(self, user_id: str, allowed_users: list[str]) -> bool:
        """Return True if user_id is in allowed_users.

        If allowed_users is empty, all users are permitted.
        """
        if not allowed_users:
            return True
        return user_id in allowed_users
