"""Membership, access-token and delivery persistence."""

from __future__ import annotations

from datetime import datetime
from typing import Any

from sqlalchemy import delete, func, select, update
from sqlalchemy.orm import selectinload

from app.database.base import utcnow
from app.database.models import (
    AccessToken,
    DeleteStatus,
    MediaAsset,
    MembershipMode,
    Post,
    PostRequiredChannel,
    RequiredChannel,
    UserDelivery,
)
from app.database.repositories.base import BaseRepository


class MembershipRepository(BaseRepository):
    async def list_required(self, *, active_only: bool = False) -> list[RequiredChannel]:
        async with self.db.session() as session:
            statement = select(RequiredChannel).order_by(
                RequiredChannel.sort_order, RequiredChannel.id
            )
            if active_only:
                statement = statement.where(RequiredChannel.is_active.is_(True))
            return list(await session.scalars(statement))

    async def get_required(self, channel_id: int) -> RequiredChannel | None:
        async with self.db.session() as session:
            return await session.get(RequiredChannel, channel_id)

    async def get_required_by_chat(self, chat_id: int) -> RequiredChannel | None:
        async with self.db.session() as session:
            return await session.scalar(
                select(RequiredChannel).where(RequiredChannel.chat_id == chat_id)
            )

    async def add_required(
        self,
        *,
        chat_id: int,
        title: str,
        invite_link: str,
        chat_type: str,
        bot_status: str | None = None,
        last_error: str | None = None,
    ) -> tuple[RequiredChannel, bool]:
        async with self.db.session() as session:
            channel = await session.scalar(
                select(RequiredChannel).where(RequiredChannel.chat_id == chat_id)
            )
            if channel is not None:
                channel.title = title or channel.title
                channel.invite_link = invite_link or channel.invite_link
                channel.chat_type = chat_type
                channel.is_active = True
                channel.bot_status = bot_status
                channel.last_checked_at = utcnow()
                channel.last_error = last_error
                return channel, False
            max_order = int(
                await session.scalar(select(func.max(RequiredChannel.sort_order))) or 0
            )
            channel = RequiredChannel(
                chat_id=chat_id,
                title=title,
                invite_link=invite_link,
                chat_type=chat_type,
                is_active=True,
                is_required_globally=True,
                sort_order=max_order + 1,
                bot_status=bot_status,
                last_checked_at=utcnow(),
                last_error=last_error,
            )
            session.add(channel)
            await session.flush()
            return channel, True

    async def update_required(self, channel_id: int, **fields: Any) -> bool:
        allowed = {
            "title",
            "invite_link",
            "chat_type",
            "is_active",
            "is_required_globally",
            "sort_order",
            "bot_status",
            "last_checked_at",
            "last_error",
        }
        values = {key: value for key, value in fields.items() if key in allowed}
        if not values:
            return False
        async with self.db.session() as session:
            channel = await session.get(RequiredChannel, channel_id)
            if channel is None:
                return False
            for key, value in values.items():
                setattr(channel, key, value)
            return True

    async def remove_required(self, channel_id: int) -> bool:
        async with self.db.session() as session:
            channel = await session.get(RequiredChannel, channel_id)
            if channel is None:
                return False
            await session.delete(channel)
            return True

    async def move_required(self, channel_id: int, direction: int) -> bool:
        async with self.db.session() as session:
            channels = list(
                await session.scalars(
                    select(RequiredChannel).order_by(
                        RequiredChannel.sort_order, RequiredChannel.id
                    )
                )
            )
            index = next((i for i, item in enumerate(channels) if item.id == channel_id), None)
            if index is None:
                return False
            target = index + direction
            if target < 0 or target >= len(channels):
                return False
            channels[index].sort_order, channels[target].sort_order = (
                channels[target].sort_order,
                channels[index].sort_order,
            )
            return True

    async def post_overrides(self, post_id: int) -> dict[int, bool]:
        async with self.db.session() as session:
            rows = await session.execute(
                select(PostRequiredChannel.channel_id, PostRequiredChannel.is_required).where(
                    PostRequiredChannel.post_id == post_id
                )
            )
            return {int(channel_id): bool(required) for channel_id, required in rows}

    async def set_post_override(self, post_id: int, channel_id: int, required: bool) -> None:
        async with self.db.session() as session:
            row = await session.get(PostRequiredChannel, (post_id, channel_id))
            if row is None:
                session.add(
                    PostRequiredChannel(
                        post_id=post_id, channel_id=channel_id, is_required=required
                    )
                )
            else:
                row.is_required = required

    async def clear_post_overrides(self, post_id: int) -> None:
        async with self.db.session() as session:
            await session.execute(
                delete(PostRequiredChannel).where(PostRequiredChannel.post_id == post_id)
            )


class TokenRepository(BaseRepository):
    async def create(
        self,
        *,
        post_id: int,
        token: str,
        expires_at: datetime | None = None,
        max_uses: int | None = None,
    ) -> AccessToken:
        async with self.db.session() as session:
            row = AccessToken(
                post_id=post_id,
                token=token,
                is_active=True,
                expires_at=expires_at,
                max_uses=max_uses,
            )
            session.add(row)
            await session.flush()
            return row

    async def active_for_post(self, post_id: int) -> AccessToken | None:
        async with self.db.session() as session:
            return await session.scalar(
                select(AccessToken)
                .where(AccessToken.post_id == post_id, AccessToken.is_active.is_(True))
                .order_by(AccessToken.created_at.desc())
                .limit(1)
            )

    async def latest_for_post(self, post_id: int) -> AccessToken | None:
        """Return the newest token even when it is inactive.

        The admin panel must be able to show and reactivate a disabled token,
        so it cannot rely on ``active_for_post`` alone.
        """

        async with self.db.session() as session:
            return await session.scalar(
                select(AccessToken)
                .where(AccessToken.post_id == post_id)
                .order_by(AccessToken.created_at.desc(), AccessToken.id.desc())
                .limit(1)
            )

    async def get_by_token(self, token: str, *, with_post: bool = False) -> AccessToken | None:
        async with self.db.session() as session:
            statement = select(AccessToken).where(AccessToken.token == token)
            if with_post:
                statement = statement.options(
                    selectinload(AccessToken.post)
                    .selectinload(Post.media_asset)
                    .selectinload(MediaAsset.source_chat),
                    selectinload(AccessToken.post).selectinload(Post.destinations),
                )
            return await session.scalar(statement)

    async def list_for_post(self, post_id: int) -> list[AccessToken]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(AccessToken)
                .where(AccessToken.post_id == post_id)
                .order_by(AccessToken.created_at.desc())
            )
            return list(rows)

    async def deactivate_for_post(self, post_id: int) -> None:
        async with self.db.session() as session:
            await session.execute(
                update(AccessToken)
                .where(AccessToken.post_id == post_id, AccessToken.is_active.is_(True))
                .values(is_active=False, rotated_at=utcnow())
            )

    async def update(self, token_id: int, **fields: Any) -> bool:
        allowed = {"is_active", "expires_at", "max_uses"}
        values = {key: value for key, value in fields.items() if key in allowed}
        if not values:
            return False
        async with self.db.session() as session:
            token = await session.get(AccessToken, token_id)
            if token is None:
                return False
            for key, value in values.items():
                setattr(token, key, value)
            return True

    async def consume(self, token_id: int, *, now: datetime) -> bool:
        """Atomically consume one use. Expired/exhausted tokens cannot be consumed."""

        async with self.db.session() as session:
            result = await session.execute(
                update(AccessToken)
                .where(
                    AccessToken.id == token_id,
                    AccessToken.is_active.is_(True),
                    (AccessToken.expires_at.is_(None)) | (AccessToken.expires_at > now),
                    (AccessToken.max_uses.is_(None))
                    | (AccessToken.use_count < AccessToken.max_uses),
                )
                .values(use_count=AccessToken.use_count + 1, last_used_at=now)
            )
            return bool(result.rowcount)


class DeliveryRepository(BaseRepository):
    async def create(
        self,
        *,
        user_id: int,
        post_id: int,
        access_token_id: int,
        telegram_message_id: int,
        sent_at: datetime,
        delete_at: datetime | None,
        correlation_id: str | None,
    ) -> UserDelivery:
        async with self.db.session() as session:
            row = UserDelivery(
                user_id=user_id,
                post_id=post_id,
                access_token_id=access_token_id,
                telegram_message_id=telegram_message_id,
                sent_at=sent_at,
                delete_at=delete_at,
                delete_status=DeleteStatus.PENDING if delete_at else DeleteStatus.SKIPPED,
                correlation_id=correlation_id,
            )
            session.add(row)
            await session.flush()
            return row

    async def get(self, delivery_id: int) -> UserDelivery | None:
        async with self.db.session() as session:
            return await session.scalar(
                select(UserDelivery)
                .options(selectinload(UserDelivery.post), selectinload(UserDelivery.user))
                .where(UserDelivery.id == delivery_id)
            )

    async def mark_deleted(self, delivery_id: int, *, error: str | None = None) -> bool:
        async with self.db.session() as session:
            row = await session.get(UserDelivery, delivery_id)
            if row is None:
                return False
            if error:
                row.delete_status = DeleteStatus.FAILED
                row.delete_error = error
            else:
                row.delete_status = DeleteStatus.DELETED
                row.deleted_at = utcnow()
                row.delete_error = None
            return True

    async def count(self) -> int:
        async with self.db.session() as session:
            return int(await session.scalar(select(func.count(UserDelivery.id))) or 0)

    async def count_deleted(self) -> int:
        async with self.db.session() as session:
            return int(
                await session.scalar(
                    select(func.count(UserDelivery.id)).where(
                        UserDelivery.delete_status == DeleteStatus.DELETED
                    )
                )
                or 0
            )

    async def list_recent(self, limit: int = 20) -> list[UserDelivery]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(UserDelivery)
                .options(selectinload(UserDelivery.user), selectinload(UserDelivery.post))
                .order_by(UserDelivery.sent_at.desc())
                .limit(limit)
            )
            return list(rows)


def effective_channels(
    post: Post,
    globally_required: list[RequiredChannel],
    overrides: dict[int, bool],
) -> list[RequiredChannel]:
    """Pure priority rule for required channels.

    DISABLE: none.
    ENFORCE: every active required channel is enforced for this post.
    INHERIT: globally required active channels.
    Per-post overrides win over the mode-derived set.
    """

    if post.membership_mode == MembershipMode.DISABLE:
        return []
    if post.membership_mode == MembershipMode.ENFORCE:
        base = [channel for channel in globally_required if channel.is_active]
    else:
        base = [
            channel
            for channel in globally_required
            if channel.is_active and channel.is_required_globally
        ]
    selected = {channel.id: channel for channel in base}
    for channel in globally_required:
        if channel.id in overrides and overrides[channel.id] and channel.is_active:
            selected[channel.id] = channel
    for channel_id, required in overrides.items():
        if not required:
            selected.pop(channel_id, None)
    return sorted(selected.values(), key=lambda item: (item.sort_order, item.id))
