"""
telegram/listener.py — Telegram bot long-polling listener.

Supports both the global (admin) bot and per-user bots.
TelegramListenerManager maintains a pool of TelegramListener instances.
"""
from __future__ import annotations

import asyncio
import logging

import httpx

from ..database import credential_store
from .triggers import get_enabled_triggers, is_allowed

logger = logging.getLogger(__name__)

_API = "https://api.telegram.org/bot{token}/{method}"
_POLL_TIMEOUT = 30
_HTTP_TIMEOUT = 35
_MAX_BACKOFF  = 60


class TelegramListener:
    """
    Single Telegram long-polling listener. user_id=None means global/admin bot.
    Per-user listeners read bot token from user_settings["telegram_bot_token"].
    """

    def __init__(self, user_id: str | None = None) -> None:
        self._user_id = user_id
        self._task: asyncio.Task | None = None
        self._running = False
        self._configured = False
        self._error: str | None = None

    # ── Lifecycle ──────────────────────────────────────────────────────────────

    def start(self) -> None:
        if self._task is None or self._task.done():
            name = f"telegram-listener-{self._user_id or 'global'}"
            self._task = asyncio.create_task(self._run_loop(), name=name)

    def stop(self) -> None:
        if self._task and not self._task.done():
            self._task.cancel()
        self._running = False

    def reconnect(self) -> None:
        self.stop()
        self.start()

    @property
    def status(self) -> dict:
        return {
            "configured": self._configured,
            "running": self._running,
            "error": self._error,
            "user_id": self._user_id,
        }

    # ── Credential helpers ─────────────────────────────────────────────────────

    async def _get_token(self) -> str | None:
        if self._user_id is None:
            return await credential_store.get("telegram:bot_token")
        from ..database import user_settings_store
        return await user_settings_store.get(self._user_id, "telegram_bot_token")

    async def _is_configured(self) -> bool:
        return bool(await self._get_token())

    # ── Session ID ────────────────────────────────────────────────────────────

    def _session_id(self, chat_id: str) -> str:
        if self._user_id is None:
            return f"telegram:{chat_id}"
        return f"telegram:{self._user_id}:{chat_id}"

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

    async def _run_loop(self) -> None:
        backoff = 1
        while True:
            self._configured = await self._is_configured()
            if not self._configured:
                await asyncio.sleep(60)
                continue
            try:
                await self._poll_loop()
                backoff = 1
            except asyncio.CancelledError:
                self._running = False
                break
            except Exception as e:
                self._running = False
                self._error = str(e)
                logger.warning("TelegramListener[%s] error: %s - retrying in %ds",
                               self._user_id or "global", e, backoff)
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, _MAX_BACKOFF)

    async def _poll_loop(self) -> None:
        offset = 0
        self._running = True
        self._error = None
        logger.info("TelegramListener[%s] started polling", self._user_id or "global")

        token = await self._get_token()

        async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT) as http:
            while True:
                url = _API.format(token=token, method="getUpdates")
                resp = await http.get(
                    url,
                    params={
                        "offset": offset,
                        "timeout": _POLL_TIMEOUT,
                        "allowed_updates": ["message"],
                    },
                )
                resp.raise_for_status()
                data = resp.json()
                if not data.get("ok"):
                    raise RuntimeError(f"Telegram API error: {data}")

                for update in data.get("result", []):
                    await self._handle_update(update, http, token)
                    offset = update["update_id"] + 1

    async def _handle_update(self, update: dict, http: httpx.AsyncClient, token: str) -> None:
        msg = update.get("message")
        if not msg:
            return

        chat_id = str(msg["chat"]["id"])
        text    = (msg.get("text") or "").strip()

        if not text:
            return

        from ..security import sanitize_external_content
        text = await sanitize_external_content(text, source="telegram")

        logger.info("TelegramListener[%s]: message from chat_id=%s",
                    self._user_id or "global", chat_id)

        # Whitelist check (scoped to this user)
        if not await is_allowed(chat_id, user_id=self._user_id):
            logger.info("TelegramListener[%s]: chat_id %s not whitelisted",
                        self._user_id or "global", chat_id)
            await self._send(http, token, chat_id,
                "Sorry, you are not authorised to interact with this bot.\n"
                "Please contact the system owner.")
            return

        # Email agent keyword routing — /keyword <message> before trigger matching
        if text.startswith("/"):
            parts = text[1:].split(None, 1)
            keyword = parts[0].lower()
            rest = parts[1].strip() if len(parts) > 1 else ""
            from ..inbox.telegram_handler import handle_keyword_message
            handled = await handle_keyword_message(
                chat_id=chat_id,
                user_id=self._user_id,
                keyword=keyword,
                message=rest,
            )
            if handled:
                return

        # Trigger matching (scoped to this user)
        triggers = await get_enabled_triggers(user_id=self._user_id)
        text_lower = text.lower()
        matched = next(
            (t for t in triggers
             if all(tok in text_lower for tok in t["trigger_word"].lower().split())),
            None,
        )

        if matched is None:
            # For global listener: fall back to default_agent_id
            # For per-user: no default (could add user-level default later)
            if self._user_id is None:
                default_agent_id = await credential_store.get("telegram:default_agent_id")
                if not default_agent_id:
                    logger.info(
                        "TelegramListener[global]: no trigger match and no default agent "
                        "for chat_id=%s - dropping", chat_id,
                    )
                    return
                matched = {"agent_id": default_agent_id, "trigger_word": "(default)"}
            else:
                logger.info(
                    "TelegramListener[%s]: no trigger match for chat_id=%s - dropping",
                    self._user_id, chat_id,
                )
                return

        logger.info(
            "TelegramListener[%s]: trigger '%s' matched - running agent %s",
            self._user_id or "global", matched["trigger_word"], matched["agent_id"],
        )
        agent_input = (
            f"You received a Telegram message.\n"
            f"From chat_id: {chat_id}\n\n"
            f"{text}\n\n"
            f"Please process this request. "
            f"Your response will be sent back to chat_id {chat_id} via Telegram."
        )
        try:
            from ..agents.runner import agent_runner
            result_text = await agent_runner.run_agent_and_wait(
                matched["agent_id"], override_message=agent_input,
                session_id=self._session_id(chat_id),
            )
        except Exception as e:
            logger.error("TelegramListener[%s]: agent run failed: %s",
                         self._user_id or "global", e)
            result_text = f"Sorry, an error occurred while processing your request: {e}"

        await self._send(http, token, chat_id, result_text)

    async def _send(self, http: httpx.AsyncClient, token: str, chat_id: str, text: str) -> None:
        try:
            url = _API.format(token=token, method="sendMessage")
            resp = await http.post(url, json={"chat_id": chat_id, "text": text[:4096]})
            resp.raise_for_status()
        except Exception as e:
            logger.error("TelegramListener[%s]: failed to send to %s: %s",
                         self._user_id or "global", chat_id, e)


# ── Manager ───────────────────────────────────────────────────────────────────

class TelegramListenerManager:
    """
    Maintains a pool of TelegramListener instances.
    Exposes the same .status / .reconnect() / .stop() interface as the old
    singleton for backward compatibility with existing admin routes.
    """

    def __init__(self) -> None:
        self._listeners: dict[str | None, TelegramListener] = {}

    def _ensure(self, user_id: str | None) -> TelegramListener:
        if user_id not in self._listeners:
            self._listeners[user_id] = TelegramListener(user_id=user_id)
        return self._listeners[user_id]

    def start(self) -> None:
        self._ensure(None).start()

    def start_all(self) -> None:
        self.start()

    def stop(self) -> None:
        g = self._listeners.get(None)
        if g:
            g.stop()

    def stop_all(self) -> None:
        for listener in self._listeners.values():
            listener.stop()
        self._listeners.clear()

    def reconnect(self) -> None:
        self._ensure(None).reconnect()

    def start_for_user(self, user_id: str) -> None:
        self._ensure(user_id).reconnect()

    def stop_for_user(self, user_id: str) -> None:
        if user_id in self._listeners:
            self._listeners[user_id].stop()
            del self._listeners[user_id]

    def reconnect_for_user(self, user_id: str) -> None:
        self._ensure(user_id).reconnect()

    @property
    def status(self) -> dict:
        g = self._listeners.get(None)
        return g.status if g else {"configured": False, "running": False, "error": None}

    def all_statuses(self) -> dict:
        return {(k or "global"): v.status for k, v in self._listeners.items()}


# Module-level singleton (backward-compatible name kept)
telegram_listener = TelegramListenerManager()
