//! Module d'accès au catalogue Qobuz (albums, tracks, artistes, playlists)

use super::QobuzApi;
use crate::error::{QobuzError, Result};
use crate::models::*;
use serde::Deserialize;
use std::collections::HashMap;
use tracing::debug;

/// Réponse paginée de l'API
#[derive(Debug, Deserialize)]
struct PaginatedResponse<T> {
    items: Vec<T>,
    #[serde(default)]
    total: Option<u32>,
    #[serde(default)]
    _limit: Option<u32>,
    #[serde(default)]
    _offset: Option<u32>,
}

/// Réponse de l'endpoint /album/get
#[derive(Debug, Deserialize)]
pub(crate) struct AlbumResponse {
    #[serde(deserialize_with = "crate::models::deserialize_id")]
    id: String,
    title: String,
    artist: ArtistResponse,
    #[serde(default)]
    tracks_count: Option<u32>,
    #[serde(default)]
    duration: Option<u32>,
    #[serde(default)]
    release_date_original: Option<String>,
    #[serde(default)]
    released_at: Option<i64>,
    #[serde(default)]
    image: Option<ImageResponse>,
    #[serde(default = "default_streamable")]
    streamable: bool,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    maximum_sampling_rate: Option<f64>,
    #[serde(default)]
    maximum_bit_depth: Option<u32>,
    #[serde(default)]
    genre: Option<GenreResponse>,
    #[serde(default)]
    label: Option<LabelResponse>,
    #[serde(default)]
    tracks: Option<PaginatedResponse<TrackResponse>>,
}

/// Réponse de l'endpoint /track/get
#[derive(Debug, Deserialize)]
pub(crate) struct TrackResponse {
    #[serde(deserialize_with = "crate::models::deserialize_id")]
    id: String,
    title: String,
    #[serde(default)]
    performer: Option<ArtistResponse>,
    #[serde(default)]
    artist: Option<ArtistResponse>,
    #[serde(default)]
    album: Option<AlbumResponse>,
    duration: u32,
    track_number: u32,
    media_number: u32,
    #[serde(default = "default_streamable")]
    streamable: bool,
}

/// Réponse artiste
#[derive(Debug, Deserialize)]
pub(crate) struct ArtistResponse {
    #[serde(deserialize_with = "crate::models::deserialize_id")]
    id: String,
    name: String,
    #[serde(default)]
    image: Option<ImageResponse>,
    #[serde(default)]
    albums: Option<PaginatedResponse<AlbumResponse>>,
}

/// Réponse image
#[derive(Debug, Deserialize)]
struct ImageResponse {
    #[serde(default)]
    large: Option<String>,
}

/// Réponse genre
#[derive(Debug, Deserialize)]
pub(crate) struct GenreResponse {
    #[serde(default)]
    id: Option<u32>,
    name: String,
}

/// Réponse label
#[derive(Debug, Deserialize)]
struct LabelResponse {
    name: String,
}

/// Réponse playlist
#[derive(Debug, Deserialize)]
pub(crate) struct PlaylistResponse {
    #[serde(deserialize_with = "crate::models::deserialize_id")]
    id: String,
    name: String,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    tracks_count: Option<u32>,
    #[serde(default)]
    duration: Option<u32>,
    #[serde(default)]
    images300: Option<Vec<String>>,
    #[serde(default)]
    is_public: bool,
    #[serde(default)]
    owner: Option<OwnerResponse>,
    #[serde(default)]
    updated_at: Option<i64>,
    #[serde(default)]
    tracks: Option<PaginatedResponse<TrackResponse>>,
}

/// Réponse propriétaire
#[derive(Debug, Deserialize)]
struct OwnerResponse {
    #[serde(deserialize_with = "crate::models::deserialize_id")]
    id: String,
    name: String,
}

/// Réponse genres list
#[derive(Debug, Deserialize)]
struct GenresResponse {
    genres: PaginatedResponse<GenreResponse>,
}

/// Réponse albums featured
#[derive(Debug, Deserialize)]
struct FeaturedAlbumsResponse {
    albums: PaginatedResponse<AlbumResponse>,
}

/// Réponse playlists featured
#[derive(Debug, Deserialize)]
struct FeaturedPlaylistsResponse {
    playlists: PaginatedResponse<PlaylistResponse>,
}

/// Réponse artistes featured
#[derive(Debug, Deserialize)]
struct FeaturedArtistsResponse {
    artists: PaginatedResponse<ArtistResponse>,
}

/// Réponse search
#[derive(Debug, Deserialize)]
struct SearchResponse {
    #[serde(default)]
    albums: Option<PaginatedResponse<AlbumResponse>>,
    #[serde(default)]
    artists: Option<PaginatedResponse<ArtistResponse>>,
    #[serde(default)]
    tracks: Option<PaginatedResponse<TrackResponse>>,
    #[serde(default)]
    playlists: Option<PaginatedResponse<PlaylistResponse>>,
}

/// Réponse track file URL
#[derive(Debug, Deserialize)]
struct FileUrlResponse {
    url: String,
    mime_type: String,
    sampling_rate: f64,
    bit_depth: u32,
    format_id: u8,
}

/// Réponse de l'endpoint track/getList
#[derive(Debug, Deserialize)]
struct TrackListResponse {
    tracks: TrackListItems,
}

#[derive(Debug, Deserialize)]
struct TrackListItems {
    items: Vec<TrackResponse>,
}

fn default_streamable() -> bool {
    true
}

impl QobuzApi {
    /// Récupère les détails d'un album
    pub async fn get_album(&self, album_id: &str) -> Result<Album> {
        debug!("Fetching album {}", album_id);
        let params = [("album_id", album_id)];
        let response: AlbumResponse = self.get("/album/get", &params).await?;
        Ok(Self::parse_album(response))
    }

    /// Récupère les tracks d'un album
    pub async fn get_album_tracks(&self, album_id: &str) -> Result<Vec<Track>> {
        debug!("Fetching tracks for album {}", album_id);
        let params = [("album_id", album_id)];
        let mut response: AlbumResponse = self.get("/album/get", &params).await?;

        if let Some(tracks) = response.tracks.take() {
            let album = Self::parse_album(response);
            Ok(tracks
                .items
                .into_iter()
                .map(|t| Self::parse_track(t, Some(album.clone())))
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère les détails de plusieurs tracks en une ou plusieurs requêtes batch.
    ///
    /// Utilise l'endpoint `track/getList` (max 50 IDs par appel). Les fenêtres
    /// supérieures à 50 sont découpées et appellées en série. L'ordre de sortie
    /// correspond à l'ordre des `track_ids` en entrée.
    ///
    /// Requiert que le secret s4 soit configuré.
    pub async fn get_tracks_batch(&self, track_ids: &[&str]) -> Result<Vec<Track>> {
        const MAX_PER_CALL: usize = 50;
        if track_ids.is_empty() {
            return Ok(Vec::new());
        }
        if track_ids.len() <= MAX_PER_CALL {
            return self.get_tracks_batch_chunk(track_ids).await;
        }
        debug!("get_tracks_batch: {} IDs en fenêtres de {}", track_ids.len(), MAX_PER_CALL);
        let mut all = Vec::with_capacity(track_ids.len());
        for chunk in track_ids.chunks(MAX_PER_CALL) {
            let mut tracks = self.get_tracks_batch_chunk(chunk).await?;
            all.append(&mut tracks);
        }
        Ok(all)
    }

    async fn get_tracks_batch_chunk(&self, track_ids: &[&str]) -> Result<Vec<Track>> {
        use super::signing;

        let secret = self.secret().ok_or_else(|| {
            QobuzError::Configuration("Secret s4 requis pour track/getList".to_string())
        })?;

        let ids_csv = track_ids.join(",");
        let timestamp = signing::get_timestamp();
        let signature = signing::sign_track_get_list(&ids_csv, &timestamp, &secret);

        let query_params = [
            ("request_ts", timestamp.as_str()),
            ("request_sig", signature.as_str()),
        ];

        // Les IDs sont envoyés comme tableau d'entiers dans le body JSON
        let ids_as_numbers: Vec<u64> = track_ids
            .iter()
            .filter_map(|id| id.parse().ok())
            .collect();
        let body = serde_json::json!({ "tracks_id": ids_as_numbers });

        debug!("get_tracks_batch_chunk POST {} IDs", track_ids.len());
        let response: TrackListResponse = self
            .post_json_with_query("/track/getList", &query_params, body)
            .await?;

        Ok(response
            .tracks
            .items
            .into_iter()
            .map(|t| Self::parse_track(t, None))
            .collect())
    }

    /// Récupère les détails d'une track
    pub async fn get_track(&self, track_id: &str) -> Result<Track> {
        debug!("Fetching track {}", track_id);
        let params = [("track_id", track_id)];
        let response: TrackResponse = self.get("/track/get", &params).await?;
        Ok(Self::parse_track(response, None))
    }

    /// Récupère l'URL de streaming d'une track
    ///
    /// Cette méthode nécessite un secret s4 pour signer la requête.
    /// Si aucun secret n'est configuré, retourne une erreur.
    ///
    /// # Errors
    ///
    /// Retourne `QobuzError::Configuration` si le secret n'est pas configuré.
    pub async fn get_file_url(&self, track_id: &str) -> Result<StreamInfo> {
        use super::signing;

        debug!("Fetching file URL for track {}", track_id);

        // Vérifier que le secret est disponible
        let secret = self.secret().ok_or_else(|| {
            QobuzError::Configuration(
                "Secret not configured. Cannot sign track/getFileUrl request.".to_string(),
            )
        })?;

        let format_id = self.format_id.id().to_string();
        let intent = "stream";
        let timestamp = signing::get_timestamp();

        // Vérifier que le token d'authentification est disponible
        self.auth_token()
            .ok_or_else(|| QobuzError::Unauthorized("Missing auth token".to_string()))?;

        // Signer la requête (comme Python: track_getFileUrl)
        let signature =
            signing::sign_track_get_file_url(&format_id, intent, track_id, &timestamp, &secret);

        debug!(
            "Signing track/getFileUrl: track_id={}, format_id={}, ts={}",
            track_id, format_id, timestamp
        );

        // Construire les paramètres signés
        // Note: app_id et user_auth_token sont envoyés automatiquement comme headers
        // par la méthode request() (X-App-Id et X-User-Auth-Token)
        // IMPORTANT: L'ordre doit correspondre EXACTEMENT à Python (raw.py)
        let params = [
            ("format_id", format_id.as_str()),
            ("intent", intent),
            ("request_ts", timestamp.as_str()),
            ("request_sig", signature.as_str()),
            ("track_id", track_id),
        ];

        // Utiliser GET (comme qobuz-player-client qui fonctionne)
        let response: FileUrlResponse = self.get("/track/getFileUrl", &params).await?;

        Ok(StreamInfo {
            url: response.url,
            mime_type: response.mime_type,
            sampling_rate: response.sampling_rate,
            bit_depth: response.bit_depth,
            format_id: response.format_id,
            expires_at: chrono::Utc::now() + chrono::Duration::minutes(5),
        })
    }

    /// Récupère les albums d'un artiste
    pub async fn get_artist_albums(&self, artist_id: &str) -> Result<Vec<Album>> {
        debug!("Fetching albums for artist {}", artist_id);
        let params = [("artist_id", artist_id), ("extra", "albums")];
        let response: ArtistResponse = self.get("/artist/get", &params).await?;

        if let Some(albums) = response.albums {
            Ok(albums
                .items
                .into_iter()
                .map(Self::parse_album)
                .filter(|a| a.streamable)
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère les artistes similaires
    pub async fn get_similar_artists(&self, artist_id: &str) -> Result<Vec<Artist>> {
        debug!("Fetching similar artists for {}", artist_id);
        let params = [("artist_id", artist_id)];

        #[derive(Debug, Deserialize)]
        struct SimilarArtistsResponse {
            artists: PaginatedResponse<ArtistResponse>,
        }

        let response: SimilarArtistsResponse =
            self.get("/artist/getSimilarArtists", &params).await?;
        Ok(response
            .artists
            .items
            .into_iter()
            .map(Self::parse_artist)
            .collect())
    }

    /// Récupère les métadonnées d'une playlist sans les tracks (appel léger)
    ///
    /// Utile pour vérifier `updated_at` avant de décider si le cache est valide.
    pub async fn get_playlist_metadata(&self, playlist_id: &str) -> Result<Playlist> {
        debug!("Fetching playlist metadata {}", playlist_id);
        let params = [("playlist_id", playlist_id)];
        let response: PlaylistResponse = self.get("/playlist/get", &params).await?;
        Ok(Self::parse_playlist(response))
    }

    /// Récupère les détails d'une playlist
    pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> {
        debug!("Fetching playlist {}", playlist_id);
        let params = [("playlist_id", playlist_id)];
        let response: PlaylistResponse = self.get("/playlist/get", &params).await?;
        Ok(Self::parse_playlist(response))
    }

    /// Récupère les tracks d'une playlist.
    ///
    /// Phase 1 — pagination concurrente :
    ///   - Page 1 séquentielle pour obtenir `total`
    ///   - Pages 2..N lancées en parallèle (semaphore 3) dès que `total` est connu
    ///   - Résultats triés par offset avant fusion
    ///
    /// Phase 2 — enrichissement via `track/getList` pour métadonnées complètes
    /// (performer, sample_rate, bit_depth, channels).
    pub async fn get_playlist_tracks(&self, playlist_id: &str) -> Result<Vec<Track>> {
        use futures::future::try_join_all;
        use std::sync::Arc;
        use tokio::sync::Semaphore;

        const PAGE_SIZE: u32 = 500;
        const LIMIT_STR: &str = "500";
        // Configurable via accounts.qobuz.page_concurrency (défaut 3).
        let max_concurrent_pages = self.page_concurrency;

        debug!("Fetching tracks for playlist {}", playlist_id);

        // Page 1 — séquentielle : récupère les IDs + total
        let first_response: PlaylistResponse = self
            .get(
                "/playlist/get",
                &[
                    ("playlist_id", playlist_id),
                    ("extra", "tracks"),
                    ("offset", "0"),
                    ("limit", LIMIT_STR),
                ],
            )
            .await?;

        let first_page = match first_response.tracks {
            Some(t) => t,
            None => return Ok(Vec::new()),
        };

        let total = first_page.total.unwrap_or(0);
        if total == 0 || first_page.items.is_empty() {
            return Ok(Vec::new());
        }

        // Offsets des pages restantes : 500, 1000, 1500, ...
        let remaining_offsets: Vec<u32> = (PAGE_SIZE..total)
            .step_by(PAGE_SIZE as usize)
            .collect();

        let n_pages = 1 + remaining_offsets.len();

        // Pages 2..N — concurrentes
        let mut pages: Vec<(u32, Vec<TrackResponse>)> =
            Vec::with_capacity(n_pages);
        pages.push((0, first_page.items));

        if !remaining_offsets.is_empty() {
            let sem = Arc::new(Semaphore::new(max_concurrent_pages));

            let futs = remaining_offsets.iter().map(|&off| {
                let sem = sem.clone();
                async move {
                    let _permit = sem.acquire().await.unwrap();
                    let offset_str = off.to_string();
                    let response: PlaylistResponse = self
                        .get(
                            "/playlist/get",
                            &[
                                ("playlist_id", playlist_id),
                                ("extra", "tracks"),
                                ("offset", offset_str.as_str()),
                                ("limit", LIMIT_STR),
                            ],
                        )
                        .await?;
                    let items = response.tracks.map(|t| t.items).unwrap_or_default();
                    Ok::<(u32, Vec<TrackResponse>), QobuzError>((off, items))
                }
            });

            let mut extra = try_join_all(futs).await?;
            pages.append(&mut extra);
        }

        // Tri par offset pour garantir l'ordre de la playlist
        pages.sort_unstable_by_key(|(off, _)| *off);

        let ordered_ids: Vec<String> = pages
            .into_iter()
            .flat_map(|(_, items)| items.into_iter().map(|t| t.id))
            .collect();

        debug!(
            "Fetched {} track IDs for playlist {} ({} pages)",
            ordered_ids.len(), playlist_id, n_pages
        );

        if ordered_ids.is_empty() {
            return Ok(Vec::new());
        }

        // Phase 2 : enrichissement via track/getList pour métadonnées complètes
        let id_refs: Vec<&str> = ordered_ids.iter().map(|s| s.as_str()).collect();
        let full_tracks = self.get_tracks_batch(&id_refs).await?;
        let mut track_map: HashMap<String, Track> =
            full_tracks.into_iter().map(|t| (t.id.clone(), t)).collect();
        let enriched: Vec<Track> = ordered_ids
            .iter()
            .filter_map(|id| track_map.remove(id.as_str()))
            .collect();
        debug!(
            "Fetched {} tracks for playlist {} via track/getList",
            enriched.len(), playlist_id
        );
        Ok(enriched)
    }

    /// Récupère la liste des genres
    pub async fn get_genres(&self) -> Result<Vec<Genre>> {
        debug!("Fetching genres");
        let response: GenresResponse = self.get("/genre/list", &[]).await?;
        Ok(response
            .genres
            .items
            .into_iter()
            .map(Self::parse_genre)
            .collect())
    }

    /// Récupère les albums featured (nouveautés, éditeur, etc.)
    pub async fn get_featured_albums(
        &self,
        genre_id: Option<&str>,
        type_: &str,
    ) -> Result<Vec<Album>> {
        debug!("Fetching featured albums (type: {})", type_);
        let mut params = vec![("type", type_), ("limit", "100")];

        if let Some(gid) = genre_id {
            params.push(("genre_ids", gid));
        }

        let response: FeaturedAlbumsResponse = self.get("/album/getFeatured", &params).await?;
        Ok(response
            .albums
            .items
            .into_iter()
            .map(Self::parse_album)
            .filter(|a| a.streamable)
            .collect())
    }

    /// Récupère les playlists featured
    pub async fn get_featured_playlists(
        &self,
        genre_id: Option<&str>,
        tags: Option<&str>,
    ) -> Result<Vec<Playlist>> {
        debug!("Fetching featured playlists");
        let mut params = vec![("type", "editor-picks"), ("limit", "100")];

        if let Some(gid) = genre_id {
            params.push(("genre_ids", gid));
        }
        if let Some(t) = tags {
            params.push(("tags", t));
        }

        let response: FeaturedPlaylistsResponse =
            self.get("/playlist/getFeatured", &params).await?;
        Ok(response
            .playlists
            .items
            .into_iter()
            .map(Self::parse_playlist)
            .collect())
    }

    /// Récupère les artistes featured
    pub async fn get_featured_artists(
        &self,
        genre_id: Option<&str>,
        limit: Option<u32>,
        offset: Option<u32>,
    ) -> Result<Vec<Artist>> {
        debug!("Fetching featured artists");
        let limit_str = limit.unwrap_or(100).to_string();
        let offset_str = offset.unwrap_or(0).to_string();

        let mut params = vec![
            ("type", "featured-artists"),
            ("limit", &limit_str),
            ("offset", &offset_str),
        ];

        if let Some(gid) = genre_id {
            params.push(("genre_ids", gid));
        }

        let response: FeaturedArtistsResponse = self.get("/artist/getFeatured", &params).await?;
        Ok(response
            .artists
            .items
            .into_iter()
            .map(Self::parse_artist)
            .collect())
    }

    /// Retourne uniquement les totaux de chaque type pour une requête (limit=1 pour minimiser le transfert).
    pub async fn search_totals(&self, query: &str) -> Result<(u32, u32, u32, u32)> {
        let response: SearchResponse = self.get("/catalog/search", &[("query", query), ("limit", "1")]).await?;
        let albums   = response.albums  .as_ref().and_then(|r| r.total).unwrap_or(0);
        let artists  = response.artists .as_ref().and_then(|r| r.total).unwrap_or(0);
        let tracks   = response.tracks  .as_ref().and_then(|r| r.total).unwrap_or(0);
        let playlists= response.playlists.as_ref().and_then(|r| r.total).unwrap_or(0);
        Ok((albums, artists, tracks, playlists))
    }

    /// Recherche dans le catalogue
    pub async fn search(&self, query: &str, type_: Option<&str>) -> Result<SearchResult> {
        debug!("Searching for '{}' (type: {:?})", query, type_);
        let mut params = vec![("query", query), ("limit", "200")];

        if let Some(t) = type_ {
            params.push(("type", t));
        }

        let response: SearchResponse = self.get("/catalog/search", &params).await?;

        Ok(SearchResult {
            albums: response
                .albums
                .map(|a| {
                    a.items
                        .into_iter()
                        .map(Self::parse_album)
                        .filter(|album| album.streamable)
                        .collect()
                })
                .unwrap_or_default(),
            artists: response
                .artists
                .map(|a| a.items.into_iter().map(Self::parse_artist).collect())
                .unwrap_or_default(),
            tracks: response
                .tracks
                .map(|t| {
                    t.items
                        .into_iter()
                        .map(|track| Self::parse_track(track, None))
                        .filter(|track| track.streamable)
                        .collect()
                })
                .unwrap_or_default(),
            playlists: response
                .playlists
                .map(|p| p.items.into_iter().map(Self::parse_playlist).collect())
                .unwrap_or_default(),
        })
    }

    /// Recherche dans les albums uniquement (`/album/search`)
    pub async fn search_albums(&self, query: &str, limit: u32, offset: u32) -> Result<Vec<Album>> {
        let limit_s = limit.to_string();
        let offset_s = offset.to_string();
        let params = [("query", query), ("limit", &limit_s), ("offset", &offset_s)];
        #[derive(Deserialize)]
        struct Resp { albums: PaginatedResponse<AlbumResponse> }
        let resp: Resp = self.get("/album/search", &params).await?;
        Ok(resp.albums.items.into_iter().map(Self::parse_album).filter(|a| a.streamable).collect())
    }

    /// Recherche dans les tracks uniquement (`/track/search`)
    pub async fn search_tracks(&self, query: &str, limit: u32, offset: u32) -> Result<Vec<Track>> {
        let limit_s = limit.to_string();
        let offset_s = offset.to_string();
        let params = [("query", query), ("limit", &limit_s), ("offset", &offset_s)];
        #[derive(Deserialize)]
        struct Resp { tracks: PaginatedResponse<TrackResponse> }
        let resp: Resp = self.get("/track/search", &params).await?;
        Ok(resp.tracks.items.into_iter().map(|t| Self::parse_track(t, None)).filter(|t| t.streamable).collect())
    }

    /// Recherche dans les artistes uniquement (`/artist/search`)
    pub async fn search_artists(&self, query: &str, limit: u32, offset: u32) -> Result<Vec<Artist>> {
        let limit_s = limit.to_string();
        let offset_s = offset.to_string();
        let params = [("query", query), ("limit", &limit_s), ("offset", &offset_s)];
        #[derive(Deserialize)]
        struct Resp { artists: PaginatedResponse<ArtistResponse> }
        let resp: Resp = self.get("/artist/search", &params).await?;
        Ok(resp.artists.items.into_iter().map(Self::parse_artist).collect())
    }

    /// Recherche dans les playlists uniquement (`/playlist/search`)
    pub async fn search_playlists(&self, query: &str, limit: u32, offset: u32) -> Result<Vec<Playlist>> {
        let limit_s = limit.to_string();
        let offset_s = offset.to_string();
        let params = [("query", query), ("limit", &limit_s), ("offset", &offset_s)];
        #[derive(Deserialize)]
        struct Resp { playlists: PaginatedResponse<PlaylistResponse> }
        let resp: Resp = self.get("/playlist/search", &params).await?;
        Ok(resp.playlists.items.into_iter().map(Self::parse_playlist).collect())
    }

    // Fonctions de parsing publiques (utilisées aussi par le module user)

    pub(crate) fn parse_album(response: AlbumResponse) -> Album {
        // Log pour débugger les valeurs audio
        if let Some(rate) = response.maximum_sampling_rate {
            debug!("Album {} - maximum_sampling_rate: {} Hz", response.id, rate);
        } else {
            debug!("Album {} - maximum_sampling_rate: None", response.id);
        }

        Album {
            id: response.id,
            title: response.title,
            artist: Self::parse_artist(response.artist),
            tracks_count: response.tracks_count,
            duration: response.duration,
            release_date: response.release_date_original,
            released_at: response.released_at,
            image: response.image.and_then(|i| i.large),
            image_cached: None,
            streamable: response.streamable,
            description: response.description,
            maximum_sampling_rate: response.maximum_sampling_rate,
            maximum_bit_depth: response.maximum_bit_depth,
            genres: response.genre.map(|g| vec![g.name]).unwrap_or_default(),
            label: response.label.map(|l| l.name),
        }
    }

    pub(crate) fn parse_track(response: TrackResponse, album: Option<Album>) -> Track {
        let performer = response
            .performer
            .or(response.artist)
            .map(Self::parse_artist);

        let album = album.or_else(|| response.album.map(Self::parse_album));

        Track {
            id: response.id,
            title: response.title,
            performer,
            album,
            duration: response.duration,
            track_number: response.track_number,
            media_number: response.media_number,
            streamable: response.streamable,
            mime_type: None,
            sample_rate: None,
            bit_depth: None,
            channels: None,
        }
    }

    pub(crate) fn parse_artist(response: ArtistResponse) -> Artist {
        Artist {
            id: response.id,
            name: response.name,
            image: response.image.and_then(|i| i.large),
            image_cached: None,
        }
    }

    pub(crate) fn parse_playlist(response: PlaylistResponse) -> Playlist {
        Playlist {
            id: response.id,
            name: response.name,
            description: response.description,
            tracks_count: response.tracks_count,
            duration: response.duration,
            image: response.images300.and_then(|imgs| imgs.first().cloned()),
            image_cached: None,
            is_public: response.is_public,
            updated_at: response.updated_at,
            owner: response.owner.map(|o| PlaylistOwner {
                id: o.id.parse().unwrap_or(0),
                name: o.name,
            }),
        }
    }

    pub(crate) fn parse_genre(response: GenreResponse) -> Genre {
        Genre {
            id: response.id,
            name: response.name,
            children: Vec::new(),
        }
    }
}
