"""Subsonic playlist endpoints — adapters over the playlist repository.

Playlists are private to their owner (no public-playlist concept yet), so every
read/write is scoped to the authenticated user. ``createPlaylist`` doubles as a
full replace when given a ``playlistId`` (Subsonic overloads it that way).
"""

from typing import Annotated, Any

from fastapi import APIRouter, Query, Response

from app.api.deps import (
    AlbumRepoDep,
    ArtistRepoDep,
    PlaylistRepoDep,
    SubsonicFormat,
    SubsonicUser,
)
from app.api.rest.envelope import subsonic_response
from app.api.rest.ids import decode_playlist, decode_track, encode_playlist
from app.api.rest.serializers import iso, song_dict
from app.domain.entities import Playlist, User
from app.domain.errors import NotFoundError, PermissionDeniedError

router = APIRouter()


def _playlist_dict(playlist: Playlist, owner: str, *, song_count: int) -> dict[str, Any]:
    return {
        "id": encode_playlist(playlist.id),
        "name": playlist.name,
        "comment": playlist.description,
        "owner": owner,
        "public": False,
        "songCount": song_count,
        "created": iso(playlist.created_at),
        "changed": iso(playlist.updated_at),
    }


async def _owned_playlist(
    playlist_id_raw: str, playlist_repo: PlaylistRepoDep, user: User
) -> Playlist:
    playlist = await playlist_repo.get_by_id(decode_playlist(playlist_id_raw))
    if playlist is None:
        raise NotFoundError("Playlist not found.")
    if playlist.owner_id != user.id:
        raise PermissionDeniedError("You don't own this playlist.")
    return playlist


async def _playlist_songs(
    playlist_id: str,
    playlist_repo: PlaylistRepoDep,
    artist_repo: ArtistRepoDep,
    album_repo: AlbumRepoDep,
) -> list[dict[str, Any]]:
    tracks = await playlist_repo.get_tracks(decode_playlist(playlist_id), limit=10_000, offset=0)
    artist_map = {a.id: a for a in await artist_repo.get_many(list({t.artist_id for t in tracks}))}
    album_map = {
        a.id: a
        for a in await album_repo.get_many(
            list({t.album_id for t in tracks if t.album_id is not None})
        )
    }
    return [
        song_dict(
            t,
            artist_map.get(t.artist_id),
            album_map.get(t.album_id) if t.album_id is not None else None,
        )
        for t in tracks
    ]


@router.api_route("/getPlaylists", methods=["GET", "POST"])
@router.api_route("/getPlaylists.view", methods=["GET", "POST"])
async def get_playlists(
    user: SubsonicUser, fmt: SubsonicFormat, playlist_repo: PlaylistRepoDep
) -> Response:
    playlists = await playlist_repo.list(owner_id=user.id, limit=500, offset=0)
    counts = await playlist_repo.track_count_many([p.id for p in playlists])
    items = [_playlist_dict(p, user.username, song_count=counts.get(p.id, 0)) for p in playlists]
    return subsonic_response({"playlists": {"playlist": items}}, fmt=fmt)


@router.api_route("/getPlaylist", methods=["GET", "POST"])
@router.api_route("/getPlaylist.view", methods=["GET", "POST"])
async def get_playlist(
    user: SubsonicUser,
    fmt: SubsonicFormat,
    playlist_repo: PlaylistRepoDep,
    artist_repo: ArtistRepoDep,
    album_repo: AlbumRepoDep,
    id: Annotated[str, Query()],
) -> Response:
    playlist = await _owned_playlist(id, playlist_repo, user)
    songs = await _playlist_songs(id, playlist_repo, artist_repo, album_repo)
    payload = {**_playlist_dict(playlist, user.username, song_count=len(songs)), "entry": songs}
    return subsonic_response({"playlist": payload}, fmt=fmt)


@router.api_route("/createPlaylist", methods=["GET", "POST"])
@router.api_route("/createPlaylist.view", methods=["GET", "POST"])
async def create_playlist(
    user: SubsonicUser,
    fmt: SubsonicFormat,
    playlist_repo: PlaylistRepoDep,
    artist_repo: ArtistRepoDep,
    album_repo: AlbumRepoDep,
    name: Annotated[str | None, Query()] = None,
    playlistId: Annotated[str | None, Query()] = None,
    songId: Annotated[list[str] | None, Query()] = None,
) -> Response:
    song_ids = [decode_track(s) for s in (songId or [])]

    if playlistId is not None:
        # Overloaded form: replace the existing playlist's tracks (and name).
        playlist = await _owned_playlist(playlistId, playlist_repo, user)
        if name is not None:
            playlist = await playlist_repo.update(playlist.id, name=name, description=None)
        existing = await playlist_repo.get_tracks(playlist.id, limit=10_000, offset=0)
        for t in existing:
            await playlist_repo.remove_track(playlist.id, t.id)
    else:
        playlist = await playlist_repo.add(
            name=name or "Untitled", description=None, owner_id=user.id
        )

    for position, track_id in enumerate(song_ids, start=1):
        await playlist_repo.add_track(playlist.id, track_id, position=float(position))

    encoded = encode_playlist(playlist.id)
    songs = await _playlist_songs(encoded, playlist_repo, artist_repo, album_repo)
    payload = {**_playlist_dict(playlist, user.username, song_count=len(songs)), "entry": songs}
    return subsonic_response({"playlist": payload}, fmt=fmt)


@router.api_route("/updatePlaylist", methods=["GET", "POST"])
@router.api_route("/updatePlaylist.view", methods=["GET", "POST"])
async def update_playlist(
    user: SubsonicUser,
    fmt: SubsonicFormat,
    playlist_repo: PlaylistRepoDep,
    playlistId: Annotated[str, Query()],
    name: Annotated[str | None, Query()] = None,
    comment: Annotated[str | None, Query()] = None,
    songIdToAdd: Annotated[list[str] | None, Query()] = None,
    songIndexToRemove: Annotated[list[int] | None, Query()] = None,
) -> Response:
    playlist = await _owned_playlist(playlistId, playlist_repo, user)

    if name is not None or comment is not None:
        playlist = await playlist_repo.update(playlist.id, name=name, description=comment)

    # Removals are by index into the current ordered track list — resolve first.
    if songIndexToRemove:
        current = await playlist_repo.get_tracks(playlist.id, limit=10_000, offset=0)
        for index in sorted(set(songIndexToRemove)):
            if 0 <= index < len(current):
                await playlist_repo.remove_track(playlist.id, current[index].id)

    if songIdToAdd:
        position = await playlist_repo.max_position(playlist.id)
        for raw in songIdToAdd:
            position += 1.0
            await playlist_repo.add_track(playlist.id, decode_track(raw), position=position)

    return subsonic_response(fmt=fmt)


@router.api_route("/deletePlaylist", methods=["GET", "POST"])
@router.api_route("/deletePlaylist.view", methods=["GET", "POST"])
async def delete_playlist(
    user: SubsonicUser,
    fmt: SubsonicFormat,
    playlist_repo: PlaylistRepoDep,
    id: Annotated[str, Query()],
) -> Response:
    playlist = await _owned_playlist(id, playlist_repo, user)
    await playlist_repo.delete(playlist.id)
    return subsonic_response(fmt=fmt)
