use std::{borrow::Cow, fmt::Debug};

use crate::{
    client::response::url_endpoint::NavigationEndpoint,
    error::{Error, ExtractionError},
    model::{
        paginator::{ContinuationEndpoint, Paginator},
        richtext::RichText,
        AlbumId, ChannelId, MusicAlbum, MusicPlaylist, TrackItem, TrackType,
    },
    serializer::{text::TextComponents, MapResult},
    util::{self, TryRemove, DOT_SEPARATOR},
};

use self::response::url_endpoint::MusicPageType;

use super::{
    response::{
        self,
        music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper},
    },
    ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
};

impl RustyPipeQuery {
    /// Get a playlist from YouTube Music
    #[tracing::instrument(skip(self), level = "error")]
    pub async fn music_playlist<S: AsRef<str> + Debug>(
        &self,
        playlist_id: S,
    ) -> Result<MusicPlaylist, Error> {
        let playlist_id = playlist_id.as_ref();
        let request_body = QBrowse {
            browse_id: &format!("VL{playlist_id}"),
        };

        self.execute_request::<response::MusicPlaylist, _, _>(
            ClientType::DesktopMusic,
            "music_playlist",
            playlist_id,
            "browse",
            &request_body,
        )
        .await
    }

    /// Get an album from YouTube Music
    #[tracing::instrument(skip(self), level = "error")]
    pub async fn music_album<S: AsRef<str> + Debug>(
        &self,
        album_id: S,
    ) -> Result<MusicAlbum, Error> {
        let album_id = album_id.as_ref();
        let request_body = QBrowse {
            browse_id: album_id,
        };

        let mut album = self
            .execute_request::<response::MusicPlaylist, MusicAlbum, _>(
                ClientType::DesktopMusic,
                "music_album",
                album_id,
                "browse",
                &request_body,
            )
            .await?;

        // In rare cases, albums may have track numbers =0 (example: MPREb_RM0QfZ0eSKL)
        // They should be replaced with the track number derived from the previous track.
        let mut n_prev = 0;
        for track in &mut album.tracks {
            let tn = track.track_nr.unwrap_or_default();
            if tn == 0 {
                n_prev += 1;
                track.track_nr = Some(n_prev);
            } else {
                n_prev = tn;
            }
        }

        // YouTube Music is replacing album tracks with their respective music videos. To get the original
        // tracks, we have to fetch the album as a playlist and replace the offending track ids.
        if let Some(playlist_id) = &album.playlist_id {
            // Get a list of music videos in the album
            let to_replace = album
                .tracks
                .iter()
                .enumerate()
                .filter_map(|(i, track)| {
                    if track.track_type.is_video() {
                        Some((i, track.name.clone()))
                    } else {
                        None
                    }
                })
                .collect::<Vec<_>>();

            if !to_replace.is_empty() {
                let mut playlist = self.music_playlist(playlist_id).await?;
                playlist
                    .tracks
                    .extend_limit(&self, album.tracks.len())
                    .await?;

                for (i, title) in to_replace {
                    let found_track = playlist.tracks.items.iter().find_map(|track| {
                        if track.name == title && track.track_type.is_track() {
                            Some((track.id.clone(), track.duration))
                        } else {
                            None
                        }
                    });
                    if let Some((track_id, duration)) = found_track {
                        album.tracks[i].id = track_id;
                        if let Some(duration) = duration {
                            album.tracks[i].duration = Some(duration);
                        }
                        album.tracks[i].track_type = TrackType::Track;
                    }
                }
            }
        }
        Ok(album)
    }

    /// Get all liked tracks of the logged-in user
    ///
    /// Requires authentication cookies.
    pub async fn music_liked_tracks(&self) -> Result<MusicPlaylist, Error> {
        self.clone()
            .authenticated()
            .music_playlist("LM")
            .await
            .map_err(util::map_internal_playlist_err)
    }
}

impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
    fn map_response(
        self,
        ctx: &MapRespCtx<'_>,
    ) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
        // dbg!(&self);

        let (header, music_contents) = match self.contents {
            response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
                self.header,
                c.contents
                    .into_iter()
                    .next()
                    .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
                    .tab_renderer
                    .content
                    .section_list_renderer,
            ),
            response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
                secondary_contents,
                tabs,
            } => (
                tabs.into_iter()
                    .next()
                    .and_then(|t| {
                        t.tab_renderer
                            .content
                            .section_list_renderer
                            .contents
                            .into_iter()
                            .next()
                    })
                    .or(self.header),
                secondary_contents.section_list_renderer,
            ),
        };
        let shelf = music_contents
            .contents
            .into_iter()
            .find_map(|section| match section {
                response::music_item::ItemSection::MusicShelfRenderer(shelf) => Some(shelf),
                _ => None,
            })
            .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
                "no sectionListRenderer content",
            )))?;

        if let Some(playlist_id) = shelf.playlist_id {
            if playlist_id != ctx.id {
                return Err(ExtractionError::WrongResult(format!(
                    "got wrong playlist id {}, expected {}",
                    playlist_id, ctx.id
                )));
            }
        }

        let mut mapper = MusicListMapper::new(ctx.lang);
        mapper.map_response(shelf.contents);
        let map_res = mapper.conv_items();

        let ctoken = shelf
            .continuations
            .into_iter()
            .next()
            .map(|cont| cont.next_continuation_data.continuation);

        let track_count = if ctoken.is_some() {
            header.as_ref().and_then(|h| {
                let parts = h
                    .music_detail_header_renderer
                    .second_subtitle
                    .split(|p| p == DOT_SEPARATOR)
                    .collect::<Vec<_>>();
                parts
                    .get(usize::from(parts.len() > 2))
                    .and_then(|txt| util::parse_numeric::<u64>(&txt[0]).ok())
            })
        } else {
            Some(map_res.c.len() as u64)
        };

        let related_ctoken = music_contents
            .continuations
            .into_iter()
            .next()
            .map(|c| c.next_continuation_data.continuation);

        let (from_ytm, channel, name, thumbnail, description) = match header {
            Some(header) => {
                let h = header.music_detail_header_renderer;

                let (from_ytm, channel) = match h.facepile {
                    Some(facepile) => {
                        let from_ytm = facepile.avatar_stack_view_model.text.starts_with("YouTube");
                        let channel = facepile
                            .avatar_stack_view_model
                            .renderer_context
                            .command_context
                            .and_then(|c| {
                                c.on_tap
                                    .innertube_command
                                    .music_page()
                                    .filter(|p| p.typ == MusicPageType::User)
                                    .map(|p| p.id)
                            })
                            .map(|id| ChannelId {
                                id,
                                name: facepile.avatar_stack_view_model.text,
                            });

                        (from_ytm && channel.is_none(), channel)
                    }
                    None => {
                        let st = match h.strapline_text_one {
                            Some(s) => s,
                            None => h.subtitle,
                        };

                        let from_ytm = st.0.iter().any(util::is_ytm);
                        let channel = st.0.into_iter().find_map(|c| ChannelId::try_from(c).ok());
                        (from_ytm, channel)
                    }
                };

                (
                    from_ytm,
                    channel,
                    h.title,
                    h.thumbnail.into(),
                    h.description.map(TextComponents::from),
                )
            }
            None => {
                // Album playlists fetched via the playlist method dont include a header
                let (album, cover) = map_res
                    .c
                    .first()
                    .and_then(|t: &TrackItem| {
                        t.album.as_ref().map(|a| (a.clone(), t.cover.clone()))
                    })
                    .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
                        "playlist without header or album items",
                    )))?;

                if !map_res.c.iter().all(|t| {
                    t.album
                        .as_ref()
                        .map(|a| a.id == album.id)
                        .unwrap_or_default()
                }) {
                    return Err(ExtractionError::InvalidData(Cow::Borrowed(
                        "album playlist containing items from different albums",
                    )));
                }

                (true, None, album.name, cover, None)
            }
        };

        Ok(MapResult {
            c: MusicPlaylist {
                id: ctx.id.to_owned(),
                name,
                thumbnail,
                channel,
                description: description.map(RichText::from),
                track_count,
                from_ytm,
                tracks: Paginator::new_ext(
                    track_count,
                    map_res.c,
                    ctoken,
                    ctx.visitor_data.map(str::to_owned),
                    ContinuationEndpoint::MusicBrowse,
                    ctx.authenticated,
                ),
                related_playlists: Paginator::new_ext(
                    None,
                    Vec::new(),
                    related_ctoken,
                    ctx.visitor_data.map(str::to_owned),
                    ContinuationEndpoint::MusicBrowse,
                    ctx.authenticated,
                ),
            },
            warnings: map_res.warnings,
        })
    }
}

impl MapResponse<MusicAlbum> for response::MusicPlaylist {
    fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicAlbum>, ExtractionError> {
        // dbg!(&self);

        let (header, sections) = match self.contents {
            response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
                self.header,
                c.contents
                    .into_iter()
                    .next()
                    .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
                    .tab_renderer
                    .content
                    .section_list_renderer
                    .contents,
            ),
            response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
                secondary_contents,
                tabs,
            } => (
                tabs.into_iter()
                    .next()
                    .and_then(|t| {
                        t.tab_renderer
                            .content
                            .section_list_renderer
                            .contents
                            .into_iter()
                            .next()
                    })
                    .or(self.header),
                secondary_contents.section_list_renderer.contents,
            ),
        };
        let header = header
            .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no header")))?
            .music_detail_header_renderer;

        let mut shelf = None;
        let mut album_variants = None;
        for section in sections {
            match section {
                response::music_item::ItemSection::MusicShelfRenderer(sh) => shelf = Some(sh),
                response::music_item::ItemSection::MusicCarouselShelfRenderer(sh) => {
                    album_variants = Some(sh.contents);
                }
                _ => (),
            }
        }
        let shelf = shelf.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
            "no sectionListRenderer content",
        )))?;

        let mut subtitle_split = header.subtitle.split(util::DOT_SEPARATOR);

        let (year_txt, artists_p) = match header.strapline_text_one {
            // New (2column) album layout
            Some(sl) => {
                let year_txt = subtitle_split
                    .try_swap_remove(1)
                    .and_then(|t| t.0.first().map(|c| c.as_str().to_owned()));
                (year_txt, Some(sl))
            }
            // Old album layout
            None => match subtitle_split.len() {
                3.. => {
                    let year_txt = subtitle_split
                        .swap_remove(2)
                        .0
                        .first()
                        .map(|c| c.as_str().to_owned());
                    (year_txt, subtitle_split.try_swap_remove(1))
                }
                2 => {
                    // The second part may either be the year or the artist
                    let p2 = subtitle_split.swap_remove(1);
                    let is_year =
                        p2.0.len() == 1 && p2.0[0].as_str().chars().all(|c| c.is_ascii_digit());
                    if is_year {
                        (Some(p2.0[0].as_str().to_owned()), None)
                    } else {
                        (None, Some(p2))
                    }
                }
                _ => (None, None),
            },
        };

        let (artists, by_va) = map_artists(artists_p);
        let album_type_txt = subtitle_split
            .into_iter()
            .next()
            .map(|part| part.to_string())
            .unwrap_or_default();

        let album_type = map_album_type(album_type_txt.as_str(), ctx.lang);
        let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());

        fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
            if let NavigationEndpoint::WatchPlaylist {
                watch_playlist_endpoint,
            } = ep
            {
                Some(watch_playlist_endpoint.playlist_id.to_owned())
            } else {
                None
            }
        }

        let playlist_id = self.microformat.and_then(|mf| {
            mf.microformat_data_renderer
                .url_canonical
                .strip_prefix("https://music.youtube.com/playlist?list=")
                .map(str::to_owned)
        });
        let (playlist_id, artist_id) = header
            .menu
            .or_else(|| header.buttons.into_iter().next())
            .map(|menu| {
                (
                    playlist_id.or_else(|| {
                        menu.menu_renderer
                            .top_level_buttons
                            .iter()
                            .find_map(|btn| {
                                map_playlist_id(&btn.button_renderer.navigation_endpoint)
                            })
                            .or_else(|| {
                                menu.menu_renderer.items.iter().find_map(|itm| {
                                    map_playlist_id(
                                        &itm.menu_navigation_item_renderer.navigation_endpoint,
                                    )
                                })
                            })
                    }),
                    map_artist_id(menu.menu_renderer.items),
                )
            })
            .unwrap_or_default();
        let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone()));

        let mut mapper = MusicListMapper::with_album(
            ctx.lang,
            artists.clone(),
            by_va,
            AlbumId {
                id: ctx.id.to_owned(),
                name: header.title.clone(),
            },
        );
        mapper.map_response(shelf.contents);
        let tracks_res = mapper.conv_items();
        let mut warnings = tracks_res.warnings;

        let mut variants_mapper = MusicListMapper::new(ctx.lang);
        if let Some(res) = album_variants {
            variants_mapper.map_response(res);
        }
        let mut variants_res = variants_mapper.conv_items();
        warnings.append(&mut variants_res.warnings);

        Ok(MapResult {
            c: MusicAlbum {
                id: ctx.id.to_owned(),
                playlist_id,
                name: header.title,
                cover: header.thumbnail.into(),
                artists,
                artist_id,
                description: header
                    .description
                    .map(|t| RichText::from(TextComponents::from(t))),
                album_type,
                year,
                by_va,
                tracks: tracks_res.c,
                variants: variants_res.c,
            },
            warnings,
        })
    }
}

#[cfg(test)]
mod tests {
    use std::{fs::File, io::BufReader};

    use path_macro::path;
    use rstest::rstest;

    use super::*;
    use crate::{model, util::tests::TESTFILES};

    #[rstest]
    #[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
    #[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
    #[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
    #[case::two_columns("20240228_twoColumns", "RDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM")]
    #[case::n_album("20240228_album", "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0")]
    #[case::facepile("20241125_facepile", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
    fn map_music_playlist(#[case] name: &str, #[case] id: &str) {
        let json_path = path!(*TESTFILES / "music_playlist" / format!("playlist_{name}.json"));
        let json_file = File::open(json_path).unwrap();

        let playlist: response::MusicPlaylist =
            serde_json::from_reader(BufReader::new(json_file)).unwrap();
        let map_res: MapResult<model::MusicPlaylist> =
            playlist.map_response(&MapRespCtx::test(id)).unwrap();

        assert!(
            map_res.warnings.is_empty(),
            "deserialization/mapping warnings: {:?}",
            map_res.warnings
        );
        insta::assert_ron_snapshot!(format!("map_music_playlist_{name}"), map_res.c, {
            ".last_update" => "[date]"
        });
    }

    #[rstest]
    #[case::one_artist("one_artist", "MPREb_nlBWQROfvjo")]
    #[case::various_artists("various_artists", "MPREb_8QkDeEIawvX")]
    #[case::single("single", "MPREb_bHfHGoy7vuv")]
    #[case::description("description", "MPREb_PiyfuVl6aYd")]
    #[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
    #[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
    #[case::two_columns("20240228_twoColumns", "MPREb_bHfHGoy7vuv")]
    fn map_music_album(#[case] name: &str, #[case] id: &str) {
        let json_path = path!(*TESTFILES / "music_playlist" / format!("album_{name}.json"));
        let json_file = File::open(json_path).unwrap();

        let playlist: response::MusicPlaylist =
            serde_json::from_reader(BufReader::new(json_file)).unwrap();
        let map_res: MapResult<model::MusicAlbum> =
            playlist.map_response(&MapRespCtx::test(id)).unwrap();

        assert!(
            map_res.warnings.is_empty(),
            "deserialization/mapping warnings: {:?}",
            map_res.warnings
        );
        insta::assert_ron_snapshot!(format!("map_music_album_{name}"), map_res.c);
    }
}
