# services/broadcast.py
from __future__ import annotations
import asyncio
import html
import logging
import aiosqlite
from aiogram import Bot
from aiogram.exceptions import TelegramForbiddenError, TelegramRetryAfter
from config import Speaker
from db import get_verified_users, mark_blocked, get_unsubscribed_users, get_all_users
from services.subscription import check_subscriptions

logger = logging.getLogger(__name__)

SEND_DELAY = 0.035   # 35ms between messages (~28 msg/s, safely below Telegram limit)
MAX_RETRIES = 3

# Telegram caption limit for send_photo
PHOTO_CAPTION_MAX = 1024

# How often to log a progress line while a broadcast is running.
PROGRESS_LOG_EVERY = 25


def _log_progress(label: str, idx: int, total: int, delivered: int, skipped: int, errors: int) -> None:
    """Fire-and-forget progress line every PROGRESS_LOG_EVERY processed users."""
    if idx % PROGRESS_LOG_EVERY == 0:
        logger.info(
            "%s progress: %d/%d processed (sent=%d blocked=%d failed=%d)",
            label, idx, total, delivered, skipped, errors,
        )


def _log_summary(label: str, delivered: int, skipped: int, errors: int) -> None:
    logger.info("%s finished: sent=%d blocked=%d failed=%d", label, delivered, skipped, errors)


async def broadcast(
    bot: Bot,
    db: aiosqlite.Connection,
    conference_id: str,
    speakers: list[Speaker],
    text: str,
    photo_file_id: str | None = None,
) -> dict[str, int]:
    """
    Sends a message to all verified users of the conference.
    conference_id and speakers are fixed at call time (snapshot).
    Re-verifies subscriptions for each user before sending.
    Returns {"delivered": int, "skipped": int, "errors": int}.
    """
    users = await get_verified_users(db, conference_id)
    delivered = skipped = errors = 0
    total = len(users)

    for idx, user_id in enumerate(users, start=1):
        try:
            not_subscribed = await check_subscriptions(bot, user_id, speakers)
        except TelegramRetryAfter as e:
            logger.warning("Rate limit during subscription check for user %d, waiting %ds", user_id, e.retry_after)
            await asyncio.sleep(e.retry_after)
            # Retry subscription check once after waiting
            try:
                not_subscribed = await check_subscriptions(bot, user_id, speakers)
            except TelegramRetryAfter:
                logger.error("Rate limit persists for user %d, skipping", user_id)
                skipped += 1
                _log_progress("broadcast", idx, total, delivered, skipped, errors)
                continue
        if not_subscribed:
            skipped += 1
            _log_progress("broadcast", idx, total, delivered, skipped, errors)
            continue   # no sleep here — no Telegram API send happened

        result = await _send_with_retry(bot, db, user_id, text, photo_file_id)
        if result == "delivered":
            delivered += 1
        elif result == "blocked":
            skipped += 1
        else:
            errors += 1

        _log_progress("broadcast", idx, total, delivered, skipped, errors)
        await asyncio.sleep(SEND_DELAY)

    _log_summary("broadcast", delivered, skipped, errors)
    return {"delivered": delivered, "skipped": skipped, "errors": errors}


async def broadcast_unsubscribed(
    bot: Bot,
    db: aiosqlite.Connection,
    conference_id: str,
    speakers: list[Speaker],
    text: str,
    photo_file_id: str | None,
    keyboard,
) -> dict[str, int]:
    """
    Sends a message to users who started the bot but never completed subscription.
    Photo (if any) is sent without caption first; text + speaker list + button follow as text message.
    Delivered = both steps succeeded. Step-1 non-403 error = errors. Any 403 = blocked + skipped.
    """
    users = await get_unsubscribed_users(db, conference_id)
    delivered = skipped = errors = 0
    total = len(users)

    speakers_list = "\n".join(
        f"• <a href='https://t.me/{s.channel.lstrip('@')}'>{html.escape(s.name)}</a>"
        for s in speakers
    )
    full_text = f"{text}\n\n{speakers_list}" if text else speakers_list

    for idx, user_id in enumerate(users, start=1):
        if photo_file_id:
            try:
                await bot.send_photo(chat_id=user_id, photo=photo_file_id)
            except TelegramForbiddenError:
                await mark_blocked(db, user_id)
                skipped += 1
                _log_progress("broadcast_unsubscribed", idx, total, delivered, skipped, errors)
                await asyncio.sleep(SEND_DELAY)
                continue
            except Exception as e:
                logger.error("Failed to send photo to user %d: %s", user_id, e)
                errors += 1
                _log_progress("broadcast_unsubscribed", idx, total, delivered, skipped, errors)
                await asyncio.sleep(SEND_DELAY)
                continue

        result = await _send_with_retry(bot, db, user_id, full_text, None, reply_markup=keyboard)
        if result == "delivered":
            delivered += 1
        elif result == "blocked":
            skipped += 1
        else:
            errors += 1

        _log_progress("broadcast_unsubscribed", idx, total, delivered, skipped, errors)
        await asyncio.sleep(SEND_DELAY)

    _log_summary("broadcast_unsubscribed", delivered, skipped, errors)
    return {"delivered": delivered, "skipped": skipped, "errors": errors}


async def broadcast_to_all(
    bot: Bot,
    db: aiosqlite.Connection,
    text: str,
    photo_file_id: str | None,
    keyboard,
    exclude_user_ids: set[int],
) -> dict[str, int]:
    """Send `text` (+ optional photo, + optional inline keyboard) to every user in DB
    minus blocked and `exclude_user_ids`. Splits photo+text into two messages when
    `caption > 1024` so the keyboard attaches to the text message.

    Returns {"delivered", "skipped", "errors"}. `delivered` requires the final
    message (the one carrying the keyboard, if any) to succeed.
    """
    users = await get_all_users(db, exclude_user_ids)
    delivered = skipped = errors = 0
    total = len(users)

    for idx, user_id in enumerate(users, start=1):
        if photo_file_id and len(text) > PHOTO_CAPTION_MAX:
            # Send photo without caption first, then text+keyboard separately.
            try:
                await bot.send_photo(chat_id=user_id, photo=photo_file_id)
            except TelegramForbiddenError:
                await mark_blocked(db, user_id)
                skipped += 1
                _log_progress("broadcast_to_all", idx, total, delivered, skipped, errors)
                await asyncio.sleep(SEND_DELAY)
                continue
            except Exception as e:
                logger.error("Failed to send photo to user %d: %s", user_id, e)
                errors += 1
                _log_progress("broadcast_to_all", idx, total, delivered, skipped, errors)
                await asyncio.sleep(SEND_DELAY)
                continue
            result = await _send_with_retry(bot, db, user_id, text, photo_file_id=None, reply_markup=keyboard)
        elif photo_file_id:
            # Single message: photo + caption + keyboard.
            result = await _send_with_retry(bot, db, user_id, text, photo_file_id, reply_markup=keyboard)
        else:
            # Text only.
            result = await _send_with_retry(bot, db, user_id, text, photo_file_id=None, reply_markup=keyboard)

        if result == "delivered":
            delivered += 1
        elif result == "blocked":
            skipped += 1
        else:
            errors += 1

        _log_progress("broadcast_to_all", idx, total, delivered, skipped, errors)
        await asyncio.sleep(SEND_DELAY)

    _log_summary("broadcast_to_all", delivered, skipped, errors)
    return {"delivered": delivered, "skipped": skipped, "errors": errors}


async def _send_with_retry(
    bot: Bot,
    db: aiosqlite.Connection,
    user_id: int,
    text: str,
    photo_file_id: str | None,
    reply_markup=None,
) -> str:
    for attempt in range(MAX_RETRIES):
        try:
            if photo_file_id:
                await bot.send_photo(chat_id=user_id, photo=photo_file_id, caption=text,
                                     parse_mode="HTML", reply_markup=reply_markup)
            else:
                await bot.send_message(chat_id=user_id, text=text, parse_mode="HTML", reply_markup=reply_markup)
            return "delivered"
        except TelegramForbiddenError:
            await mark_blocked(db, user_id)
            return "blocked"
        except TelegramRetryAfter as e:
            if attempt < MAX_RETRIES - 1:
                logger.warning("Rate limit hit for user %d, waiting %ds", user_id, e.retry_after)
                await asyncio.sleep(e.retry_after)
            else:
                logger.warning("Rate limit hit for user %d on final attempt, giving up", user_id)
        except Exception as e:
            logger.error("Failed to send to user %d (attempt %d): %s", user_id, attempt + 1, e)
            if attempt < MAX_RETRIES - 1:
                await asyncio.sleep(2 ** attempt)
    return "error"
