import asyncio
import os
import time

from collections import deque
from datetime import datetime

import aiofiles
import pytz

from aiogram import Bot
from aiogram.exceptions import (
    TelegramBadRequest,
    TelegramForbiddenError,
    TelegramRetryAfter,
)
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup
from sqlalchemy.ext.asyncio import AsyncSession

from database import create_blocked_user
from handlers.tariffs.tariff_display import get_key_tariff_display
from handlers.utils import format_hours, format_minutes, get_russian_month
from logger import logger


moscow_tz = pytz.timezone("Europe/Moscow")


class NotificationRateLimiter:
    def __init__(self, max_rate: int = 35, window: float = 1.0) -> None:
        self.max_rate = max_rate
        self.window = window
        self.send_times = deque()
        self.lock = asyncio.Lock()

    def _clean_old_timestamps(self, current_time: float):
        cutoff_time = current_time - self.window
        while self.send_times and self.send_times[0] <= cutoff_time:
            self.send_times.popleft()

    async def acquire(self):
        async with self.lock:
            while True:
                now = time.time()
                self._clean_old_timestamps(now)
                if len(self.send_times) < self.max_rate:
                    self.send_times.append(now)
                    return
                oldest_timestamp = self.send_times[0]
                time_to_wait = (oldest_timestamp + self.window) - now
                if time_to_wait > 0:
                    await asyncio.sleep(time_to_wait + 0.001)


class NotificationMessage:
    def __init__(self, tg_id: int, text: str, photo: str | None = None, keyboard=None) -> None:
        self.tg_id = tg_id
        self.text = text
        self.photo = photo
        self.keyboard = keyboard
        self.retry_after = None
        self.attempts = 0


class FastNotificationSender:
    def __init__(self, bot: Bot, session: AsyncSession | None, messages_per_second: int = 35) -> None:
        self.bot = bot
        self.session = session
        self.rate_limiter = NotificationRateLimiter(max_rate=messages_per_second)
        self.blocked_users = set()
        self.queue = asyncio.Queue()
        self.delayed_queue = asyncio.Queue()
        self.results = []
        self.total_sent = 0
        self.is_running = False

    async def _send_single_message(self, msg: NotificationMessage) -> bool:
        try:
            await self.rate_limiter.acquire()

            if msg.photo:
                photo_path = os.path.join("img", msg.photo)
                if os.path.isfile(photo_path):
                    async with aiofiles.open(photo_path, "rb") as f:
                        image_data = await f.read()
                    buffered_photo = BufferedInputFile(image_data, filename=msg.photo)
                    await self.bot.send_photo(
                        chat_id=msg.tg_id, photo=buffered_photo, caption=msg.text, reply_markup=msg.keyboard
                    )
                else:
                    await self.bot.send_message(chat_id=msg.tg_id, text=msg.text, reply_markup=msg.keyboard)
            else:
                await self.bot.send_message(chat_id=msg.tg_id, text=msg.text, reply_markup=msg.keyboard)
            return True

        except TelegramRetryAfter as e:
            msg.retry_after = e.retry_after
            msg.attempts += 1
            await self.delayed_queue.put(msg)
            return False

        except TelegramForbiddenError:
            self.blocked_users.add(msg.tg_id)
            return False

        except TelegramBadRequest as e:
            if "chat not found" in str(e).lower():
                self.blocked_users.add(msg.tg_id)
            return False

        except Exception:
            return False

    async def _process_delayed_messages(self):
        while self.is_running:
            try:
                if not self.delayed_queue.empty():
                    msg = await asyncio.wait_for(self.delayed_queue.get(), timeout=0.1)
                    if msg.retry_after:
                        await asyncio.sleep(msg.retry_after)
                        msg.retry_after = None
                    if msg.attempts < 3:
                        await self.queue.put(msg)
                    else:
                        self.results.append(False)
                else:
                    await asyncio.sleep(0.1)
            except TimeoutError:
                continue
            except Exception:
                await asyncio.sleep(0.1)

    async def _worker(self):
        while self.is_running:
            try:
                msg = await asyncio.wait_for(self.queue.get(), timeout=0.1)
                success = await self._send_single_message(msg)
                if success:
                    self.total_sent += 1
                    self.results.append(True)
                elif msg.attempts == 0:
                    self.results.append(False)
                self.queue.task_done()
            except TimeoutError:
                continue
            except Exception:
                await asyncio.sleep(0.1)

    async def _save_blocked_users(self):
        if not self.blocked_users or not self.session:
            return
        try:
            from sqlalchemy.dialects.postgresql import insert
            from database.models import BlockedUser

            values = [{"tg_id": tg_id} for tg_id in self.blocked_users]
            stmt = insert(BlockedUser).values(values).on_conflict_do_nothing(index_elements=[BlockedUser.tg_id])
            await self.session.execute(stmt)
            await self.session.commit()
            logger.info(f"📝 Добавлено {len(self.blocked_users)} пользователей в blocked_users")
        except Exception as e:
            logger.error(f"❌ Ошибка при сохранении заблокированных пользователей: {e}")
            await self.session.rollback()

    async def send_all(self, messages: list[dict], workers: int = 15) -> list[bool]:
        if not messages:
            return []

        self.is_running = True
        self.results = []
        self.total_sent = 0
        self.blocked_users = set()
        start_time = time.time()

        for msg_data in messages:
            msg = NotificationMessage(
                tg_id=msg_data["tg_id"],
                text=msg_data["text"],
                photo=msg_data.get("photo"),
                keyboard=msg_data.get("keyboard"),
            )
            await self.queue.put(msg)

        worker_tasks = [asyncio.create_task(self._worker()) for _ in range(workers)]
        delayed_task = asyncio.create_task(self._process_delayed_messages())

        await self.queue.join()

        await asyncio.sleep(0.5)
        while not self.delayed_queue.empty():
            await asyncio.sleep(0.5)

        self.is_running = False

        for task in worker_tasks:
            task.cancel()
        delayed_task.cancel()

        await asyncio.gather(*worker_tasks, delayed_task, return_exceptions=True)
        await self._save_blocked_users()

        duration = time.time() - start_time
        speed = self.total_sent / duration if duration > 0 else 0
        logger.info(f"📨 Уведомления: {self.total_sent}/{len(messages)} за {duration:.1f}s ({speed:.1f} msg/s)")

        return self.results


async def send_messages_with_limit(
    bot: Bot,
    messages: list[dict],
    session: AsyncSession = None,
    source_file: str = None,
    messages_per_second: int = 35,
):
    sender = FastNotificationSender(bot, session, messages_per_second)
    return await sender.send_all(messages)


async def try_add_blocked_user(tg_id: int, session: AsyncSession, source_file: str | None):
    if source_file == "special_notifications" and session:
        try:
            await create_blocked_user(session, tg_id)
            logger.info(f"Пользователь {tg_id} добавлен в blocked_users.")
        except Exception as e:
            logger.warning(f"Не удалось добавить {tg_id} в blocked_users: {e}")


def rate_limited_send(func):
    async def wrapper(*args, **kwargs):
        while True:
            try:
                return await func(*args, **kwargs)
            except TelegramRetryAfter as e:
                retry_in = int(e.retry_after) + 1
                logger.warning(f"⚠️ Flood control: повтор через {retry_in} сек.")
                await asyncio.sleep(retry_in)
            except TelegramForbiddenError:
                tg_id = kwargs.get("tg_id") or args[1]
                logger.warning(f"🚫 Бот заблокирован пользователем {tg_id}.")
                return False
            except TelegramBadRequest:
                tg_id = kwargs.get("tg_id") or args[1]
                logger.warning(f"🚫 Чат не найден для пользователя {tg_id}.")
                return False
            except Exception as e:
                tg_id = kwargs.get("tg_id") or args[1]
                logger.error(f"❌ Ошибка отправки сообщения пользователю {tg_id}: {e}")
                return False

    return wrapper


async def send_notification(
    bot: Bot,
    tg_id: int,
    image_filename: str | None,
    caption: str,
    keyboard: InlineKeyboardMarkup | None = None,
) -> bool:
    if image_filename is None:
        return await _send_text_notification(bot, tg_id, caption, keyboard)

    photo_path = os.path.join("img", image_filename)
    if os.path.isfile(photo_path):
        return await _send_photo_notification(bot, tg_id, photo_path, image_filename, caption, keyboard)
    else:
        logger.warning(f"Файл с изображением не найден: {photo_path}")
        return await _send_text_notification(bot, tg_id, caption, keyboard)


@rate_limited_send
async def _send_photo_notification(
    bot: Bot,
    tg_id: int,
    photo_path: str,
    image_filename: str,
    caption: str,
    keyboard: InlineKeyboardMarkup | None = None,
) -> bool:
    try:
        async with aiofiles.open(photo_path, "rb") as image_file:
            image_data = await image_file.read()
        buffered_photo = BufferedInputFile(image_data, filename=image_filename)
        await bot.send_photo(tg_id, buffered_photo, caption=caption, reply_markup=keyboard)
        return True
    except (TelegramForbiddenError, TelegramBadRequest):
        return False
    except Exception as e:
        logger.error(f"Ошибка отправки фото для пользователя {tg_id}: {e}")
        return await _send_text_notification(bot, tg_id, caption, keyboard)


@rate_limited_send
async def _send_text_notification(
    bot: Bot,
    tg_id: int,
    caption: str,
    keyboard: InlineKeyboardMarkup | None = None,
) -> bool:
    try:
        await bot.send_message(tg_id, caption, reply_markup=keyboard)
        return True
    except (TelegramForbiddenError, TelegramBadRequest):
        return False
    except Exception as e:
        logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}")
        return False


async def prepare_key_expiry_data(key, session: AsyncSession, current_time: int) -> dict:
    """Готовит данные об истечении подписки для уведомлений."""
    if isinstance(key, dict):
        expiry_timestamp = key.get("expiry_time")
        email = key.get("email") or ""
        record = dict(key)
    else:
        expiry_timestamp = getattr(key, "expiry_time", None)
        email = getattr(key, "email", "") or ""
        record = {
            "tariff_id": getattr(key, "tariff_id", None),
            "server_id": getattr(key, "server_id", None),
            "client_id": getattr(key, "client_id", None),
            "selected_device_limit": getattr(key, "selected_device_limit", None),
            "selected_traffic_limit": getattr(key, "selected_traffic_limit", None),
        }

    if not expiry_timestamp:
        return {
            "hours_left_formatted": "",
            "formatted_expiry_date": "",
            "tariff_name": "—",
            "tariff_details": "",
        }

    delta_ms = max(0, expiry_timestamp - current_time)
    total_minutes = delta_ms // (60 * 1000)
    hours_left = total_minutes // 60
    minutes_left = total_minutes % 60

    if hours_left > 0 or minutes_left > 0:
        parts = []
        if hours_left > 0:
            parts.append(format_hours(hours_left))
        if minutes_left > 0:
            parts.append(format_minutes(minutes_left))
        hours_left_formatted = f"⏳ Осталось времени: {' '.join(parts)}"
    else:
        hours_left_formatted = "⏳ Последний день подписки!"

    expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz)
    month_name = get_russian_month(expiry_datetime)
    formatted_expiry_date = expiry_datetime.strftime(f"%d {month_name} %Y, %H:%M (МСК)")

    tariff_name = "—"
    subgroup_title = ""
    traffic_limit_gb = 0
    device_limit = 0

    try:
        name, subgroup_title, traffic_limit_gb, device_limit, _ = await get_key_tariff_display(
            session=session,
            key_record=record,
        )
        if name:
            tariff_name = name
    except Exception as error:
        logger.warning(f"[NOTIFY] Ошибка при получении тарифных лимитов для {email}: {error}")

    traffic_text = "безлимит" if traffic_limit_gb == 0 else f"{traffic_limit_gb} ГБ"
    devices_text = "безлимит" if device_limit == 0 else str(device_limit)

    lines = []
    if subgroup_title:
        lines.append(subgroup_title)
    lines.append(f"Трафик: {traffic_text}")
    lines.append(f"Устройств: {devices_text}")
    tariff_details = "\n" + "\n".join(lines) if lines else ""

    return {
        "hours_left_formatted": hours_left_formatted,
        "formatted_expiry_date": formatted_expiry_date,
        "tariff_name": tariff_name,
        "tariff_details": tariff_details,
    }
