"""Playlist repository — adapter over ``AsyncSession``."""

import uuid

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.domain.entities.playlist import Playlist
from app.domain.entities.track import Track
from app.infrastructure.db.models.playlist import PlaylistModel, PlaylistTrackModel
from app.infrastructure.db.models.track import TrackModel


def _to_entity(row: PlaylistModel) -> Playlist:
    return Playlist(
        id=row.id,
        name=row.name,
        description=row.description,
        owner_id=row.owner_id,
        version=row.version,
        created_at=row.created_at,
        updated_at=row.updated_at,
    )


def _track_to_entity(row: TrackModel) -> Track:
    return Track(
        id=row.id,
        title=row.title,
        artist_id=row.artist_id,
        album_id=row.album_id,
        storage_uri=row.storage_uri,
        file_format=row.file_format,
        file_size=row.file_size,
        source=row.source,
        source_id=row.source_id,
        duration_seconds=row.duration_seconds,
        genre=row.genre,
        year=row.year,
        track_number=row.track_number,
        metadata_status=row.metadata_status,
        metadata_error=row.metadata_error,
        enriched_at=row.enriched_at,
        created_at=row.created_at,
        updated_at=row.updated_at,
    )


class SqlAlchemyPlaylistRepository:
    def __init__(self, session: AsyncSession) -> None:
        self._session = session

    async def get_by_id(self, playlist_id: uuid.UUID) -> Playlist | None:
        row = await self._session.get(PlaylistModel, playlist_id)
        return _to_entity(row) if row is not None else None

    async def count(self, *, owner_id: uuid.UUID) -> int:
        return (
            await self._session.execute(
                select(func.count())
                .select_from(PlaylistModel)
                .where(PlaylistModel.owner_id == owner_id)
            )
        ).scalar_one()

    async def add(self, *, name: str, description: str | None, owner_id: uuid.UUID) -> Playlist:
        row = PlaylistModel(name=name, description=description, owner_id=owner_id)
        self._session.add(row)
        await self._session.flush()
        await self._session.refresh(row)
        return _to_entity(row)

    async def update(
        self, playlist_id: uuid.UUID, *, name: str | None, description: str | None
    ) -> Playlist:
        row = await self._session.get(PlaylistModel, playlist_id)
        if row is None:
            from app.domain.errors import NotFoundError

            raise NotFoundError(f"Playlist {playlist_id} not found.")
        if name is not None:
            row.name = name
        if description is not None:
            row.description = description
        row.version = row.version + 1
        await self._session.flush()
        await self._session.refresh(row)
        return _to_entity(row)

    async def delete(self, playlist_id: uuid.UUID) -> None:
        row = await self._session.get(PlaylistModel, playlist_id)
        if row is not None:
            await self._session.delete(row)
            await self._session.flush()

    async def track_count(self, playlist_id: uuid.UUID) -> int:
        return (
            await self._session.execute(
                select(func.count())
                .select_from(PlaylistTrackModel)
                .where(PlaylistTrackModel.playlist_id == playlist_id)
            )
        ).scalar_one()

    async def track_count_many(self, playlist_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
        if not playlist_ids:
            return {}
        rows = (
            await self._session.execute(
                select(
                    PlaylistTrackModel.playlist_id,
                    func.count(PlaylistTrackModel.id).label("cnt"),
                )
                .where(PlaylistTrackModel.playlist_id.in_(playlist_ids))
                .group_by(PlaylistTrackModel.playlist_id)
            )
        ).all()
        return {row.playlist_id: row.cnt for row in rows}

    async def get_tracks(self, playlist_id: uuid.UUID, *, limit: int, offset: int) -> list[Track]:
        rows = (
            (
                await self._session.execute(
                    select(TrackModel)
                    .join(PlaylistTrackModel, TrackModel.id == PlaylistTrackModel.track_id)
                    .where(PlaylistTrackModel.playlist_id == playlist_id)
                    .order_by(PlaylistTrackModel.position)
                    .limit(limit)
                    .offset(offset)
                )
            )
            .scalars()
            .all()
        )
        return [_track_to_entity(r) for r in rows]

    async def get_track_total(self, playlist_id: uuid.UUID) -> int:
        return await self.track_count(playlist_id)

    async def add_track(
        self, playlist_id: uuid.UUID, track_id: uuid.UUID, *, position: float
    ) -> None:
        row = PlaylistTrackModel(playlist_id=playlist_id, track_id=track_id, position=position)
        self._session.add(row)
        playlist = await self._session.get(PlaylistModel, playlist_id)
        if playlist is not None:
            playlist.version = playlist.version + 1
        await self._session.flush()

    async def remove_track(self, playlist_id: uuid.UUID, track_id: uuid.UUID) -> None:
        row = (
            await self._session.execute(
                select(PlaylistTrackModel).where(
                    PlaylistTrackModel.playlist_id == playlist_id,
                    PlaylistTrackModel.track_id == track_id,
                )
            )
        ).scalar_one_or_none()
        if row is not None:
            await self._session.delete(row)
            playlist = await self._session.get(PlaylistModel, playlist_id)
            if playlist is not None:
                playlist.version = playlist.version + 1
            await self._session.flush()

    async def max_position(self, playlist_id: uuid.UUID) -> float:
        result = (
            await self._session.execute(
                select(func.max(PlaylistTrackModel.position)).where(
                    PlaylistTrackModel.playlist_id == playlist_id
                )
            )
        ).scalar_one_or_none()
        return float(result) if result is not None else 0.0

    # list must come after methods using list[...] in signatures (builtin name shadowing)
    async def list(self, *, owner_id: uuid.UUID, limit: int, offset: int) -> list[Playlist]:
        rows = (
            (
                await self._session.execute(
                    select(PlaylistModel)
                    .where(PlaylistModel.owner_id == owner_id)
                    .order_by(PlaylistModel.updated_at.desc())
                    .limit(limit)
                    .offset(offset)
                )
            )
            .scalars()
            .all()
        )
        return [_to_entity(r) for r in rows]
