"""Discord interaction router — dispatches component and modal interactions.

Registered as the ``on_interaction`` gateway event handler in
:class:`peon_mcp.discord.gateway.DiscordGateway`.

Features:
- Dispatches to component handlers by ``custom_id`` prefix.
- Per-user sliding-window rate limiting (5 interactions / 10 s).
- Component TTL tracking — expired components respond with an error message
  instead of attempting the API call.
- Modal submission forwarding (``TaskCreateModal`` handles itself via
  ``on_submit``; the router logs unknown modal custom_ids).
"""

from __future__ import annotations

import logging
import time
from collections import defaultdict
from typing import Any

from peon_mcp.grukk.discord import components as _components

logger = logging.getLogger(__name__)

import discord


# ---------------------------------------------------------------------------
# Rate limiter — sliding window, per user
# ---------------------------------------------------------------------------


class _RateLimiter:
    """Sliding-window rate limiter keyed by user ID string."""

    def __init__(self, max_calls: int = 5, window_seconds: float = 10.0) -> None:
        self._max = max_calls
        self._window = window_seconds
        self._history: dict[str, list[float]] = defaultdict(list)

    def is_allowed(self, user_id: str) -> bool:
        """Return True and record the call if within rate limit; False otherwise."""
        now = time.monotonic()
        timestamps = self._history[user_id]
        cutoff = now - self._window
        # Prune timestamps outside the window
        while timestamps and timestamps[0] < cutoff:
            timestamps.pop(0)
        if len(timestamps) >= self._max:
            return False
        timestamps.append(now)
        return True


# ---------------------------------------------------------------------------
# Component state tracker — TTL-based expiry
# ---------------------------------------------------------------------------


class _ComponentTracker:
    """Tracks active component custom_ids with an expiry timestamp.

    Components are registered when a View is attached to a message.
    After the TTL elapses the component is considered expired; the router
    rejects the interaction with an informative message instead of calling
    the REST API.

    Unknown custom_ids (never registered) are treated as active so that
    components sent before a restart are still functional.
    """

    def __init__(self, default_ttl: float = 3600.0) -> None:
        self._default_ttl = default_ttl
        self._expiry: dict[str, float] = {}

    def register(self, custom_id: str, ttl: float | None = None) -> None:
        """Mark *custom_id* as active for the given TTL (seconds)."""
        self._expiry[custom_id] = time.monotonic() + (ttl or self._default_ttl)

    def is_active(self, custom_id: str) -> bool:
        """Return True if the component is active or has never been registered."""
        exp = self._expiry.get(custom_id)
        if exp is None:
            return True  # Unknown — allow (pre-restart components)
        return time.monotonic() < exp

    def expire(self, custom_id: str) -> None:
        """Immediately expire a custom_id after a one-shot action is consumed."""
        self._expiry.pop(custom_id, None)

    def cleanup(self) -> None:
        """Remove stale entries to prevent unbounded memory growth."""
        now = time.monotonic()
        self._expiry = {k: v for k, v in self._expiry.items() if v > now}


# Module-level singletons shared across all gateway instances in this process.
_rate_limiter = _RateLimiter(max_calls=5, window_seconds=10.0)
_tracker = _ComponentTracker(default_ttl=3600.0)


# ---------------------------------------------------------------------------
# Public helpers
# ---------------------------------------------------------------------------


def register_component(custom_id: str, ttl: float | None = None) -> None:
    """Register a component custom_id so the TTL tracker knows it is active.

    Call this after attaching a View to a Discord message to enable expiry
    checking in the interaction router.
    """
    _tracker.register(custom_id, ttl)


# ---------------------------------------------------------------------------
# Top-level handler
# ---------------------------------------------------------------------------


async def on_interaction(interaction: Any) -> None:
    """Dispatch a Discord interaction to the appropriate handler.

    Handles component interactions (buttons/selects) and modal submissions.
    Application command interactions are silently ignored here — they are
    handled by the command tree registered in the gateway.

    Rate-limits per user: 5 interactions per 10 seconds.  Exceeding the
    limit sends an ephemeral warning and drops the interaction.
    """
    interaction_type = getattr(interaction, "type", None)
    if interaction_type is None:
        return

    # Application commands are handled elsewhere.
    if interaction_type == discord.InteractionType.application_command:
        return

    # Rate limiting
    user = getattr(interaction, "user", None)
    user_id = str(user.id) if user is not None else "unknown"
    if not _rate_limiter.is_allowed(user_id):
        try:
            if not interaction.response.is_done():
                await interaction.response.send_message(
                    "⚠️ You're clicking too fast! Please wait a moment.",
                    ephemeral=True,
                )
        except Exception:
            pass
        return

    if interaction_type == discord.InteractionType.component:
        await _handle_component(interaction)
    elif interaction_type == discord.InteractionType.modal_submit:
        await _handle_modal(interaction)


# ---------------------------------------------------------------------------
# Component dispatcher
# ---------------------------------------------------------------------------


async def _handle_component(interaction: Any) -> None:
    """Route a component interaction by its custom_id prefix."""
    data = getattr(interaction, "data", None) or {}
    custom_id: str = (
        data.get("custom_id", "") if isinstance(data, dict) else getattr(data, "custom_id", "")
    )
    if not custom_id:
        logger.debug("Component interaction with no custom_id — ignoring.")
        return

    if not _tracker.is_active(custom_id):
        try:
            await interaction.response.send_message(
                "⏱️ This component has expired and is no longer active.", ephemeral=True
            )
        except Exception:
            pass
        return

    logger.debug("Component interaction: custom_id=%r", custom_id)

    prefix, _, suffix = custom_id.partition(":")

    try:
        await _dispatch_component(interaction, prefix, suffix, data)
    except getattr(discord, "InteractionResponded", Exception):
        # Already responded — safe to ignore.
        pass
    except Exception as exc:
        logger.error("Error handling component %r: %s", custom_id, exc)
        try:
            if not interaction.response.is_done():
                await interaction.response.send_message(
                    "⚠️ An error occurred while processing this interaction.",
                    ephemeral=True,
                )
        except Exception:
            pass


async def _dispatch_component(
    interaction: Any, prefix: str, suffix: str, data: Any
) -> None:
    """Perform the action for a given component prefix."""

    def _values() -> list[str]:
        if isinstance(data, dict):
            return data.get("values", [])
        return getattr(data, "values", [])

    if prefix == "task_approve":
        await interaction.response.defer()
        ok, msg = await _components._api_patch_task(suffix, {"status": "in_progress"})
        result = "✅ Task approved — status set to **in_progress**." if ok else f"❌ Failed: {msg}"
        await interaction.edit_original_response(content=result)
        _tracker.expire(f"{prefix}:{suffix}")

    elif prefix == "task_reject":
        await interaction.response.defer()
        ok, msg = await _components._api_patch_task(suffix, {"status": "cancelled"})
        result = "🚫 Task **cancelled**." if ok else f"❌ Failed: {msg}"
        await interaction.edit_original_response(content=result)
        _tracker.expire(f"{prefix}:{suffix}")

    elif prefix == "task_skip":
        await interaction.response.defer()
        await interaction.edit_original_response(content="⏭️ Skipped.")
        _tracker.expire(f"{prefix}:{suffix}")

    elif prefix == "pr_approve":
        user = getattr(interaction, "user", None)
        mention = user.mention if user else "Someone"
        await interaction.response.defer()
        await interaction.edit_original_response(
            content=f"✅ {mention} approved PR for Task #{suffix}."
        )
        _tracker.expire(f"{prefix}:{suffix}")

    elif prefix == "pr_changes":
        user = getattr(interaction, "user", None)
        mention = user.mention if user else "Someone"
        await interaction.response.defer()
        await interaction.edit_original_response(
            content=f"📝 {mention} requested changes on Task #{suffix}."
        )
        _tracker.expire(f"{prefix}:{suffix}")

    elif prefix == "priority_select":
        values = _values()
        if values:
            priority = values[0]
            await interaction.response.defer()
            ok, msg = await _components._api_patch_task(suffix, {"priority": priority})
            badges = {
                "critical": "🔴 Critical",
                "high": "🟠 High",
                "medium": "🟡 Medium",
                "low": "🟢 Low",
            }
            badge = badges.get(priority, priority)
            result = (
                f"Priority updated to **{badge}**." if ok else f"❌ Failed: {msg}"
            )
            await interaction.edit_original_response(content=result)

    elif prefix == "feature_status_select":
        values = _values()
        if values:
            status = values[0]
            await interaction.response.defer()
            ok, msg = await _components._api_update_feature(suffix, {"status": status})
            result = (
                f"Feature status updated to **{status}**." if ok else f"❌ Failed: {msg}"
            )
            await interaction.edit_original_response(content=result)

    else:
        logger.debug("Unknown component prefix %r — ignoring.", prefix)


# ---------------------------------------------------------------------------
# Modal dispatcher
# ---------------------------------------------------------------------------


async def _handle_modal(interaction: Any) -> None:
    """Handle modal_submit interactions.

    ``TaskCreateModal`` handles itself via ``discord.ui.Modal.on_submit``.
    This fallback logs unknown modal custom_ids for debugging.
    """
    data = getattr(interaction, "data", None) or {}
    custom_id: str = (
        data.get("custom_id", "") if isinstance(data, dict) else getattr(data, "custom_id", "")
    )
    logger.debug("Modal submit: custom_id=%r", custom_id)

    if not custom_id.startswith("task_create"):
        logger.debug("Unknown modal custom_id %r — no handler registered.", custom_id)
