"""Paged tag-directory cards with window-function preview thumbnails.

Cursor orders by (Tag.name ASC, Tag.id ASC). Preview thumbnails for the
page's tags are fetched in ONE windowed query (ROW_NUMBER <= 3) to avoid
an N+1. fandom_name is resolved via a self-join on Tag (fandom_id -> Tag).
"""

import base64
from dataclasses import dataclass

from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased

from ..models import ImageRecord, Tag
from ..models.tag import image_tag
from .gallery_service import thumbnail_url

_SEP = "|"


def _encode(name: str, tag_id: int) -> str:
    return base64.urlsafe_b64encode(f"{name}{_SEP}{tag_id}".encode()).decode()


def _decode(cursor: str) -> tuple[str, int]:
    try:
        raw = base64.urlsafe_b64decode(cursor.encode()).decode()
        name, tid = raw.rsplit(_SEP, 1)
        return name, int(tid)
    except Exception as exc:
        raise ValueError(f"invalid cursor: {cursor!r}") from exc


@dataclass(frozen=True)
class DirectoryPage:
    cards: list[dict]
    next_cursor: str | None


class TagDirectoryService:
    def __init__(self, session: AsyncSession):
        self.session = session

    async def list_tags(
        self,
        kind: str | None,
        q: str | None,
        cursor: str | None,
        limit: int = 60,
    ) -> DirectoryPage:
        if limit < 1 or limit > 200:
            raise ValueError("limit must be between 1 and 200")

        fandom = aliased(Tag)
        member = aliased(Tag)
        # image_count aggregates the tag's images INCLUDING, for a fandom, every
        # image carrying one of its characters. `member` is the tag actually on
        # the image; an image counts for the outer Tag if that applied tag IS the
        # Tag (direct) OR is a character whose fandom_id is the Tag (the fandom
        # leg). Both legs correlate the OUTER Tag.id at a SINGLE level — a nested
        # `tag_id IN (SELECT ... WHERE member.fandom_id == Tag.id)` does NOT
        # correlate and silently counts every fandom-character globally (~every
        # tag collapses to the same inflated number). DISTINCT so an image with
        # the fandom AND ≥1 of its characters counts once; a correlated scalar
        # subquery keeps it 1 row per tag with no group_by.
        count_col = (
            select(func.count(image_tag.c.image_record_id.distinct()))
            .select_from(image_tag)
            .join(member, member.id == image_tag.c.tag_id)
            .where(or_(image_tag.c.tag_id == Tag.id, member.fandom_id == Tag.id))
            .correlate(Tag)
            .scalar_subquery()
            .label("image_count")
        )
        stmt = select(Tag, fandom.name.label("fandom_name"), count_col).outerjoin(
            fandom, Tag.fandom_id == fandom.id
        )
        if kind is not None:
            stmt = stmt.where(Tag.kind == kind)
        if q:
            stmt = stmt.where(Tag.name.ilike(f"%{q}%"))
        if cursor:
            c_name, c_id = _decode(cursor)
            stmt = stmt.where(
                or_(
                    Tag.name > c_name,
                    and_(Tag.name == c_name, Tag.id > c_id),
                )
            )
        stmt = stmt.order_by(Tag.name.asc(), Tag.id.asc()).limit(limit + 1)
        rows = (await self.session.execute(stmt)).all()

        next_cursor = None
        if len(rows) > limit:
            last_tag = rows[limit - 1][0]
            next_cursor = _encode(last_tag.name, last_tag.id)
            rows = rows[:limit]

        tag_ids = [r[0].id for r in rows]
        previews = await self._previews(tag_ids)

        cards = [
            {
                "id": tag.id,
                "name": tag.name,
                "kind": tag.kind.value if hasattr(tag.kind, "value") else tag.kind,
                "fandom_id": tag.fandom_id,
                "fandom_name": fandom_name,
                "is_system": tag.is_system,
                "image_count": int(image_count),
                "preview_thumbnails": previews.get(tag.id, []),
            }
            for tag, fandom_name, image_count in rows
        ]
        return DirectoryPage(cards=cards, next_cursor=next_cursor)

    async def _previews(self, tag_ids: list[int]) -> dict[int, list[str]]:
        if not tag_ids:
            return {}
        # Preview pool per card = images tagged with the card's tag DIRECTLY,
        # UNION images carrying a character whose fandom_id is the card's tag (so
        # a fandom card previews its characters' images). UNION dedups the
        # overlap. For non-fandom cards the via-fandom leg is empty.
        member = aliased(Tag)
        direct = select(
            image_tag.c.tag_id.label("card_id"),
            image_tag.c.image_record_id.label("image_record_id"),
        ).where(image_tag.c.tag_id.in_(tag_ids))
        via_fandom = (
            select(
                member.fandom_id.label("card_id"),
                image_tag.c.image_record_id.label("image_record_id"),
            )
            .select_from(image_tag)
            .join(member, member.id == image_tag.c.tag_id)
            .where(member.fandom_id.in_(tag_ids))
        )
        scoped = direct.union(via_fandom).subquery()
        rn = func.row_number().over(
            partition_by=scoped.c.card_id,
            order_by=scoped.c.image_record_id.desc(),
        ).label("rn")
        sub = select(
            scoped.c.card_id.label("tag_id"),
            scoped.c.image_record_id.label("image_record_id"),
            rn,
        ).subquery()
        stmt = (
            select(
                sub.c.tag_id,
                ImageRecord.sha256,
                ImageRecord.mime,
                ImageRecord.thumbnail_path,
            )
            .join(ImageRecord, ImageRecord.id == sub.c.image_record_id)
            .where(sub.c.rn <= 3)
            .order_by(sub.c.tag_id, sub.c.rn)
        )
        out: dict[int, list[str]] = {}
        for tag_id, sha, mime, tp in (await self.session.execute(stmt)).all():
            out.setdefault(tag_id, []).append(thumbnail_url(tp, sha, mime))
        return out
