"""
Telegram Card Navigator — navig gateway channel module.

Splits long LLM responses into paginated "reasoning cards" and renders
them with prev/next navigation inline keyboards.  The last card includes
extra actions: ✅ Accept · ♻️ Refine · 📋 Copy All.

Sessions are stored ephemerally in CallbackStore; no extra DB required.

Callback protocol:
  card:next:<KEY>       — advance to next card
  card:prev:<KEY>       — go to previous card
  card:jump:<KEY>:<N>   — jump to card index N (0-based)
  card:copy:<KEY>       — return full text as a single message
  card:accept:<KEY>     — dismiss navigator, confirm output
  card:refine:<KEY>     — hand off to RefinementEngine

Usage::

    from navig.gateway.channels.telegram_navigator import CardNavigator

    nav = CardNavigator(channel)
    session = await nav.create(chat_id, user_id, topic, llm_text, send_fn)
"""

from __future__ import annotations

import json
import logging
import re
import textwrap
import time
import uuid
from dataclasses import asdict, dataclass, field
from typing import Any, Callable, Coroutine, Optional

logger = logging.getLogger(__name__)

# ─────────────────────────────────────────────────────────────────
# Constants
# ─────────────────────────────────────────────────────────────────

MAX_CARD_CHARS = 3_800   # safe Telegram limit (4096 with nav header overhead)
CARD_SPLIT_PRIORITY = ["paragraph", "sentence", "hard"]


# ─────────────────────────────────────────────────────────────────
# Card splitting
# ─────────────────────────────────────────────────────────────────

def split_into_cards(
    text: str,
    max_chars: int = MAX_CARD_CHARS,
) -> list[str]:
    """
    Split *text* into a list of cards each ≤ *max_chars*.

    Strategy priority:
    1. paragraph boundaries (``\\n\\n``)
    2. sentence boundaries (``. ``, ``! ``, ``? ``)
    3. hard word‑wrap at *max_chars*
    """
    if len(text) <= max_chars:
        return [text]

    paragraphs = re.split(r"\n{2,}", text)
    chunks: list[str] = []
    current = ""

    for para in paragraphs:
        candidate = (current + "\n\n" + para).lstrip("\n") if current else para

        if len(candidate) <= max_chars:
            current = candidate
            continue

        # Paragraph alone is too large → split on sentences
        if len(para) > max_chars:
            if current:
                chunks.append(current)
                current = ""
            sentences = re.split(r"(?<=[.!?])\s+", para)
            for sentence in sentences:
                if len(current) + len(sentence) + 1 <= max_chars:
                    current = (current + " " + sentence).strip()
                else:
                    if current:
                        chunks.append(current)
                    if len(sentence) > max_chars:
                        # Hard split
                        for i in range(0, len(sentence), max_chars):
                            chunks.append(sentence[i : i + max_chars])
                        current = ""
                    else:
                        current = sentence
        else:
            # Flush current, start new card with this paragraph
            if current:
                chunks.append(current)
            current = para

    if current:
        chunks.append(current)

    # Final pass: ensure nothing exceeds max_chars (hard-split stragglers)
    final: list[str] = []
    for chunk in chunks:
        if len(chunk) <= max_chars:
            final.append(chunk)
        else:
            final.extend(
                chunk[i : i + max_chars] for i in range(0, len(chunk), max_chars)
            )
    return final


# ─────────────────────────────────────────────────────────────────
# CardSession dataclass
# ─────────────────────────────────────────────────────────────────

@dataclass
class CardSession:
    """Ephemeral session stored in CallbackStore extra."""

    cards: list[str]
    current: int
    chat_id: int
    user_id: int
    message_id: Optional[int]
    topic: str
    session_key: str         # CallbackStore key prefix "nav:<uuid>"
    created_at: float = field(default_factory=time.time)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "CardSession":
        return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})

    def serialise(self) -> str:
        return json.dumps(self.to_dict(), ensure_ascii=False)

    @classmethod
    def deserialise(cls, s: str) -> "CardSession":
        return cls.from_dict(json.loads(s))


# ─────────────────────────────────────────────────────────────────
# Keyboard builder
# ─────────────────────────────────────────────────────────────────

def build_nav_keyboard(
    session: CardSession,
    idx: int,
) -> list[list[dict]]:
    """
    Build inline keyboard for card at index *idx*.

    Normal card:  [← Prev] [Card X/N] [Next →]
    Last card extras: [✅ Accept] [♻️ Refine] [📋 Copy All]
    """
    key = session.session_key
    total = len(session.cards)
    rows: list[list[dict]] = []

    # Navigation row
    nav_row: list[dict] = []
    if idx > 0:
        nav_row.append({"text": "◀ Prev", "callback_data": f"card:prev:{key}"})
    nav_row.append({"text": f"📄 {idx + 1}/{total}", "callback_data": f"card:jump:{key}:{idx}"})
    if idx < total - 1:
        nav_row.append({"text": "Next ▶", "callback_data": f"card:next:{key}"})
    rows.append(nav_row)

    # Last card action row
    if idx == total - 1:
        rows.append([
            {"text": "✅ Accept", "callback_data": f"card:accept:{key}"},
            {"text": "♻️ Refine", "callback_data": f"card:refine:{key}"},
            {"text": "📋 All",    "callback_data": f"card:copy:{key}"},
        ])

    return rows


def _card_header(idx: int, total: int, topic: str) -> str:
    topic_short = textwrap.shorten(topic, width=40, placeholder="…") if topic else ""
    header = f"*{'💭 ' if topic_short else ''}思 Card {idx + 1} of {total}*"
    if topic_short:
        header = f"*💭 {topic_short}* — card {idx + 1}/{total}"
    return header


def _format_card(session: CardSession, idx: int) -> str:
    header = _card_header(idx, len(session.cards), session.topic)
    return f"{header}\n\n{session.cards[idx]}"


# ─────────────────────────────────────────────────────────────────
# CardNavigator
# ─────────────────────────────────────────────────────────────────

SendFn = Callable[..., Coroutine[Any, Any, Any]]


class CardNavigator:
    """
    Manages reasoning card sessions for a Telegram channel.

    Parameters
    ----------
    channel:
        The ``TelegramChannel`` (or compatible) instance that owns
        ``send_message`` / ``edit_message`` methods.
    cb_store:
        A ``CallbackStore`` instance from ``telegram_keyboards``.
        If not provided, one will be retrieved via
        ``get_callback_store()``.
    """

    def __init__(self, channel: Any, cb_store: Any = None):
        self.channel = channel
        self._store = cb_store

    def _get_store(self) -> Any:
        if self._store is not None:
            return self._store
        try:
            from navig.gateway.channels.telegram_keyboards import get_callback_store
            return get_callback_store()
        except Exception as exc:
            logger.error("CardNavigator: cannot get CallbackStore: %s", exc)
            raise

    # ── Public API ───────────────────────────────────────────────

    async def create(
        self,
        chat_id: int,
        user_id: int,
        topic: str,
        llm_text: str,
        send_fn: Optional[SendFn] = None,
    ) -> CardSession:
        """
        Split *llm_text* into cards and send the first card to *chat_id*.

        Returns the created ``CardSession``.
        """
        cards = split_into_cards(llm_text)
        session_key = f"nav:{uuid.uuid4().hex[:12]}"
        session = CardSession(
            cards=cards,
            current=0,
            chat_id=chat_id,
            user_id=user_id,
            message_id=None,
            topic=topic,
            session_key=session_key,
        )

        # Persist to CallbackStore with 2h TTL
        store = self._get_store()
        store.put(session_key, {"session": session.serialise()}, ttl=7200)

        if send_fn is None:
            send_fn = self.channel.send_message

        msg_id = await self._send_card(send_fn, session, 0)
        session.message_id = msg_id

        # Update store with message_id
        store.put(session_key, {"session": session.serialise()}, ttl=7200)
        return session

    async def go_to(
        self,
        session: CardSession,
        idx: int,
    ) -> None:
        """Navigate session to card *idx*, editing the existing message."""
        idx = max(0, min(idx, len(session.cards) - 1))
        session.current = idx
        keyboard = build_nav_keyboard(session, idx)
        text = _format_card(session, idx)

        if session.message_id is not None:
            try:
                await self.channel.edit_message(
                    session.chat_id,
                    session.message_id,
                    text,
                    parse_mode="Markdown",
                    reply_markup={"inline_keyboard": keyboard},
                )
                return
            except Exception as exc:
                logger.warning("CardNavigator.go_to edit failed: %s", exc)

        # Fallback: send new
        msg_id = await self.channel.send_message(
            session.chat_id,
            text,
            parse_mode="Markdown",
            reply_markup={"inline_keyboard": keyboard},
        )
        session.message_id = msg_id

    # ── Internal ─────────────────────────────────────────────────

    async def _send_card(
        self, send_fn: SendFn, session: CardSession, idx: int
    ) -> Optional[int]:
        keyboard = build_nav_keyboard(session, idx)
        text = _format_card(session, idx)
        try:
            result = await send_fn(
                session.chat_id,
                text,
                parse_mode="Markdown",
                reply_markup={"inline_keyboard": keyboard},
            )
            if isinstance(result, dict):
                return result.get("message_id")
            return None
        except Exception as exc:
            logger.error("CardNavigator._send_card failed: %s", exc)
            return None


# ─────────────────────────────────────────────────────────────────
# Callback handler (registered in CallbackHandler.handle)
# ─────────────────────────────────────────────────────────────────

async def handle_card_callback(
    channel: Any,
    callback_query: Any,
    cb_store: Any,
) -> None:
    """
    Dispatch ``card:*`` callback data.

    Expected callback_data formats:
      ``card:next:<KEY>``
      ``card:prev:<KEY>``
      ``card:jump:<KEY>:<N>``
      ``card:copy:<KEY>``
      ``card:accept:<KEY>``
      ``card:refine:<KEY>``
    """
    cb_id = getattr(callback_query, "id", None) or ""
    cb_data: str = ""
    if hasattr(callback_query, "data"):
        cb_data = callback_query.data or ""
    chat_id: int = 0
    message_id: Optional[int] = None

    msg = getattr(callback_query, "message", None)
    if msg:
        chat_id = getattr(getattr(msg, "chat", None), "id", 0) or 0
        message_id = getattr(msg, "message_id", None)

    # Parse: card:<action>:<key>[:<extra>]
    parts = cb_data.split(":", 3)
    if len(parts) < 3:
        return
    action = parts[1]
    session_key = parts[2]
    extra = parts[3] if len(parts) > 3 else ""

    # Load session
    entry = cb_store.get(session_key)
    if not entry:
        try:
            await channel.answer_callback_query(cb_id, "⚠️ Session expired")
        except Exception:  # noqa: BLE001
            pass  # best-effort; failure is non-critical
        return

    try:
        session = CardSession.deserialise(entry.get("session", "{}"))
    except Exception as exc:
        logger.warning("handle_card_callback: deserialise error: %s", exc)
        await channel.answer_callback_query(cb_id, "⚠️ Session error")
        return

    nav = CardNavigator(channel, cb_store)

    if action == "next":
        await nav.go_to(session, session.current + 1)
        cb_store.put(session_key, {"session": session.serialise()}, ttl=7200)

    elif action == "prev":
        await nav.go_to(session, session.current - 1)
        cb_store.put(session_key, {"session": session.serialise()}, ttl=7200)

    elif action == "jump":
        try:
            idx = int(extra)
            await nav.go_to(session, idx)
            cb_store.put(session_key, {"session": session.serialise()}, ttl=7200)
        except ValueError:
            pass  # malformed value; skip

    elif action == "copy":
        full_text = "\n\n───\n\n".join(session.cards)
        await channel.send_message(chat_id, full_text)
        await channel.answer_callback_query(cb_id, "📋 Full text sent")

    elif action == "accept":
        # Edit card to accepted state
        text = _format_card(session, session.current) + "\n\n✅ _Accepted_"
        try:
            await channel.edit_message(
                chat_id,
                session.message_id or message_id,
                text,
                parse_mode="Markdown",
                reply_markup={"inline_keyboard": []},
            )
        except Exception:  # noqa: BLE001
            pass  # best-effort; failure is non-critical
        cb_store.remove(session_key)
        await channel.answer_callback_query(cb_id, "✅ Accepted")

    elif action == "refine":
        # Hand off to RefinementEngine
        try:
            from navig.gateway.channels.telegram_refiner import RefinementEngine
            full_text = "\n\n".join(session.cards)
            refiner = RefinementEngine(channel, cb_store)
            await channel.answer_callback_query(cb_id, "♻️ Starting refinement…")
            await refiner.start(
                chat_id=session.chat_id,
                user_id=session.user_id,
                text=full_text,
                topic=session.topic,
            )
        except Exception as exc:
            logger.error("handle_card_callback refine error: %s", exc)
            await channel.answer_callback_query(cb_id, "⚠️ Refine error")
    else:
        await channel.answer_callback_query(cb_id, "")
