from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Optional

from aiogram import Bot
from sqlalchemy.orm import sessionmaker

from bot.middlewares.i18n import JsonI18n
from bot.services.email_auth_service import EmailAuthService
from bot.services.notification_service import NotificationService
from config.settings import Settings
from db.dal import message_log_dal, subscription_dal, support_dal, user_dal
from db.models import SupportTicket, SupportTicketMessage, User

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class AdminNotificationDecision:
    send_telegram: bool
    send_email: bool


def _as_utc(value: Optional[datetime]) -> Optional[datetime]:
    if value is None:
        return None
    if value.tzinfo is None:
        return value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)


def _notification_due(
    last_sent_at: Optional[datetime],
    *,
    now: datetime,
    cooldown_seconds: int,
) -> bool:
    last_sent_at = _as_utc(last_sent_at)
    if last_sent_at is None:
        return True
    cooldown = max(0, int(cooldown_seconds or 0))
    if cooldown <= 0:
        return True
    return (now - last_sent_at).total_seconds() >= cooldown


def _support_admin_notification_decision(
    ticket: SupportTicket,
    settings: Settings,
    *,
    now: Optional[datetime] = None,
    admin_email_notifications_enabled: Optional[bool] = None,
) -> AdminNotificationDecision:
    now = _as_utc(now) or datetime.now(timezone.utc)
    unread_count = max(0, int(getattr(ticket, "unread_admin_count", 0) or 0))
    if unread_count <= 0:
        return AdminNotificationDecision(send_telegram=False, send_email=False)

    first_unread = unread_count <= 1
    send_telegram = first_unread or _notification_due(
        getattr(ticket, "admin_last_notified_at", None),
        now=now,
        cooldown_seconds=getattr(settings, "SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS", 300),
    )
    if admin_email_notifications_enabled is None:
        admin_email_notifications_enabled = bool(
            getattr(settings, "SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED", False)
        )
    send_email = bool(admin_email_notifications_enabled) and (
        first_unread
        or _notification_due(
            getattr(ticket, "admin_last_emailed_at", None),
            now=now,
            cooldown_seconds=getattr(settings, "SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS", 1800),
        )
    )
    return AdminNotificationDecision(send_telegram=send_telegram, send_email=send_email)


def _format_support_remaining(seconds: int, lang: str) -> str:
    if seconds <= 0:
        return "Subscription inactive" if lang == "en" else "Подписка не активна"
    days, rem = divmod(seconds, 86400)
    hours, rem = divmod(rem, 3600)
    minutes = rem // 60
    if lang == "en":
        if days > 0:
            return f"{days} d. {hours} h."
        if hours > 0:
            return f"{hours} h. {minutes} min."
        return f"{max(1, minutes)} min."
    if days > 0:
        return f"{days} д. {hours} ч."
    if hours > 0:
        return f"{hours} ч. {minutes} мин."
    return f"{max(1, minutes)} мин."


class TicketForbidden(PermissionError):
    pass


class TicketRateLimited(RuntimeError):
    pass


class TicketNotFound(LookupError):
    pass


class SupportService:
    def __init__(
        self,
        session_factory: sessionmaker,
        settings: Settings,
        bot: Bot,
        i18n: Optional[JsonI18n],
        notification_service: Optional[NotificationService] = None,
        email_auth_service: Optional[EmailAuthService] = None,
    ):
        self.session_factory = session_factory
        self.settings = settings
        self.bot = bot
        self.i18n = i18n
        self.email_auth_service = email_auth_service or EmailAuthService(settings, i18n)
        self.notification_service = notification_service or NotificationService(
            bot,
            settings,
            i18n,
            session_factory=session_factory,
            email_auth_service=self.email_auth_service,
        )

    @staticmethod
    def _schedule_notification(coro, error_message: str, *error_args: Any) -> None:
        async def _runner():
            try:
                await coro
            except Exception:
                logger.exception(error_message, *error_args)

        asyncio.create_task(_runner(), name="support-notification")

    async def _ensure_user_allowed(self, session, user_id: int) -> User:
        user = await user_dal.get_user_by_id(session, user_id)
        if not user or user.is_banned or not self.settings.SUPPORT_TICKETS_ENABLED:
            raise TicketForbidden("ticket_forbidden")
        return user

    async def create_ticket(
        self,
        user_id: int,
        subject: str,
        category: str,
        priority: str,
        first_message_body: str,
    ) -> SupportTicket:
        async with self.session_factory() as session:
            user = await self._ensure_user_allowed(session, user_id)
            limit = max(0, int(self.settings.SUPPORT_TICKET_RATE_LIMIT_PER_HOUR or 0))
            if limit:
                recent = await support_dal.count_recent_tickets_for_user(session, user_id, 3600)
                if recent >= limit:
                    raise TicketRateLimited("ticket_rate_limited")
            ticket = await support_dal.create_ticket(
                session,
                user_id,
                subject[: self.settings.SUPPORT_TICKET_MAX_SUBJECT_LENGTH],
                category,
                priority,
                first_message_body[: self.settings.SUPPORT_TICKET_MAX_BODY_LENGTH],
            )
            snapshot = await self.build_user_snapshot(user, session=session)
            await session.commit()

        try:
            await self.notification_service.notify_new_support_ticket(
                ticket,
                user,
                first_message_body,
                snapshot,
            )
        except Exception:
            logger.exception("Failed to notify about support ticket %s", ticket.ticket_id)
        return ticket

    async def reply_as_user(
        self,
        user_id: int,
        ticket_id: int,
        body: str,
    ) -> tuple[SupportTicket, SupportTicketMessage]:
        async with self.session_factory() as session:
            user = await self._ensure_user_allowed(session, user_id)
            ticket, _messages = await support_dal.get_ticket(session, ticket_id)
            if not ticket or ticket.user_id != user_id:
                raise TicketNotFound("not_found")
            message = await support_dal.add_message(
                session,
                ticket_id,
                "user",
                user_id,
                body[: self.settings.SUPPORT_TICKET_MAX_BODY_LENGTH],
            )
            if message is None:
                raise TicketNotFound("not_found")
            await session.refresh(ticket)
            notification_at = datetime.now(timezone.utc)
            admin_email_notifications_enabled = (
                await self.notification_service.support_admin_email_notifications_enabled()
            )
            notification_decision = _support_admin_notification_decision(
                ticket,
                self.settings,
                now=notification_at,
                admin_email_notifications_enabled=admin_email_notifications_enabled,
            )
            await support_dal.record_admin_notification(
                session,
                ticket_id,
                notified_at=notification_at if notification_decision.send_telegram else None,
                emailed_at=notification_at if notification_decision.send_email else None,
            )
            snapshot = await self.build_user_snapshot(user, session=session)
            await session.commit()

        if notification_decision.send_telegram or notification_decision.send_email:
            self._schedule_notification(
                self.notification_service.notify_support_user_reply(
                    ticket,
                    message,
                    user,
                    snapshot,
                    unread_count=int(ticket.unread_admin_count or 0),
                    send_telegram=notification_decision.send_telegram,
                    send_email=notification_decision.send_email,
                ),
                "Failed to notify about support user reply %s",
                ticket_id,
            )
        return ticket, message

    async def reply_as_admin(
        self,
        admin_id: int,
        ticket_id: int,
        body: str,
        *,
        is_internal_note: bool = False,
    ) -> tuple[SupportTicket, SupportTicketMessage]:
        async with self.session_factory() as session:
            ticket, _messages = await support_dal.get_ticket(
                session,
                ticket_id,
                include_internal=True,
            )
            if not ticket:
                raise TicketNotFound("not_found")
            user = await user_dal.get_user_by_id(session, ticket.user_id)
            message = await support_dal.add_message(
                session,
                ticket_id,
                "admin",
                admin_id,
                body[: self.settings.SUPPORT_TICKET_MAX_BODY_LENGTH],
                is_internal_note=is_internal_note,
            )
            if message is None:
                raise TicketNotFound("not_found")
            await support_dal.mark_read(session, ticket_id, "admin")
            if not is_internal_note:
                await message_log_dal.create_message_log_no_commit(
                    session,
                    {
                        "user_id": admin_id,
                        "event_type": "support_admin_reply",
                        "content": f"Support ticket #{ticket_id} reply",
                        "is_admin_event": True,
                        "target_user_id": ticket.user_id,
                    },
                )
            await session.refresh(ticket)
            await session.commit()

        if user and not is_internal_note:
            self._schedule_notification(
                self.notification_service.notify_support_admin_reply(ticket, message, user),
                "Failed to notify user about support admin reply %s",
                ticket_id,
            )
        return ticket, message

    async def change_status(self, admin_id: int, ticket_id: int, status: str) -> SupportTicket:
        return await self._update_and_audit(admin_id, ticket_id, status=status)

    async def change_priority(self, admin_id: int, ticket_id: int, priority: str) -> SupportTicket:
        return await self._update_and_audit(admin_id, ticket_id, priority=priority)

    async def change_category(self, admin_id: int, ticket_id: int, category: str) -> SupportTicket:
        return await self._update_and_audit(admin_id, ticket_id, category=category)

    async def assign_admin(
        self,
        admin_id: int,
        ticket_id: int,
        assigned_admin_id: Optional[int],
    ) -> SupportTicket:
        return await self._update_and_audit(
            admin_id,
            ticket_id,
            assigned_admin_id=assigned_admin_id,
        )

    async def close_ticket(self, admin_id: int, ticket_id: int) -> SupportTicket:
        ticket = await self._update_and_audit(
            admin_id,
            ticket_id,
            status="closed",
            closed_by_admin_id=admin_id,
        )
        async with self.session_factory() as session:
            user = await user_dal.get_user_by_id(session, ticket.user_id)
        if user:
            try:
                await self.notification_service.notify_support_ticket_closed(ticket, user, admin_id)
            except Exception:
                logger.exception("Failed to notify user about support close %s", ticket_id)
        return ticket

    async def _update_and_audit(
        self,
        admin_id: int,
        ticket_id: int,
        **updates: Any,
    ) -> SupportTicket:
        async with self.session_factory() as session:
            ticket = await support_dal.update_ticket(session, ticket_id, **updates)
            if not ticket:
                raise TicketNotFound("not_found")
            await message_log_dal.create_message_log_no_commit(
                session,
                {
                    "user_id": admin_id,
                    "event_type": "support_ticket_update",
                    "content": f"Support ticket #{ticket_id}: {updates}",
                    "is_admin_event": True,
                    "target_user_id": ticket.user_id,
                },
            )
            await session.commit()
            return ticket

    async def mark_read_as_user(self, user_id: int, ticket_id: int) -> None:
        async with self.session_factory() as session:
            ticket, _messages = await support_dal.get_ticket(session, ticket_id)
            if not ticket or ticket.user_id != user_id:
                raise TicketNotFound("not_found")
            await support_dal.mark_read(session, ticket_id, "user")
            await session.commit()

    async def mark_read_as_admin(self, ticket_id: int) -> None:
        async with self.session_factory() as session:
            await support_dal.mark_read(session, ticket_id, "admin")
            await session.commit()

    async def build_user_snapshot(self, user: User, *, session=None) -> dict:
        owns_session = session is None
        if owns_session:
            session = self.session_factory()
            await session.__aenter__()
        try:
            sub = None
            if getattr(user, "panel_user_uuid", None):
                sub = await subscription_dal.get_active_subscription_by_user_id(
                    session,
                    int(user.user_id),
                    user.panel_user_uuid,
                )
            lang = (
                getattr(user, "language_code", None) or self.settings.DEFAULT_LANGUAGE or "ru"
            ).split("-")[0]
            tariff_name = ""
            if sub and getattr(sub, "tariff_key", None) and self.settings.tariffs_config:
                try:
                    tariff = self.settings.tariffs_config.require(str(sub.tariff_key))
                    tariff_name = tariff.name(lang)
                except Exception:
                    tariff_name = str(sub.tariff_key or "")
            end_date = getattr(sub, "end_date", None)
            seconds_left = (
                max(0, int((end_date - datetime.now(timezone.utc)).total_seconds()))
                if end_date
                else 0
            )
            return {
                "user_id": int(user.user_id),
                "name": " ".join(
                    part for part in [user.first_name, getattr(user, "last_name", None)] if part
                )
                or user.username
                or str(user.user_id),
                "username": user.username,
                "email": user.email,
                "telegram_id": user.telegram_id,
                "language": lang,
                "registration_date": (
                    user.registration_date.isoformat() if user.registration_date else None
                ),
                "email_login": bool(user.email and user.email_verified_at),
                "subscription_active": bool(sub),
                "panel_status": getattr(sub, "status_from_panel", None) if sub else None,
                "end_date": end_date.isoformat() if end_date else None,
                "remaining": _format_support_remaining(seconds_left, lang),
                "tariff": tariff_name or (getattr(sub, "tariff_key", None) if sub else ""),
                "traffic_regular": self._traffic_snapshot(
                    getattr(sub, "traffic_used_bytes", 0) if sub else 0,
                    self._regular_limit(sub),
                ),
                "traffic_premium": self._traffic_snapshot(
                    getattr(sub, "premium_used_bytes", 0) if sub else 0,
                    self._premium_limit(sub),
                ),
                "is_throttled": bool(getattr(sub, "is_throttled", False)) if sub else False,
                "topup_balance_bytes": int(getattr(sub, "topup_balance_bytes", 0) or 0)
                if sub
                else 0,
                "premium_topup_balance_bytes": int(
                    getattr(sub, "premium_topup_balance_bytes", 0) or 0
                )
                if sub
                else 0,
                "lifetime_used_traffic_bytes": int(user.lifetime_used_traffic_bytes or 0),
            }
        finally:
            if owns_session:
                await session.__aexit__(None, None, None)

    @staticmethod
    def _regular_limit(sub) -> int:
        if not sub:
            return 0
        if getattr(sub, "regular_unlimited_override", False):
            return 0
        return int(
            (sub.traffic_limit_bytes or 0)
            + (sub.topup_balance_bytes or 0)
            + (getattr(sub, "regular_bonus_bytes", 0) or 0)
        )

    @staticmethod
    def _premium_limit(sub) -> int:
        if not sub:
            return 0
        if getattr(sub, "premium_unlimited_override", False):
            return 0
        return int(
            (sub.premium_baseline_bytes or 0)
            + (sub.premium_topup_balance_bytes or 0)
            + (getattr(sub, "premium_topup_used_bytes", 0) or 0)
            + (getattr(sub, "premium_bonus_bytes", 0) or 0)
        )

    @staticmethod
    def _traffic_snapshot(used: Any, limit: Any) -> dict:
        used_value = max(0, int(used or 0))
        limit_value = max(0, int(limit or 0))
        percent = round((used_value / limit_value) * 100, 2) if limit_value else 0
        left = max(0, limit_value - used_value) if limit_value else 0
        return {
            "used_bytes": used_value,
            "limit_bytes": limit_value,
            "percent": percent,
            "left_bytes": left,
        }
