"""FC-3f: cursor-paginated artists directory.

Mirrors TagDirectoryService surface-for-surface, but backed by
Artist + Source + ImageRecord.artist_id (FC-2d-vii-c authoritative
attribution). Pure read-surface; no writes.

Cursor wire format is identical to tag_directory_service: base64 of
"<name>|<id>". The _encode/_decode helpers are copied (not imported)
to keep the two services decoupled.
"""
from __future__ import annotations

import base64
from dataclasses import dataclass

from sqlalchemy import and_, case, exists, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from ..models import Artist, ArtistVisit, ImageRecord, Source
from .gallery_service import thumbnail_url

_SEP = "|"
_PREVIEW_COUNT = 3


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


def _decode(cursor: str) -> tuple[str, int]:
    try:
        raw = base64.urlsafe_b64decode(cursor.encode()).decode()
        name, aid = raw.rsplit(_SEP, 1)
        return name, int(aid)
    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 ArtistDirectoryService:
    def __init__(self, session: AsyncSession):
        self.session = session

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

        count_col = func.count(ImageRecord.id).label("image_count")
        # Unseen = images imported since the artist's last_viewed_at.
        # NULL last_viewed_at (artist created before alembic 0034 seed
        # or before find_or_create autoseed) defensively counts as
        # "never visited" → all images unseen. Single grouped query, no
        # N+1.
        unseen_col = func.count(
            case(
                (
                    or_(
                        ArtistVisit.last_viewed_at.is_(None),
                        ImageRecord.created_at > ArtistVisit.last_viewed_at,
                    ),
                    ImageRecord.id,
                ),
                else_=None,
            )
        ).label("unseen_count")
        stmt = (
            select(Artist, count_col, unseen_col)
            .outerjoin(ImageRecord, ImageRecord.artist_id == Artist.id)
            .outerjoin(ArtistVisit, ArtistVisit.artist_id == Artist.id)
            .group_by(Artist.id)
        )
        if q:
            stmt = stmt.where(Artist.name.ilike(f"%{q}%"))
        if platform is not None:
            # Correlated EXISTS — NOT a JOIN. A JOIN to Source duplicates
            # the artist row when the artist has multiple sources on this
            # platform, breaking the keyset cursor and image_count.
            stmt = stmt.where(
                exists().where(
                    and_(
                        Source.artist_id == Artist.id,
                        Source.platform == platform,
                    )
                )
            )
        if cursor:
            c_name, c_id = _decode(cursor)
            stmt = stmt.where(
                or_(
                    Artist.name > c_name,
                    and_(Artist.name == c_name, Artist.id > c_id),
                )
            )
        stmt = stmt.order_by(Artist.name.asc(), Artist.id.asc()).limit(limit + 1)
        rows = (await self.session.execute(stmt)).all()

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

        artist_ids = [a.id for a, _, _ in rows]
        previews = await self._previews(artist_ids)

        cards = [
            {
                "id": artist.id,
                "name": artist.name,
                "slug": artist.slug,
                "is_subscription": bool(artist.is_subscription),
                "image_count": int(image_count),
                "unseen_count": int(unseen_count),
                "preview_thumbnails": previews.get(artist.id, []),
            }
            for artist, image_count, unseen_count in rows
        ]
        return DirectoryPage(cards=cards, next_cursor=next_cursor)

    async def _previews(self, artist_ids: list[int]) -> dict[int, list[str]]:
        """artist_id -> [thumbnail_url, ...up to _PREVIEW_COUNT].

        Window function ranks ImageRecord rows per artist by id DESC
        (newest first) and slices to the top _PREVIEW_COUNT in one query.
        """
        if not artist_ids:
            return {}
        rn = func.row_number().over(
            partition_by=ImageRecord.artist_id,
            order_by=ImageRecord.id.desc(),
        ).label("rn")
        sub = (
            select(
                ImageRecord.artist_id.label("artist_id"),
                ImageRecord.sha256.label("sha256"),
                ImageRecord.mime.label("mime"),
                ImageRecord.thumbnail_path.label("thumbnail_path"),
                rn,
            )
            .where(ImageRecord.artist_id.in_(artist_ids))
            .subquery()
        )
        stmt = (
            select(
                sub.c.artist_id, sub.c.sha256, sub.c.mime, sub.c.thumbnail_path,
            )
            .where(sub.c.rn <= _PREVIEW_COUNT)
            .order_by(sub.c.artist_id, sub.c.rn)
        )
        out: dict[int, list[str]] = {}
        for aid, sha, mime, tp in (await self.session.execute(stmt)).all():
            out.setdefault(aid, []).append(thumbnail_url(tp, sha, mime))
        return out
