"""Post lifecycle: draft editing, destinations, preview, scheduling and cloning."""

from __future__ import annotations

import logging
from dataclasses import dataclass
from datetime import datetime

from app.database.base import utcnow
from app.database.models import (
    JobType,
    MediaStatus,
    MembershipMode,
    Post,
    PostStatus,
)
from app.database.repositories import Repositories
from app.errors import NotFoundError, PublishError, ValidationError
from app.services.publishing_service import PublishingService, PublishResult
from app.services.settings_service import SettingsService
from app.services.token_service import TokenService
from app.utils.validation import validate_link_template

logger = logging.getLogger(__name__)


@dataclass(frozen=True, slots=True)
class PostPreview:
    post_id: int
    caption: str
    cover_file_id: str | None
    video_file_id: str
    video_link: str
    buttons: list[tuple[str, str]]
    destination_titles: list[str]
    status: PostStatus


class PostService:
    def __init__(
        self,
        repos: Repositories,
        app_settings: SettingsService,
        tokens: TokenService,
        publishing: PublishingService,
    ) -> None:
        self.repos = repos
        self.app_settings = app_settings
        self.tokens = tokens
        self.publishing = publishing

    async def create_from_media(self, media_id: int, actor_id: int) -> Post:
        media = await self.repos.media.get(media_id)
        if media is None:
            raise NotFoundError("فایل انتخاب‌شده پیدا نشد.")
        if media.status not in {MediaStatus.READY, MediaStatus.IN_USE}:
            raise ValidationError("این فایل آماده استفاده در پست نیست.")
        post = await self.repos.posts.create(media_asset_id=media.id, created_by=actor_id)
        await self.repos.audit.log(
            actor_id,
            "post.create_draft",
            entity_type="post",
            entity_id=post.id,
            details={"media_id": media.id},
        )
        return post

    async def set_title(self, post_id: int, title: str | None) -> None:
        value = (title or "").strip()
        if len(value) > 255:
            raise ValidationError("عنوان حداکثر ۲۵۵ کاراکتر است.")
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, title=value or None)

    async def set_description(self, post_id: int, description: str | None) -> None:
        value = (description or "").strip()
        if len(value) > 3000:
            raise ValidationError("توضیحات حداکثر ۳۰۰۰ کاراکتر است.")
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, description=value or None)

    async def set_cover(self, post_id: int, cover_file_id: str | None) -> None:
        await self._require_draft(post_id)
        if cover_file_id is not None and len(cover_file_id) > 512:
            raise ValidationError("شناسه عکس کاور نامعتبر است.")
        await self.repos.posts.update(post_id, cover_file_id=cover_file_id)

    async def set_caption_override(self, post_id: int, caption: str | None) -> None:
        value = (caption or "").strip()
        max_length = await self.app_settings.get_int("max_caption_length", 1024)
        if len(value) > max_length:
            raise ValidationError(f"کپشن حداکثر {max_length} کاراکتر است.")
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, caption_override=value or None)

    async def set_caption_prefix_override(self, post_id: int, value: str | None) -> None:
        if value is not None and len(value) > 1024:
            raise ValidationError("متن ابتدای کپشن حداکثر ۱۰۲۴ کاراکتر است.")
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, caption_prefix_override=value)

    async def set_caption_suffix_override(self, post_id: int, value: str | None) -> None:
        if value is not None and len(value) > 1024:
            raise ValidationError("متن انتهای کپشن حداکثر ۱۰۲۴ کاراکتر است.")
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, caption_suffix_override=value)

    async def set_signature_override(self, post_id: int, value: str | None) -> None:
        if value is not None and len(value) > 255:
            raise ValidationError("امضای پست حداکثر ۲۵۵ کاراکتر است.")
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, signature_override=value)

    async def set_video_link_template_override(self, post_id: int, value: str | None) -> None:
        if value is not None:
            value = validate_link_template(value)
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, video_link_template_override=value)

    async def set_destinations(self, post_id: int, channel_ids: list[int]) -> list[int]:
        await self._require_draft(post_id)
        channels = await self.repos.channels.list(active_only=True)
        allowed = {channel.id for channel in channels}
        selected = [channel_id for channel_id in dict.fromkeys(channel_ids) if channel_id in allowed]
        if not selected:
            default = await self.repos.channels.default()
            if default is not None:
                selected = [default.id]
        if not selected:
            raise ValidationError("حداقل یک کانال مقصد فعال انتخاب کنید.")
        await self.repos.posts.replace_destinations(post_id, selected)
        return selected

    async def set_membership_mode(self, post_id: int, mode: MembershipMode) -> None:
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, membership_mode=mode)

    async def set_delete_settings(
        self,
        post_id: int,
        *,
        enabled: bool,
        user_seconds: int,
        channel_seconds: int,
    ) -> None:
        if user_seconds < 0 or channel_seconds < 0:
            raise ValidationError("زمان حذف نمی‌تواند منفی باشد.")
        if user_seconds > 7 * 24 * 3600 or channel_seconds > 7 * 24 * 3600:
            raise ValidationError("حداکثر زمان حذف خودکار ۷ روز است.")
        await self._require_draft(post_id)
        await self.repos.posts.update(
            post_id,
            auto_delete_enabled=enabled,
            user_delete_after_seconds=user_seconds,
            channel_delete_after_seconds=channel_seconds,
        )

    async def set_protect_content(self, post_id: int, enabled: bool) -> None:
        await self._require_draft(post_id)
        await self.repos.posts.update(post_id, protect_content=enabled)

    async def prepare(self, post_id: int) -> Post:
        post = await self.repos.posts.get(post_id)
        if post is None:
            raise NotFoundError("پست پیدا نشد.")
        if post.status in {
            PostStatus.PUBLISHED,
            PostStatus.ARCHIVED,
            PostStatus.PAUSED,
        }:
            raise PublishError(
                "این پست در وضعیت فعلی قابل انتشار نیست؛ برای انتشار دوباره از گزینه «انتشار مجدد» استفاده کنید."
            )
        if post.media_asset is None or post.media_asset.status == MediaStatus.INVALID:
            raise ValidationError("فایل ویدیویی پست معتبر نیست.")
        # A direct publish or an edit that prepares a post must not leave an
        # older scheduled publish job behind; otherwise the same post could be
        # sent twice. Scheduling itself enqueues a fresh job after this call.
        await self.repos.jobs.cancel_for_post(post_id, JobType.PUBLISH_POST)
        destinations = await self.repos.posts.destinations(post_id)
        if not destinations:
            default = await self.repos.channels.default()
            if default is None:
                raise ValidationError("هیچ کانال مقصد فعالی وجود ندارد.")
            await self.repos.posts.replace_destinations(post_id, [default.id])
        await self.tokens.ensure_for_post(post_id)
        await self.repos.posts.update(
            post_id,
            status=PostStatus.READY,
            last_error=None,
        )
        refreshed = await self.repos.posts.get(post_id)
        if refreshed is None:
            raise NotFoundError("پست پیدا نشد.")
        return refreshed

    async def preview(self, post_id: int) -> PostPreview:
        post = await self.repos.posts.get(post_id)
        if post is None:
            raise NotFoundError("پست پیدا نشد.")
        if post.media_asset is None:
            raise ValidationError("پست فایل ویدیویی ندارد.")
        destinations = await self.repos.posts.destinations(post_id)
        if destinations:
            channel_name = destinations[0].channel.title
            channel_username = destinations[0].channel.username
        else:
            channel_name = None
            channel_username = None
        caption = await self.app_settings.build_caption(
            post,
            channel_name=channel_name,
            channel_username=channel_username,
            publish_date=utcnow(),
        )
        cover = await self.app_settings.cover(post)
        buttons = await self.app_settings.buttons(post)
        return PostPreview(
            post_id=post.id,
            caption=caption,
            cover_file_id=cover,
            video_file_id=post.media_asset.file_id,
            video_link=post.video_link or "",
            buttons=buttons,
            destination_titles=[
                destination.channel.title or str(destination.channel.chat_id)
                for destination in destinations
            ],
            status=post.status,
        )

    async def publish_now(self, post_id: int, actor_id: int) -> PublishResult:
        await self.prepare(post_id)
        result = await self.publishing.publish(post_id, actor_id=actor_id)
        return result

    async def republish(self, post_id: int, actor_id: int) -> PublishResult:
        """Send an already published/archived post again.

        Telegram has no edit/republish primitive for a previously sent video, so
        this intentionally creates a second channel post. The old post is left
        untouched (and any of its pending auto-delete jobs continue to run).
        """

        post = await self.repos.posts.get(post_id)
        if post is None:
            raise NotFoundError("پست مورد نظر یافت نشد.")
        if post.status not in {
            PostStatus.PUBLISHED,
            PostStatus.ARCHIVED,
            PostStatus.FAILED,
        }:
            raise PublishError(
                "فقط پست منتشرشده، آرشیوشده یا ناموفق را می‌توان به‌صورت صریح دوباره منتشر کرد."
            )
        await self.repos.jobs.cancel_for_post(post_id, JobType.PUBLISH_POST)
        reset = await self.repos.posts.reset_destinations_for_republish(post_id)
        if reset == 0:
            raise PublishError("برای این پست مقصد انتشار تعریف نشده است.")
        await self.repos.posts.update(
            post_id,
            status=PostStatus.READY,
            published_at=None,
            scheduled_at=None,
            last_error=None,
        )
        await self.tokens.ensure_for_post(post_id)
        await self.repos.audit.log(
            actor_id,
            "post.republish",
            entity_type="post",
            entity_id=post_id,
            details={"destinations": reset},
        )
        return await self.publishing.publish(post_id, actor_id=actor_id)

    async def schedule(
        self,
        post_id: int,
        run_at: datetime,
        actor_id: int,
    ) -> int:
        if run_at <= utcnow():
            raise ValidationError("زمان انتشار باید در آینده باشد.")
        await self.prepare(post_id)
        await self.repos.jobs.cancel_for_post(post_id, JobType.PUBLISH_POST)
        job, _created = await self.repos.jobs.enqueue(
            job_type=JobType.PUBLISH_POST,
            run_at=run_at,
            payload={"post_id": post_id},
            max_attempts=3,
            idempotency_key=f"schedule_publish:{post_id}:{int(run_at.timestamp())}",
            post_id=post_id,
        )
        await self.repos.posts.update(post_id, status=PostStatus.SCHEDULED, scheduled_at=run_at)
        await self.repos.audit.log(
            actor_id,
            "post.schedule",
            entity_type="post",
            entity_id=post_id,
            details={"job_id": job.id, "run_at": run_at.isoformat()},
        )
        return job.id

    async def cancel_schedule(self, post_id: int, actor_id: int) -> int:
        post = await self.repos.posts.get(post_id)
        if post is None:
            raise NotFoundError("پست پیدا نشد.")
        count = await self.repos.jobs.cancel_for_post(post_id, JobType.PUBLISH_POST)
        if post.status == PostStatus.SCHEDULED:
            await self.repos.posts.update(post_id, status=PostStatus.READY, scheduled_at=None)
        await self.repos.audit.log(
            actor_id,
            "post.cancel_schedule",
            entity_type="post",
            entity_id=post_id,
            details={"cancelled_jobs": count},
        )
        return count

    async def archive(self, post_id: int, actor_id: int) -> None:
        await self.repos.jobs.cancel_for_post(post_id, JobType.PUBLISH_POST)
        if not await self.repos.posts.update(
            post_id, status=PostStatus.ARCHIVED, scheduled_at=None
        ):
            raise NotFoundError("پست پیدا نشد.")
        await self.repos.audit.log(
            actor_id, "post.archive", entity_type="post", entity_id=post_id
        )

    async def pause(self, post_id: int, actor_id: int) -> None:
        await self.repos.jobs.cancel_for_post(post_id, JobType.PUBLISH_POST)
        if not await self.repos.posts.update(
            post_id, status=PostStatus.PAUSED, scheduled_at=None
        ):
            raise NotFoundError("پست پیدا نشد.")
        await self.repos.audit.log(actor_id, "post.pause", entity_type="post", entity_id=post_id)

    async def resume(self, post_id: int, actor_id: int) -> None:
        post = await self.repos.posts.get(post_id)
        if post is None:
            raise NotFoundError("پست پیدا نشد.")
        if post.status != PostStatus.PAUSED:
            raise PublishError("فقط پست متوقف‌شده را می‌توان ادامه داد.")
        await self.repos.posts.update(post_id, status=PostStatus.READY, scheduled_at=None)
        await self.repos.audit.log(actor_id, "post.resume", entity_type="post", entity_id=post_id)

    async def delete(self, post_id: int, actor_id: int) -> None:
        # Deleting a post cascades its deliveries and destinations. Cancel every
        # pending job for it first so a later delete job does not chase a row that
        # no longer exists.
        for job_type in JobType:
            await self.repos.jobs.cancel_for_post(post_id, job_type)
        if not await self.repos.posts.delete(post_id):
            raise NotFoundError("پست پیدا نشد.")
        await self.repos.audit.log(
            actor_id, "post.delete", entity_type="post", entity_id=post_id
        )

    async def clone(self, post_id: int, actor_id: int) -> Post:
        source = await self.repos.posts.get(post_id)
        if source is None:
            raise NotFoundError("پست پیدا نشد.")
        clone = await self.repos.posts.create(
            media_asset_id=source.media_asset_id,
            created_by=actor_id,
            title=(source.title + " (کپی)")[:255] if source.title else "کپی پست",
        )
        await self.repos.posts.update(
            clone.id,
            description=source.description,
            cover_file_id=source.cover_file_id,
            caption_override=source.caption_override,
            caption_template_override=source.caption_template_override,
            caption_prefix_override=source.caption_prefix_override,
            caption_suffix_override=source.caption_suffix_override,
            signature_override=source.signature_override,
            video_link_template_override=source.video_link_template_override,
            buttons_override=source.buttons_override,
            membership_mode=source.membership_mode,
            auto_delete_enabled=source.auto_delete_enabled,
            user_delete_after_seconds=source.user_delete_after_seconds,
            channel_delete_after_seconds=source.channel_delete_after_seconds,
            protect_content=source.protect_content,
        )
        channels = await self.repos.posts.channels_for_publish(post_id)
        if channels:
            await self.repos.posts.replace_destinations(clone.id, [c.id for c in channels])
        overrides = await self.repos.membership.post_overrides(post_id)
        for channel_id, required in overrides.items():
            await self.repos.membership.set_post_override(clone.id, channel_id, required)
        await self.repos.audit.log(
            actor_id,
            "post.clone",
            entity_type="post",
            entity_id=clone.id,
            details={"source_post_id": post_id},
        )
        return clone

    async def bulk_publish(self, post_ids: list[int], actor_id: int) -> list[PublishResult]:
        results: list[PublishResult] = []
        for post_id in post_ids[:20]:
            try:
                results.append(await self.publish_now(post_id, actor_id))
            except Exception as exc:
                results.append(
                    PublishResult(post_id, PostStatus.FAILED, message=str(exc)[:500])
                )
        return results

    async def bulk_archive(self, post_ids: list[int], actor_id: int) -> int:
        count = 0
        for post_id in post_ids[:20]:
            try:
                await self.archive(post_id, actor_id)
                count += 1
            except NotFoundError:
                continue
        return count

    async def bulk_delete(self, post_ids: list[int], actor_id: int) -> int:
        count = 0
        for post_id in post_ids[:20]:
            try:
                await self.delete(post_id, actor_id)
                count += 1
            except NotFoundError:
                continue
        return count

    async def _require_draft(self, post_id: int) -> Post:
        post = await self.repos.posts.get(post_id)
        if post is None:
            raise NotFoundError("پست پیدا نشد.")
        if post.status in {PostStatus.PUBLISHED, PostStatus.ARCHIVED}:
            raise PublishError("پست منتشرشده یا آرشیوشده قابل ویرایش نیست؛ ابتدا کپی بسازید.")
        if post.status == PostStatus.SCHEDULED:
            raise PublishError("ابتدا زمان‌بندی پست را لغو کنید و سپس آن را ویرایش کنید.")
        return post
