"""ConversationHandler — Multi-step conversation state machine.

Models after python-telegram-bot's ConversationHandler pattern,
adapted for the Dexster Bot API with optional persistent state
via getSessionState/setSessionState endpoints.
"""

from __future__ import annotations

import time
from typing import Any, Optional

from ._logging import get_logger
from .handlers import Handler
from .types import Update

logger = get_logger("dexfather.conversation")

# Constants returned from handlers to control conversation flow
END = -1       # Return from handler to end conversation
TIMEOUT = -2   # Internal: conversation timed out


class ConversationHandler:
    """Manages multi-step conversation state machine.

    Models after python-telegram-bot's ConversationHandler pattern.
    State is keyed by (chat_id, user_id) tuple by default.

    Usage:
        NAME, AGE = range(2)

        conv = ConversationHandler(
            entry_points=[CommandHandler("start", ask_name)],
            states={
                NAME: [MessageHandler(process_name)],
                AGE: [MessageHandler(process_age)],
            },
            fallbacks=[CommandHandler("cancel", cancel)],
        )
        bot.add_handler(conv)
    """

    def __init__(
        self,
        entry_points: list[Handler],
        states: dict[int, list[Handler]],
        fallbacks: list[Handler],
        per_user: bool = True,
        per_chat: bool = True,
        allow_reentry: bool = False,
        conversation_timeout: Optional[float] = None,
        name: Optional[str] = None,
        persistent: bool = False,
    ) -> None:
        """Initialize a conversation state machine.

        Args:
            entry_points: Handlers that trigger conversation start.
            states: Mapping of state integers to handler lists.
            fallbacks: Handlers tried when no state handler matches.
            per_user: Key conversations by user ID (default: True).
            per_chat: Key conversations by chat ID (default: True).
            allow_reentry: Allow entry points while conversation is active.
            conversation_timeout: Seconds before an idle conversation expires.
            name: Handler name (required when ``persistent=True``).
            persistent: Persist state via server-side session storage.
        """
        if persistent and name is None:
            raise ValueError(
                "ConversationHandler with persistent=True requires a name"
            )

        self.entry_points = entry_points
        self.states = states
        self.fallbacks = fallbacks
        self.per_user = per_user
        self.per_chat = per_chat
        self.allow_reentry = allow_reentry
        self.conversation_timeout = conversation_timeout
        self.name = name
        self.persistent = persistent

        # Internal state: maps conversation key tuples to current state int
        self._conversations: dict[tuple, int] = {}
        # Timeout tracking: maps conversation key tuples to last-activity timestamps
        self._timeouts: dict[tuple, float] = {}

    # ── Key Construction ──────────────────────────────────────────────

    def _get_key(self, update: Update) -> tuple:
        """Build conversation key tuple from update."""
        parts: list[int] = []
        if self.per_chat:
            chat = update.effective_chat
            parts.append(chat.id if chat else 0)
        if self.per_user:
            user = update.effective_user
            parts.append(user.id if user else 0)
        return tuple(parts)

    def _state_key_str(self, key: tuple) -> str:
        """Convert tuple key to string for persistence API."""
        return ":".join(str(k) for k in key)

    # ── Persistence ───────────────────────────────────────────────────

    async def _save_state(self, bot: Any, key: tuple, state: int) -> None:
        """Save conversation state via setSessionState API endpoint."""
        if not self.persistent:
            return
        try:
            await bot._request(
                "setSessionState",
                handler_name=self.name,
                conversation_key=self._state_key_str(key),
                state=state,
                data=None,
            )
        except Exception as e:
            logger.warning("Failed to save persistent state: %s", e)

    async def _delete_state(self, bot: Any, key: tuple) -> None:
        """Delete conversation state via setSessionState API endpoint."""
        if not self.persistent:
            return
        try:
            await bot._request(
                "setSessionState",
                handler_name=self.name,
                conversation_key=self._state_key_str(key),
                state=None,
                data=None,
            )
        except Exception as e:
            logger.warning("Failed to delete persistent state: %s", e)

    async def _load_state(self, bot: Any, key: tuple) -> None:
        """Load conversation state from getSessionState API endpoint."""
        if not self.persistent:
            return
        try:
            resp = await bot._request(
                "getSessionState",
                handler_name=self.name,
                conversation_key=self._state_key_str(key),
            )
            if resp.result and resp.result.get("state") is not None:
                self._conversations[key] = resp.result["state"]
                self._timeouts[key] = time.time()
        except Exception as e:
            logger.warning("Failed to load persistent state: %s", e)

    # ── Handler Protocol ──────────────────────────────────────────────

    def check(self, update: Update) -> bool:
        """Return True if this handler should process the update.

        Matches if there is an active conversation for this key,
        or if any entry_point handler matches.
        """
        key = self._get_key(update)

        # Active conversation exists in memory
        if key in self._conversations:
            return True

        # Check if any entry_point matches
        for ep in self.entry_points:
            if ep.check(update):
                return True

        return False

    async def handle(self, update: Update, bot: Any) -> Any:
        """Core state machine: dispatch update to appropriate handler.

        1. If no active conversation: try entry points
        2. If active conversation: try state handlers, then fallbacks
        """
        key = self._get_key(update)

        # Try to load persistent state if not in memory
        if key not in self._conversations and self.persistent:
            await self._load_state(bot, key)

        # ── No active conversation: try entry points ──────────────
        if key not in self._conversations:
            for ep in self.entry_points:
                if ep.check(update):
                    result = await ep.handle(update, bot)
                    if result is not None and result != END:
                        self._conversations[key] = result
                        self._timeouts[key] = time.time()
                        if self.persistent:
                            await self._save_state(bot, key, result)
                    return result
            return None

        # ── Active conversation ───────────────────────────────────
        current_state = self._conversations[key]

        # Check timeout
        if self.conversation_timeout is not None and key in self._timeouts:
            elapsed = time.time() - self._timeouts[key]
            if elapsed > self.conversation_timeout:
                logger.info(
                    "Conversation %s timed out after %.1fs", key, elapsed
                )
                self._conversations.pop(key, None)
                self._timeouts.pop(key, None)
                if self.persistent:
                    await self._delete_state(bot, key)
                return TIMEOUT

        # Allow reentry: if an entry point matches while conversation is active
        if self.allow_reentry:
            for ep in self.entry_points:
                if ep.check(update):
                    result = await ep.handle(update, bot)
                    if result is None or result == END:
                        self._conversations.pop(key, None)
                        self._timeouts.pop(key, None)
                        if self.persistent:
                            await self._delete_state(bot, key)
                    else:
                        self._conversations[key] = result
                        self._timeouts[key] = time.time()
                        if self.persistent:
                            await self._save_state(bot, key, result)
                    return result

        # Try handlers for the current state
        state_handlers = self.states.get(current_state, [])
        for handler in state_handlers:
            if handler.check(update):
                result = await handler.handle(update, bot)
                if result == END:
                    self._conversations.pop(key, None)
                    self._timeouts.pop(key, None)
                    if self.persistent:
                        await self._delete_state(bot, key)
                elif result is not None:
                    self._conversations[key] = result
                    self._timeouts[key] = time.time()
                    if self.persistent:
                        await self._save_state(bot, key, result)
                return result

        # Try fallbacks
        for handler in self.fallbacks:
            if handler.check(update):
                result = await handler.handle(update, bot)
                if result == END:
                    self._conversations.pop(key, None)
                    self._timeouts.pop(key, None)
                    if self.persistent:
                        await self._delete_state(bot, key)
                elif result is not None:
                    self._conversations[key] = result
                    self._timeouts[key] = time.time()
                    if self.persistent:
                        await self._save_state(bot, key, result)
                return result

        logger.warning(
            "No handler matched in state %s for conversation %s",
            current_state,
            key,
        )
        return None
