from __future__ import annotations

import asyncio
import logging
from datetime import datetime
from pathlib import Path

from aiogram import Bot
from aiogram.types import FSInputFile
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from app.db.base import utcnow
from app.db.models import SubscriptionNotification
from app.services.bot_config_service import BotConfigSnapshot, StaticBotConfigService
from app.services.sync_service import SyncService
from app.utils.formatters import format_datetime_with_days_left

logger = logging.getLogger(__name__)

NOTIFICATION_TYPE_EXPIRE_SOON = "EXPIRE_SOON"


class SubscriptionNotificationService:
    def __init__(
        self,
        *,
        session_factory: async_sessionmaker[AsyncSession],
        sync_service: SyncService,
        config_service: StaticBotConfigService,
        project_root: Path,
    ) -> None:
        self._session_factory = session_factory
        self._sync_service = sync_service
        self._config_service = config_service
        self._project_root = project_root

    async def _get_config(self) -> BotConfigSnapshot:
        return await self._config_service.get_snapshot()

    def _resolve_image_path(self, config: BotConfigSnapshot) -> Path | None:
        if not config.bot_start_image_enabled:
            return None

        raw_path = config.bot_start_image_path.strip()
        default_path = self._project_root / "assets" / "main.png"

        if not raw_path:
            return default_path if default_path.is_file() else None

        image_path = Path(raw_path).expanduser()
        if not image_path.is_absolute():
            image_path = self._project_root / image_path

        return image_path if image_path.is_file() else None

    async def _already_notified(
        self,
        session: AsyncSession,
        *,
        remnawave_user_id: int,
        notification_type: str,
        expire_at_snapshot: datetime,
    ) -> bool:
        existing = await session.scalar(
            select(SubscriptionNotification.id).where(
                SubscriptionNotification.remnawave_user_id == remnawave_user_id,
                SubscriptionNotification.notification_type == notification_type,
                SubscriptionNotification.expire_at_snapshot == expire_at_snapshot,
            )
        )
        return existing is not None

    async def _record_notification(
        self,
        session: AsyncSession,
        *,
        remnawave_user_id: int,
        telegram_id: int,
        notification_type: str,
        expire_at_snapshot: datetime,
    ) -> None:
        session.add(
            SubscriptionNotification(
                remnawave_user_id=remnawave_user_id,
                telegram_id=telegram_id,
                notification_type=notification_type,
                expire_at_snapshot=expire_at_snapshot,
                sent_at=utcnow(),
            )
        )

    async def check_and_notify(self, bot: Bot) -> int:
        config = await self._get_config()
        if not config.notification_enabled:
            return 0

        days_list = config.notification_days_list
        if not days_list:
            return 0

        total_sent = 0
        image_path = self._resolve_image_path(config)

        for days_before in days_list:
            expiring = await self._sync_service.get_expiring_users(
                days_before=days_before,
                tolerance_hours=max(config.notification_check_interval_hours, 1),
            )

            for rw_user_id, telegram_id, expire_at in expiring:
                if telegram_id is None:
                    continue

                try:
                    sent = await self._send_notification(
                        bot,
                        rw_user_id=rw_user_id,
                        telegram_id=telegram_id,
                        expire_at=expire_at,
                        days_before=days_before,
                        brand_name=config.bot_brand_name,
                        image_path=image_path,
                    )
                    if sent:
                        total_sent += 1
                except Exception:
                    logger.exception(
                        "Failed to send expiration notification to telegram_id=%s",
                        telegram_id,
                    )

        return total_sent

    async def _send_notification(
        self,
        bot: Bot,
        *,
        rw_user_id: int,
        telegram_id: int,
        expire_at: datetime,
        days_before: int,
        brand_name: str,
        image_path: Path | None,
    ) -> bool:
        notification_type = f"{NOTIFICATION_TYPE_EXPIRE_SOON}_{days_before}d"

        async with self._session_factory() as session:
            if await self._already_notified(
                session,
                remnawave_user_id=rw_user_id,
                notification_type=notification_type,
                expire_at_snapshot=expire_at,
            ):
                return False

            text = self._build_notification_text(
                brand_name=brand_name,
                expire_at=expire_at,
                days_before=days_before,
            )

            try:
                if image_path is not None:
                    await bot.send_photo(
                        chat_id=telegram_id,
                        photo=FSInputFile(str(image_path)),
                        caption=text,
                    )
                else:
                    await bot.send_message(
                        chat_id=telegram_id,
                        text=text,
                    )
            except Exception:
                logger.warning(
                    "Cannot send notification to telegram_id=%s (blocked or unavailable)",
                    telegram_id,
                )
                return False

            await self._record_notification(
                session,
                remnawave_user_id=rw_user_id,
                telegram_id=telegram_id,
                notification_type=notification_type,
                expire_at_snapshot=expire_at,
            )
            await session.commit()

        return True

    @staticmethod
    def _build_notification_text(
        *,
        brand_name: str,
        expire_at: datetime,
        days_before: int,
    ) -> str:
        import html as html_module

        if days_before <= 1:
            urgency = "⚠️ Ваша подписка истекает <b>завтра</b>!"
        else:
            urgency = f"⏰ Ваша подписка истекает через <b>{days_before} дн.</b>"

        lines = [
            f"<b>{html_module.escape(brand_name)}</b>",
            "",
            urgency,
            "",
            (
                "<blockquote>"
                f"Дата истечения: <b>{format_datetime_with_days_left(expire_at)}</b>\n\n"
                "Чтобы продлить доступ, откройте бот и выберите тариф."
                "</blockquote>"
            ),
            "",
            "<i>Нажмите /start чтобы открыть меню и продлить подписку.</i>",
        ]
        return "\n".join(lines)

    async def run_periodic(self, bot: Bot, *, default_interval_hours: int = 6) -> None:
        logger.info("Subscription notification periodic task started")
        while True:
            try:
                config = await self._get_config()
                interval_hours = max(config.notification_check_interval_hours, 1)
            except Exception:
                interval_hours = max(default_interval_hours, 1)

            try:
                total = await self.check_and_notify(bot)
                if total > 0:
                    logger.info("Sent %d subscription expiration notifications", total)
            except Exception:
                logger.exception("Error in subscription notification check")

            await asyncio.sleep(interval_hours * 3600)
