"""Фоновая отправка отложенных SMS в рабочие часы пользователя."""

from __future__ import annotations

import asyncio
import logging
import random
from collections import defaultdict

from config import (
    PENDING_SMS_PAUSE_AFTER_SEND_SEC_MAX,
    PENDING_SMS_PAUSE_AFTER_SEND_SEC_MIN,
    PENDING_SMS_TICK_SEC,
    SMS_DAILY_LIMIT,
    effective_sms_resend_after_days,
)
from app.database.crud_counter import increment_global_counter
from app.database.crud_filter import get_filter_settings_dict
from app.database.crud_pending_sms import (
    STATUS_FAILED,
    STATUS_SENT,
    count_pending_sms,
    list_pending_sms,
    mark_pending_sms_status,
)
from app.database.crud_static import (
    add_sms_log,
    count_today_sent_sms,
    mark_phone_sent,
    try_reserve_phone_for_sms,
)
from app.notify import notify_user_and_maybe_admin
from app.schedule_utils import is_within_window
from spam_sms import send_sms

_log = logging.getLogger(__name__)

_worker_task: asyncio.Task | None = None
_limit_notified_date: str | None = None
_limit_notified_80: bool = False
_limit_notified_100: bool = False


def _empty_stats() -> dict:
    return {
        "sent": 0,
        "failed": 0,
        "skipped": 0,  # дубликаты / уже были в sent_phones
        "limited": 0,
        "outside": 0,
        "total": 0,
        "unique_phones": set(),
        "by_user": defaultdict(lambda: {
            "sent": 0,
            "failed": 0,
            "skipped": 0,
            "unique_phones": set(),
        }),
    }


def _reset_limit_flags_if_new_day() -> None:
    global _limit_notified_date, _limit_notified_80, _limit_notified_100
    from app.schedule_utils import now_in_bot_tz

    today = now_in_bot_tz().date().isoformat()
    if _limit_notified_date != today:
        _limit_notified_date = today
        _limit_notified_80 = False
        _limit_notified_100 = False


async def _notify_limit(user_id: int | None, today_sent: int) -> None:
    global _limit_notified_80, _limit_notified_100
    if SMS_DAILY_LIMIT <= 0:
        return
    _reset_limit_flags_if_new_day()
    threshold_80 = int(SMS_DAILY_LIMIT * 0.8)
    if (
        today_sent >= threshold_80
        and today_sent < SMS_DAILY_LIMIT
        and not _limit_notified_80
    ):
        _limit_notified_80 = True
        text = f"⚠️ Дневной лимит SMS на 80%: {today_sent}/{SMS_DAILY_LIMIT}."
        await notify_user_and_maybe_admin(user_id, text, also_admin=True)
    if today_sent >= SMS_DAILY_LIMIT and not _limit_notified_100:
        _limit_notified_100 = True
        text = (
            f"⏸ Дневной лимит SMS достигнут: {today_sent}/{SMS_DAILY_LIMIT}.\n"
            "Очередь pending приостановлена до завтра."
        )
        await notify_user_and_maybe_admin(user_id, text, also_admin=True)


def _record_stat(stats: dict, row, status: str) -> None:
    if status not in ("sent", "failed", "skipped", "limited", "outside"):
        return
    stats[status] = int(stats.get(status) or 0) + 1
    if status in ("sent", "failed", "skipped"):
        phone = (row.phone_number or "").strip()
        if phone:
            stats["unique_phones"].add(phone)
            u = stats["by_user"][int(row.user_id)]
            u[status] = int(u.get(status) or 0) + 1
            u["unique_phones"].add(phone)


def format_queue_summary(stats: dict, *, left: int, force: bool = False) -> str:
    """Текст итога по пакету отправки из очереди."""
    sent = int(stats.get("sent") or 0)
    failed = int(stats.get("failed") or 0)
    skipped = int(stats.get("skipped") or 0)
    unique_n = len(stats.get("unique_phones") or ())
    processed = sent + failed + skipped

    if failed == 0 and sent > 0:
        title = "✅ Все SMS из очереди успешно отправлены"
    elif sent == 0 and failed > 0:
        title = "❌ SMS из очереди не удалось отправить"
    elif processed == 0:
        title = "ℹ️ Очередь SMS: нечего отправлять"
    else:
        title = "⚠️ Очередь SMS обработана частично"

    source = "ручная отправка" if force else "авто (окно расписания)"
    return (
        f"{title}\n"
        f"<i>{source}</i>\n\n"
        f"📤 Успешно: <b>{sent}</b>\n"
        f"❌ Ошибки: <b>{failed}</b>\n"
        f"🔁 Дубликаты / уже были: <b>{skipped}</b>\n"
        f"📞 Уникальных номеров: <b>{unique_n}</b>\n"
        f"📦 Обработано записей: <b>{processed}</b>\n"
        f"⏳ Осталось в очереди: <b>{left}</b>"
    )


async def _ensure_autoinfo_after_pending_sent(row) -> None:
    """После успешной SMS из очереди — запись в статистику парсинга (AutoInfo)."""
    ad_id = str(row.ad_id or "").strip()
    if not ad_id:
        return
    from sqlalchemy.exc import IntegrityError

    from app.database.crud import add_autoinfo, update_autoinfo_phones

    phones = [row.phone_number] if row.phone_number else []
    try:
        await add_autoinfo(
            ad_id=ad_id,
            name=row.ad_name or "Неизвестно",
            year=0,
            usd_price=0,
            publish_time="",
            all_phones=phones,
            link=row.link or "",
            spam=False,
        )
        _log.info("AutoInfo из pending: ad_id=%s phone=%s", ad_id, row.phone_number)
    except IntegrityError:
        await update_autoinfo_phones(ad_id, phones)
    except Exception:
        _log.exception("Не удалось сохранить AutoInfo для pending ad_id=%s", ad_id)


async def _flush_one(row, *, force: bool = False) -> str:
    """
    Отправляет одну pending SMS.
    force=True — игнорировать окно рабочих часов.
    Возвращает: sent | failed | skipped | limited | outside
    Без per-SMS уведомлений — итог шлётся пакетом.
    """
    if not force:
        settings = await get_filter_settings_dict(row.user_id) or {}
        if not is_within_window(
            settings.get("work_from"),
            settings.get("work_to"),
            settings.get("work_days"),
        ):
            return "outside"

    if SMS_DAILY_LIMIT > 0:
        today_sent = await count_today_sent_sms()
        await _notify_limit(row.user_id, today_sent)
        if today_sent >= SMS_DAILY_LIMIT:
            return "limited"

    reserved = await try_reserve_phone_for_sms(
        phone_number=row.phone_number,
        ad_id=row.ad_id,
        ad_name=row.ad_name,
        resend_after_days=effective_sms_resend_after_days(),
    )
    if not reserved:
        await mark_pending_sms_status(row.id, STATUS_SENT)
        _log.info(
            "Pending #%s phone=%s уже в sent_phones — помечаем sent",
            row.id,
            row.phone_number,
        )
        return "skipped"

    result = await send_sms([row.phone_number])
    link = row.link or ""
    ad_name = row.ad_name or "Неизвестно"

    if result.get("success"):
        await mark_phone_sent(
            phone_number=row.phone_number,
            ad_id=row.ad_id,
            ad_name=ad_name,
            is_successful=True,
        )
        await increment_global_counter(ok=True, step=int(result.get("sent_count") or 1))
        await add_sms_log(
            ad_name=ad_name,
            link=link or "pending://flush",
            phones=[row.phone_number],
            is_sent=True,
        )
        await mark_pending_sms_status(row.id, STATUS_SENT)
        await _ensure_autoinfo_after_pending_sent(row)
        if SMS_DAILY_LIMIT > 0:
            after = await count_today_sent_sms()
            await _notify_limit(row.user_id, after)
        return "sent"

    await mark_phone_sent(
        phone_number=row.phone_number,
        ad_id=row.ad_id,
        ad_name=ad_name,
        is_successful=False,
    )
    await increment_global_counter(ok=False, step=1)
    await add_sms_log(
        ad_name=ad_name,
        link=link or "pending://flush",
        phones=[row.phone_number],
        is_sent=False,
    )
    await mark_pending_sms_status(row.id, STATUS_FAILED)
    return "failed"


_flush_lock = asyncio.Lock()


async def _notify_queue_summary(stats: dict, *, force: bool = False) -> None:
    from app.notify import notify_admin, notify_chat

    processed = (
        int(stats.get("sent") or 0)
        + int(stats.get("failed") or 0)
        + int(stats.get("skipped") or 0)
    )
    if processed <= 0:
        return

    left = await count_pending_sms()
    text = format_queue_summary(stats, left=left, force=force)
    by_user = stats.get("by_user") or {}
    sent_to: set[int] = set()

    for uid in by_user:
        uid_i = int(uid)
        if await notify_chat(uid_i, text):
            sent_to.add(uid_i)

    # админам — без дубля, если они уже получили как владельцы очереди
    from config import ADMIN_USER_IDS

    for admin_id in sorted(ADMIN_USER_IDS or ()):
        if int(admin_id) in sent_to:
            continue
        await notify_chat(int(admin_id), text)

    if not by_user:
        await notify_admin(text)


async def flush_pending_sms(
    *,
    force: bool = False,
    max_items: int = 500,
    pause_sec: float | None = None,
    notify_summary: bool = False,
) -> dict:
    """
    Сливает очередь pending до пустоты / лимита / выхода из окна.
    force=True — без проверки рабочих часов.
    pause_sec=None — PENDING_SMS_PAUSE после sent; число — фиксированная пауза;
    0 — без паузы.
    notify_summary=True — одно итоговое сообщение в Telegram.
    """
    stats = _empty_stats()
    async with _flush_lock:
        remaining_budget = max(1, int(max_items))
        while remaining_budget > 0:
            batch = min(50, remaining_budget)
            rows = await list_pending_sms(limit=batch)
            if not rows:
                break

            progressed = False
            hit_limited = False
            for row in rows:
                status = await _flush_one(row, force=force)
                stats["total"] += 1
                remaining_budget -= 1
                _record_stat(stats, row, status)

                if status == "limited":
                    hit_limited = True
                    break
                if status == "outside" and not force:
                    continue
                if status in ("sent", "failed", "skipped"):
                    progressed = True
                if status == "sent":
                    if pause_sec is None:
                        pause = random.randint(
                            PENDING_SMS_PAUSE_AFTER_SEND_SEC_MIN,
                            PENDING_SMS_PAUSE_AFTER_SEND_SEC_MAX,
                        )
                    else:
                        pause = max(0, float(pause_sec))
                    if pause > 0:
                        _log.info(
                            "Pending SMS pause %ss after send (force=%s)",
                            pause,
                            force,
                        )
                        await asyncio.sleep(pause)

            if hit_limited:
                break
            if not progressed and not force:
                # все строки вне окна — ждём следующего тика
                break
            if not progressed and force:
                break

    # для JSON-friendly ответа (set -> count)
    result = {
        "sent": int(stats["sent"]),
        "failed": int(stats["failed"]),
        "skipped": int(stats["skipped"]),
        "limited": int(stats["limited"]),
        "outside": int(stats["outside"]),
        "total": int(stats["total"]),
        "unique": len(stats["unique_phones"]),
        "unique_phones": stats["unique_phones"],
        "by_user": stats["by_user"],
    }
    if notify_summary:
        await _notify_queue_summary(stats, force=force)
    return result


async def pending_sms_loop() -> None:
    tick = int(PENDING_SMS_TICK_SEC)
    _log.info(
        "Pending SMS worker started (tick=%ss pause=%s-%ss)",
        tick,
        PENDING_SMS_PAUSE_AFTER_SEND_SEC_MIN,
        PENDING_SMS_PAUSE_AFTER_SEND_SEC_MAX,
    )
    await asyncio.sleep(5)
    while True:
        try:
            pending = await count_pending_sms()
            if pending > 0:
                stats = await flush_pending_sms(
                    force=False,
                    max_items=max(pending, 50),
                    notify_summary=True,
                )
                processed = stats["sent"] + stats["failed"] + stats["skipped"]
                if processed:
                    _log.info(
                        "Pending auto-flush done: sent=%s failed=%s skipped=%s unique=%s",
                        stats["sent"],
                        stats["failed"],
                        stats["skipped"],
                        stats["unique"],
                    )
        except asyncio.CancelledError:
            break
        except Exception:
            _log.exception("Pending SMS tick failed")
        try:
            await asyncio.sleep(tick)
        except asyncio.CancelledError:
            break
    _log.info("Pending SMS worker stopped")


def start_pending_sms_worker() -> asyncio.Task:
    global _worker_task
    if _worker_task is not None and not _worker_task.done():
        return _worker_task
    _worker_task = asyncio.create_task(pending_sms_loop(), name="pending_sms")
    return _worker_task
