"""Discord inbound message router.

MessageRouter performs preflight checks on incoming Discord messages and
determines whether they should be queued for agent processing.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field

from peon_mcp.grukk.discord.access import AccessController


@dataclass
class DiscordMessage:
    """Lightweight representation of a Discord message for routing."""

    config_id: int
    channel_id: str
    user_id: str
    username: str
    content: str
    message_id: str = ""
    replied_to: str = ""
    # guild_id is empty for DMs
    guild_id: str = ""
    # bot_user_id is the bot's own Discord user ID (used for mention detection)
    bot_user_id: str = ""
    # Role IDs the user holds in the guild (empty for DMs)
    member_roles: list[str] = field(default_factory=list)


@dataclass
class RouteResult:
    """Result of MessageRouter.route_message()."""

    accepted: bool
    reason: str = ""
    # Normalized content after stripping a mention prefix or command prefix
    normalized_content: str = ""


class MessageRouter:
    """Routes incoming Discord messages to the inbound queue.

    The router applies preflight checks in order:
    1. Access control: DM policy (open/pairing/disabled) or guild policy
       (open/allowlist/disabled) via AccessController.
    2. If require_mention is set, the message must @-mention the bot.
    3. Normalize content (strip mention prefix, leading/trailing whitespace).

    The router is stateless — it receives config + channel mapping data from
    the caller (routes.py) and returns a RouteResult.
    """

    def __init__(self) -> None:
        self._access = AccessController()

    def route_message(
        self,
        message: DiscordMessage,
        *,
        dm_policy: str,
        guild_policy: str,
        require_mention: bool,
        channel_mapping: dict | None,
        paired_user_ids: list[str] | None = None,
    ) -> RouteResult:
        """Decide whether to accept and queue an inbound Discord message.

        Args:
            message: The incoming Discord message.
            dm_policy: One of 'open', 'pairing', or 'disabled'.
            guild_policy: One of 'open', 'allowlist', or 'disabled'.
            require_mention: If True, the bot ignores messages without @mention.
            channel_mapping: The channel mapping row for this channel, or None
                             if no mapping exists. Expected keys: enabled,
                             allowed_user_ids (list[str]), allowed_role_ids (list[str]).
            paired_user_ids: Approved Discord user IDs for 'pairing' DM policy.

        Returns:
            RouteResult with accepted=True if the message should be queued.
        """
        is_dm = not message.guild_id

        if is_dm:
            # Pass paired_user_ids as-is; None means "data unavailable → fail open".
            allowed, reason = self._access.check_dm_access(
                message.user_id, dm_policy, paired_user_ids
            )
        else:
            # Guild messages: routing depends on guild_policy.
            # - "disabled": reject all guild messages.
            # - "open": accept all guild messages (no channel mapping required).
            # - "allowlist": require an enabled channel mapping with user/role checks.
            if guild_policy == "disabled":
                allowed, reason = False, "guild_policy is disabled"
            elif guild_policy == "open":
                # Open policy: accept from any channel, no mapping needed.
                if channel_mapping is not None and not channel_mapping.get("enabled", True):
                    allowed, reason = False, "channel mapping is disabled"
                else:
                    allowed, reason = True, ""
            elif channel_mapping is None:
                allowed, reason = False, "channel not mapped"
            elif not channel_mapping.get("enabled", True):
                allowed, reason = False, "channel mapping is disabled"
            else:
                allowed, reason = self._access.check_guild_access(
                    message.channel_id,
                    message.user_id,
                    message.member_roles,
                    guild_policy,
                    channel_mapping,
                )

        if not allowed:
            return RouteResult(accepted=False, reason=reason)

        # Mention check (applies to guild messages when require_mention is set)
        if require_mention and not is_dm:
            normalized = self._strip_mention(message.content, message.bot_user_id)
            if normalized is None:
                return RouteResult(accepted=False, reason="mention required but not present")
            content = normalized.strip()
        else:
            content = message.content.strip()

        return RouteResult(accepted=True, normalized_content=content)

    def _strip_mention(self, content: str, bot_user_id: str) -> str | None:
        """Return content with the bot @mention stripped, or None if not present.

        Handles both <@USER_ID> and <@!USER_ID> (nickname mention) forms.
        Mentions can appear anywhere in the message (not just at the start).
        If bot_user_id is empty, any mention is accepted and stripped.
        """
        # Match <@USER_ID> or <@!USER_ID> anywhere in the message
        if bot_user_id:
            pattern = rf"<@!?{re.escape(bot_user_id)}>"
        else:
            pattern = r"<@!?\d+>"

        m = re.search(pattern, content.strip())
        if m:
            # Remove the mention from wherever it appears
            stripped = content.strip()[:m.start()] + content.strip()[m.end():]
            return stripped.strip()

        return None
