"""Channel publishing with per-destination results and conservative retries."""

from __future__ import annotations

import logging
import uuid
from dataclasses import dataclass, field
from datetime import timedelta

from aiogram.exceptions import (
    TelegramBadRequest,
    TelegramNetworkError,
    TelegramRetryAfter,
    TelegramServerError,
)

from app.config import Settings
from app.database.base import utcnow
from app.database.models import (
    DestinationStatus,
    JobType,
    OperationStatus,
    Post,
    PostStatus,
)
from app.database.repositories import Repositories
from app.errors import NotFoundError, PublishError, RetryableJobError
from app.gateways.base import MessageRef, TelegramGateway
from app.services.settings_service import SettingsService
from app.services.token_service import TokenService
from app.utils.retry import classify_exception, sleep_before_retry

logger = logging.getLogger(__name__)


@dataclass(frozen=True, slots=True)
class DestinationResult:
    destination_id: int
    channel_title: str
    status: DestinationStatus
    message_id: int | None = None
    error: str | None = None


@dataclass(slots=True)
class PublishResult:
    post_id: int
    status: PostStatus
    destinations: list[DestinationResult] = field(default_factory=list)
    message: str = ""

    @property
    def published_count(self) -> int:
        return sum(1 for item in self.destinations if item.status == DestinationStatus.PUBLISHED)

    @property
    def failed_count(self) -> int:
        return sum(
            1
            for item in self.destinations
            if item.status in {DestinationStatus.FAILED, DestinationStatus.UNKNOWN}
        )


class PublishingService:
    def __init__(
        self,
        repos: Repositories,
        settings: Settings,
        app_settings: SettingsService,
        gateway: TelegramGateway,
        tokens: TokenService,
    ) -> None:
        self.repos = repos
        self.settings = settings
        self.app_settings = app_settings
        self.gateway = gateway
        self.tokens = tokens

    async def publish(
        self,
        post_id: int,
        *,
        actor_id: int | None,
        correlation_id: str | None = None,
    ) -> PublishResult:
        correlation_id = correlation_id or uuid.uuid4().hex
        post = await self.repos.posts.get(post_id)
        if post is None:
            raise NotFoundError("پست پیدا نشد.")
        if post.status in {PostStatus.ARCHIVED, PostStatus.PAUSED}:
            return PublishResult(post_id, post.status, message="پست آرشیو یا متوقف شده است.")
        if not await self.repos.settings.get_bool("publish_enabled", True):
            raise PublishError("انتشار خودکار در تنظیمات عمومی غیرفعال است.")
        destinations = await self.repos.posts.destinations(post_id)
        if not destinations:
            await self.repos.posts.set_status(post_id, PostStatus.FAILED, last_error="مقصدی انتخاب نشده است.")
            raise PublishError("برای این پست هیچ کانال مقصدی انتخاب نشده است.")

        try:
            await self.tokens.ensure_for_post(post_id)
            post = await self.repos.posts.get(post_id)
            if post is None:
                raise NotFoundError("پست پیدا نشد.")
        except Exception as exc:
            await self.repos.posts.set_status(
                post_id, PostStatus.FAILED, last_error="ساخت لینک اختصاصی ناموفق بود."
            )
            raise PublishError("ساخت لینک اختصاصی ناموفق بود. نام کاربری ربات را بررسی کنید.") from exc

        policy = await self.app_settings.delete_policy(post)
        protect = await self.app_settings.protect_content(post)
        cover = await self.app_settings.cover(post)
        buttons = await self.app_settings.buttons(post)
        results: list[DestinationResult] = []
        attempted = 0

        for destination in destinations:
            if destination.status == DestinationStatus.PUBLISHED:
                results.append(
                    DestinationResult(
                        destination.id,
                        destination.channel.title or str(destination.channel.chat_id),
                        DestinationStatus.PUBLISHED,
                        destination.telegram_message_id,
                    )
                )
                continue
            if destination.status == DestinationStatus.UNKNOWN:
                results.append(
                    DestinationResult(
                        destination.id,
                        destination.channel.title or str(destination.channel.chat_id),
                        DestinationStatus.UNKNOWN,
                        destination.telegram_message_id,
                        "نتیجه ارسال قبلی نامعلوم است و برای جلوگیری از تکرار، دوباره ارسال نشد.",
                    )
                )
                continue
            if not destination.channel.is_active:
                await self.repos.posts.update_destination(
                    destination.id,
                    status=DestinationStatus.SKIPPED,
                    error_message="کانال مقصد غیرفعال است.",
                )
                results.append(
                    DestinationResult(
                        destination.id,
                        destination.channel.title or str(destination.channel.chat_id),
                        DestinationStatus.SKIPPED,
                        error="کانال غیرفعال است.",
                    )
                )
                continue
            if not await self.repos.posts.claim_destination(destination.id):
                results.append(
                    DestinationResult(
                        destination.id,
                        destination.channel.title or str(destination.channel.chat_id),
                        DestinationStatus.SKIPPED,
                        error="این مقصد در حال ارسال یا قبلاً ارسال شده است.",
                    )
                )
                continue

            attempted += 1
            channel_name = destination.channel.title or str(destination.channel.chat_id)
            channel_username = destination.channel.username
            caption = await self.app_settings.build_caption(
                post,
                channel_name=channel_name,
                channel_username=channel_username,
                publish_date=utcnow(),
            )
            operation_id = await self.repos.operations.start(
                correlation_id,
                "publish_channel",
                entity_type="post_destination",
                entity_id=destination.id,
                meta={"post_id": post_id, "channel_id": destination.channel.chat_id},
            )
            result, error, uncertain = await self._send_video(
                destination.channel.chat_id,
                post,
                caption=caption,
                cover=cover,
                buttons=buttons,
                protect=protect,
            )
            if result is not None:
                now = utcnow()
                delete_at = (
                    now + timedelta(seconds=policy.channel_seconds)
                    if policy.enabled and policy.channel_seconds > 0
                    else None
                )
                await self.repos.posts.update_destination(
                    destination.id,
                    status=DestinationStatus.PUBLISHED,
                    telegram_message_id=result.message_id,
                    error_message=None,
                    published_at=now,
                    delete_at=delete_at,
                )
                if delete_at is not None:
                    await self.repos.jobs.enqueue(
                        job_type=JobType.DELETE_CHANNEL_MESSAGE,
                        run_at=delete_at,
                        payload={"destination_id": destination.id, "post_id": post_id},
                        max_attempts=self.settings.job_max_attempts,
                        correlation_id=correlation_id,
                        idempotency_key=f"delete_channel:{destination.id}:{result.message_id}",
                        post_id=post_id,
                    )
                await self.repos.operations.finish(operation_id, OperationStatus.SUCCESS)
                results.append(
                    DestinationResult(
                        destination.id,
                        channel_name,
                        DestinationStatus.PUBLISHED,
                        result.message_id,
                    )
                )
            elif uncertain:
                await self.repos.posts.update_destination(
                    destination.id,
                    status=DestinationStatus.UNKNOWN,
                    error_message=error or "نتیجه ارسال نامعلوم است.",
                )
                await self.repos.operations.finish(
                    operation_id, OperationStatus.UNKNOWN, error_message=error
                )
                results.append(
                    DestinationResult(
                        destination.id,
                        channel_name,
                        DestinationStatus.UNKNOWN,
                        error=error,
                    )
                )
            else:
                await self.repos.posts.update_destination(
                    destination.id,
                    status=DestinationStatus.FAILED,
                    error_message=error or "ارسال ناموفق بود.",
                )
                await self.repos.operations.finish(
                    operation_id, OperationStatus.FAILED, error_message=error
                )
                results.append(
                    DestinationResult(
                        destination.id,
                        channel_name,
                        DestinationStatus.FAILED,
                        error=error,
                    )
                )

        all_destinations = await self.repos.posts.destinations(post_id)
        statuses = {item.status for item in all_destinations}
        published = statuses == {DestinationStatus.PUBLISHED}
        now = utcnow()
        if published:
            await self.repos.posts.update(
                post_id,
                status=PostStatus.PUBLISHED,
                published_at=now,
                last_error=None,
                publish_count=(post.publish_count or 0) + (1 if attempted else 0),
                scheduled_at=None,
            )
            final_status = PostStatus.PUBLISHED
            message = "انتشار در همه مقصدها با موفقیت تأیید شد."
        else:
            failures = [
                item
                for item in all_destinations
                if item.status in {DestinationStatus.FAILED, DestinationStatus.UNKNOWN}
            ]
            error_summary = "انتشار ناقص یا ناموفق. نتیجه هر مقصد را بررسی کنید."
            if failures:
                first = failures[0]
                error_summary = f"{error_summary} {first.error_message or ''}".strip()
            await self.repos.posts.update(
                post_id,
                status=PostStatus.FAILED,
                published_at=now if any(
                    item.status == DestinationStatus.PUBLISHED for item in all_destinations
                ) else post.published_at,
                last_error=error_summary[:2000],
                publish_count=(post.publish_count or 0) + (1 if attempted else 0),
            )
            final_status = PostStatus.FAILED
            message = error_summary

        await self.repos.audit.log(
            actor_id,
            "post.publish",
            entity_type="post",
            entity_id=post_id,
            details={
                "published": sum(
                    1 for item in all_destinations if item.status == DestinationStatus.PUBLISHED
                ),
                "failed": sum(
                    1
                    for item in all_destinations
                    if item.status in {DestinationStatus.FAILED, DestinationStatus.UNKNOWN}
                ),
            },
            correlation_id=correlation_id,
        )
        return PublishResult(post_id, final_status, results, message)

    async def _send_video(
        self,
        chat_id: int,
        post: Post,
        *,
        caption: str,
        cover: str | None,
        buttons: list[tuple[str, str]],
        protect: bool,
    ) -> tuple[MessageRef | None, str | None, bool]:
        if post.media_asset is None:
            return None, "فایل ویدیویی پست پیدا نشد.", False
        last_error: str | None = None
        for attempt in range(1, self.settings.retry_max_attempts + 1):
            try:
                result = await self.gateway.send_video(
                    chat_id,
                    post.media_asset.file_id,
                    caption=caption,
                    thumbnail_file_id=cover,
                    protect_content=protect,
                    buttons=buttons,
                )
                return result, None, False
            except Exception as exc:
                logger.exception(
                    "Video publishing failed: chat_id=%s attempt=%s error_type=%s",
                    chat_id,
                    attempt,
                    type(exc).__name__,
                )
                last_error = _safe_error(exc)
                decision = classify_exception(exc, sending=True)
                if isinstance(exc, (TelegramNetworkError, TelegramServerError, TimeoutError)):
                    # A transport failure after submission may still have delivered
                    # the message. Never retry blindly.
                    return None, "نتیجه ارسال نامعلوم است؛ برای جلوگیری از تکرار، دوباره ارسال نشد.", True
                if isinstance(exc, TelegramRetryAfter) and attempt < self.settings.retry_max_attempts:
                    await sleep_before_retry(attempt, decision, self.settings.retry_base_delay_seconds)
                    continue
                if decision.retryable and attempt < self.settings.retry_max_attempts:
                    await sleep_before_retry(attempt, decision, self.settings.retry_base_delay_seconds)
                    continue
                return None, last_error, False
        return None, last_error or "ارسال ناموفق بود.", False

    async def retry_failed(self, post_id: int, actor_id: int) -> PublishResult:
        count = await self.repos.posts.reset_failed_destinations(post_id)
        if count == 0:
            raise PublishError("هیچ مقصد شکست‌خورده‌ای برای تلاش دوباره وجود ندارد.")
        await self.repos.audit.log(
            actor_id,
            "post.retry_failed",
            entity_type="post",
            entity_id=post_id,
            details={"destinations": count},
        )
        return await self.publish(post_id, actor_id=actor_id)

    async def delete_channel_message(self, destination_id: int) -> None:
        destination = await self.repos.posts.channel_post(destination_id)
        if destination is None:
            raise RetryableJobError("مقصد پیدا نشد.", delay_seconds=60)
        if destination.telegram_message_id is None:
            await self.repos.posts.update_destination(
                destination_id, deleted_at=utcnow(), delete_error=None
            )
            return
        try:
            await self.gateway.delete_message(
                destination.channel.chat_id, destination.telegram_message_id
            )
        except TelegramBadRequest as exc:
            text = str(exc).lower()
            if "not found" in text or "message to delete" in text:
                await self.repos.posts.update_destination(
                    destination_id, deleted_at=utcnow(), delete_error=None
                )
                return
            await self.repos.posts.update_destination(
                destination_id, delete_error=_safe_error(exc)
            )
            raise
        except TelegramRetryAfter as exc:
            raise RetryableJobError("Telegram درخواست حذف را محدود کرد.", delay_seconds=exc.retry_after) from exc
        except (TelegramNetworkError, TelegramServerError) as exc:
            await self.repos.posts.update_destination(
                destination_id, delete_error="حذف پیام کانال به‌دلیل خطای شبکه انجام نشد."
            )
            raise RetryableJobError("خطای موقت در حذف پیام کانال.", delay_seconds=60) from exc
        except Exception as exc:
            await self.repos.posts.update_destination(
                destination_id, delete_error=_safe_error(exc)
            )
            raise
        await self.repos.posts.update_destination(
            destination_id, deleted_at=utcnow(), delete_error=None
        )
        await self.repos.audit.log(
            None,
            "channel_message.delete",
            entity_type="post_destination",
            entity_id=destination_id,
        )


def _safe_error(exc: BaseException) -> str:
    if isinstance(exc, TelegramRetryAfter):
        return f"Telegram درخواست را محدود کرد؛ تلاش مجدد پس از {exc.retry_after} ثانیه."
    if isinstance(exc, TelegramBadRequest):
        return "درخواست تلگرام نامعتبر بود (فایل، کپشن یا دسترسی را بررسی کنید)."
    if isinstance(exc, TelegramServerError):
        return "خطای موقت سرور تلگرام."
    if isinstance(exc, TelegramNetworkError):
        return "خطای شبکه هنگام ارتباط با تلگرام."
    return "عملیات تلگرام ناموفق بود. جزئیات در لاگ ثبت شد."
