"""Telegram long-polling service for bots in polling mode.

Uses APScheduler to run getUpdates periodically for each bot
with update_mode == "polling". Processes updates identically
to the webhook handler (auto-save chat, dispatch commands).

Ref-counted: only starts/stops polling for bots that have active
CommandTrackerListeners with enabled CommandTrackers.
"""

from __future__ import annotations

import logging
import time
from typing import Any

from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from notify_bridge_core.log_context import bind_log_context
from notify_bridge_core.notifications.telegram.client import TelegramClient

from ..database.engine import get_engine
from ..database.models import CommandTracker, CommandTrackerListener, TelegramBot, TelegramChat
from ..services.telegram import save_chat_from_webhook
from .scheduler import get_scheduler

_LOGGER = logging.getLogger(__name__)

# Track last update_id per bot to use as offset
_last_update_id: dict[int, int] = {}

# Throttle auto-reclaim attempts so we don't hammer deleteWebhook when a
# stubborn external instance keeps re-setting the webhook.  (bot_id → unix ts)
_last_webhook_reclaim_at: dict[int, float] = {}
_WEBHOOK_RECLAIM_COOLDOWN_SECONDS = 60.0

# Phrase Telegram uses in the 409 response description for the
# "webhook is active" conflict.  Matched case-insensitively so we don't
# depend on exact wording.
_WEBHOOK_CONFLICT_PHRASE = "webhook is active"


async def _get_bot_ids_with_active_listeners() -> set[int]:
    """Return bot IDs that have at least one active command tracker listener.

    A bot is "active" if there is a CommandTrackerListener with
    listener_type="telegram_bot" pointing to it, AND the associated
    CommandTracker is enabled.
    """
    engine = get_engine()
    async with AsyncSession(engine) as session:
        result = await session.exec(
            select(CommandTrackerListener).where(
                CommandTrackerListener.listener_type == "telegram_bot"
            )
        )
        listeners = result.all()

        active_bot_ids: set[int] = set()
        tracker_ids = list({l.command_tracker_id for l in listeners})
        if tracker_ids:
            tracker_result = await session.exec(
                select(CommandTracker).where(
                    CommandTracker.id.in_(tracker_ids),
                    CommandTracker.enabled == True,  # noqa: E712
                )
            )
            enabled_tracker_ids = {t.id for t in tracker_result.all()}
            for listener in listeners:
                if listener.command_tracker_id in enabled_tracker_ids:
                    active_bot_ids.add(listener.listener_id)

        return active_bot_ids


async def start_command_listener_polling() -> None:
    """Schedule polling jobs only for bots with active command tracker listeners."""
    active_bot_ids = await _get_bot_ids_with_active_listeners()
    if not active_bot_ids:
        _LOGGER.info("No bots with active command listeners to poll")
        return

    engine = get_engine()
    async with AsyncSession(engine) as session:
        result = await session.exec(
            select(TelegramBot).where(
                TelegramBot.update_mode == "polling",
                TelegramBot.id.in_(active_bot_ids),
            )
        )
        bots = result.all()

    for bot in bots:
        schedule_bot_polling(bot.id)

    _LOGGER.info("Started command listener polling for %d bot(s)", len(bots))


async def start_bot_polling() -> None:
    """Schedule polling jobs for all bots with update_mode == 'polling'.

    Deprecated: prefer start_command_listener_polling() which only starts
    bots with active command tracker listeners.
    """
    await start_command_listener_polling()


async def start_bot_if_needed(bot_id: int) -> None:
    """Start polling for a bot if it has active listeners and is not already running."""
    engine = get_engine()
    async with AsyncSession(engine) as session:
        bot = await session.get(TelegramBot, bot_id)
        if not bot or bot.update_mode != "polling":
            return

    active_bot_ids = await _get_bot_ids_with_active_listeners()
    if bot_id in active_bot_ids:
        schedule_bot_polling(bot_id)


async def stop_bot_if_unused(bot_id: int) -> None:
    """Stop polling for a bot if it has no enabled command tracker listeners."""
    active_bot_ids = await _get_bot_ids_with_active_listeners()
    if bot_id not in active_bot_ids:
        unschedule_bot_polling(bot_id)


def schedule_bot_polling(bot_id: int) -> None:
    """Add a polling job for a bot (idempotent).

    We schedule at a 30 s interval, but each tick calls ``getUpdates`` with
    ``timeout=25`` — Telegram holds the connection open until either an
    update arrives or the timeout elapses, so in practice the bot streams
    updates with sub-second latency while consuming ~2 API calls / minute
    per bot (down from 20 under the old 3 s short-poll).
    """
    scheduler = get_scheduler()
    job_id = f"telegram_poll_{bot_id}"
    if scheduler.get_job(job_id):
        return
    scheduler.add_job(
        _poll_bot,
        "interval",
        seconds=30,
        id=job_id,
        args=[bot_id],
        replace_existing=True,
        max_instances=1,
    )
    _LOGGER.info("Started polling for bot %d (long-poll, 25s timeout)", bot_id)


def unschedule_bot_polling(bot_id: int) -> None:
    """Remove polling job for a bot."""
    scheduler = get_scheduler()
    job_id = f"telegram_poll_{bot_id}"
    if scheduler.get_job(job_id):
        scheduler.remove_job(job_id)
        _LOGGER.info("Stopped polling for bot %d", bot_id)


async def _handle_webhook_conflict(bot_id: int, bot_token: str, description: str) -> None:
    """Reclaim a bot stuck behind an active webhook set by another instance.

    Telegram's ``getUpdates`` returns 409 ``Conflict: can't use getUpdates
    method while webhook is active`` whenever a webhook is currently
    registered for the bot.  Since this bot row has ``update_mode="polling"``
    in our DB (that's the only reason we're polling it), the user's intent
    is polling, so we drop the webhook and resume.  Throttled to once per
    minute per bot so a rival instance constantly re-registering the
    webhook doesn't trigger a reclaim storm.
    """
    import time
    now = time.time()
    last = _last_webhook_reclaim_at.get(bot_id, 0.0)
    if now - last < _WEBHOOK_RECLAIM_COOLDOWN_SECONDS:
        # Already logged recently; stay quiet until cooldown expires so the
        # user gets one clear warning line per minute, not one every 3s.
        return
    _last_webhook_reclaim_at[bot_id] = now

    from .http_session import get_http_session
    http = await get_http_session()
    client = TelegramClient(http, bot_token)

    # Surface which URL stole the bot so the user can tell where it came from.
    conflicting_url = ""
    try:
        info = await client.get_webhook_info()
        if info.get("success"):
            conflicting_url = info.get("result", {}).get("url", "") or ""
    except Exception as err:  # noqa: BLE001
        _LOGGER.debug("getWebhookInfo during conflict recovery failed: %s", err)

    _LOGGER.warning(
        "Bot %d: webhook is active (url=%r) but this instance is in polling "
        "mode — calling deleteWebhook to reclaim. Telegram said: %s",
        bot_id, conflicting_url, description,
    )

    try:
        del_result = await client.delete_webhook()
        if del_result.get("success"):
            _LOGGER.warning(
                "Bot %d: webhook cleared; polling will resume on next tick",
                bot_id,
            )
            # Reset offset so we don't skip updates that accumulated during the
            # conflict window (Telegram held them until a client acknowledged).
            _last_update_id.pop(bot_id, None)
        else:
            _LOGGER.error(
                "Bot %d: deleteWebhook failed: %s",
                bot_id, del_result.get("error"),
            )
    except Exception as err:  # noqa: BLE001
        _LOGGER.error("Bot %d: deleteWebhook raised: %s", bot_id, err)


async def _poll_bot(bot_id: int) -> None:
    """Fetch updates from Telegram and process them."""
    engine = get_engine()

    # Eagerly load bot data and close session before aiohttp work
    # (cannot nest aiohttp inside active SQLAlchemy async session)
    async with AsyncSession(engine) as session:
        bot = await session.get(TelegramBot, bot_id)
        if not bot or bot.update_mode != "polling":
            unschedule_bot_polling(bot_id)
            return
        # Copy attributes before session closes to avoid detached-instance errors
        from types import SimpleNamespace
        bot_token = bot.token
        bot_obj = SimpleNamespace(
            id=bot.id, name=bot.name, token=bot.token, user_id=bot.user_id,
        )

    offset = _last_update_id.get(bot_id, 0)

    try:
        from .http_session import get_http_session
        http = await get_http_session()
        client = TelegramClient(http, bot_token)
        # Long-poll: hold connection open until an update arrives or 25 s
        # elapse. Drastically cuts API calls vs. 3 s short-poll.
        result = await client.get_updates(
            offset=offset + 1 if offset else None, limit=50, timeout=25,
        )
        if not result.get("success"):
            err_text = str(result.get("error") or "")
            # Detect the webhook-is-active conflict: another instance (or a
            # stale registration) owns this bot's delivery, so getUpdates
            # returns 409 and we get zero updates forever.  Reclaim it —
            # but only for bots the user explicitly set to polling mode.
            if _WEBHOOK_CONFLICT_PHRASE in err_text.lower():
                await _handle_webhook_conflict(bot_id, bot_token, err_text)
            else:
                _LOGGER.debug("Polling error for bot %d: %s", bot_id, err_text)
            return
        updates = result.get("result", [])
    except Exception as e:
        _LOGGER.debug("Polling error for bot %d: %s", bot_id, e)
        return

    if not updates:
        return

    # Update offset to latest
    _last_update_id[bot_id] = updates[-1]["update_id"]

    # Process each update
    from ..commands.handler import (
        classify_command_chat_action,
        handle_command,
        send_media_group,
        send_reply,
    )
    from .telegram_send import telegram_chat_action

    for update in updates:
        message = update.get("message")
        if not message:
            continue

        chat_info = message.get("chat", {})
        chat_id = str(chat_info.get("id", ""))
        text = message.get("text", "")
        from_user = message.get("from", {})
        msg_language = from_user.get("language_code", "")

        if not chat_id:
            continue

        # Auto-persist chat (fresh session per save)
        try:
            async with AsyncSession(engine) as save_session:
                await save_chat_from_webhook(save_session, bot_obj.id, chat_info, language_code=msg_language)
                await save_session.commit()
        except Exception:
            _LOGGER.debug("Failed to auto-save chat %s", chat_id, exc_info=True)

        # Dispatch commands (only if chat has commands enabled)
        if text and text.startswith("/"):
            from ..commands.parser import parse_command
            cmd_name, _, _ = parse_command(text)
            update_id = update.get("update_id")
            message_id = message.get("message_id")
            request_id = f"tg:{update_id}" if update_id is not None else f"tg:msg{message_id}"
            with bind_log_context(
                request_id=request_id,
                command=cmd_name or "-",
                chat_id=chat_id,
                bot_id=bot_obj.id,
            ):
                started = time.monotonic()
                try:
                    async with AsyncSession(engine) as cmd_session:
                        chat_row = (await cmd_session.exec(
                            select(TelegramChat).where(
                                TelegramChat.bot_id == bot_obj.id,
                                TelegramChat.chat_id == chat_id,
                            )
                        )).first()
                        if not chat_row or not chat_row.commands_enabled:
                            _LOGGER.info(
                                "Command ignored — commands disabled (poll) for bot=%s chat=%s",
                                bot_obj.id, chat_id,
                            )
                            continue
                        effective_lang = chat_row.language_override or msg_language
                    _LOGGER.info("Command received (poll): /%s args=%r lang=%s", cmd_name, text[:200], effective_lang)
                    async with telegram_chat_action(
                        bot_token, chat_id, classify_command_chat_action(text),
                    ):
                        responses = await handle_command(
                            bot_obj, chat_id, text,
                            language_code=effective_lang,
                            issuer=from_user or None,
                        )
                        if not responses:
                            _LOGGER.info(
                                "Command produced no response (cmd=%r, poll) after %.0f ms",
                                cmd_name, (time.monotonic() - started) * 1000,
                            )
                            continue
                        text_count = sum(1 for r in responses if r.text)
                        media_count = sum(len(r.media or []) for r in responses)
                        _LOGGER.info(
                            "Command dispatching %d response(s): text=%d media_items=%d",
                            len(responses), text_count, media_count,
                        )
                        for resp in responses:
                            if resp.text:
                                await send_reply(bot_token, chat_id, resp.text, reply_to_message_id=message_id)
                            if resp.media:
                                await send_media_group(bot_token, chat_id, resp.media, reply_to_message_id=message_id)
                        _LOGGER.info(
                            "Command /%s completed in %.0f ms (responses=%d media=%d)",
                            cmd_name, (time.monotonic() - started) * 1000,
                            len(responses), media_count,
                        )
                except Exception:
                    _LOGGER.exception(
                        "Error handling command /%s from bot %d after %.0f ms",
                        cmd_name, bot_id, (time.monotonic() - started) * 1000,
                    )


