use std::{borrow::Cow, sync::LazyLock};

pub use lofty::{
    config::WriteOptions,
    tag::{Tag, TagExt},
};
use lofty::{
    error::LoftyError,
    tag::{Accessor, ItemKey, ItemValue, TagItem, TagType, items::Timestamp},
};

use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use lunar_lib::{
    database::{DatabaseEntry, Entry, Id},
    iterator_ext::IteratorExtensions,
};
use regex::Regex;

use crate::{
    config::ExportConfig,
    database::SeleneIdExt,
    library::{
        album::Album,
        artist::{Artist, artists_from_string, extract_from_featuring},
        image_art::ImageArt,
        track::{Track, lyric_data::LyricData},
    },
    lyrics::LyricFormat,
    media_container::ContainerFormat,
};

static INSTRUMENTAL_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?i)\s*\(Inst(?:rumental)?(?:\s+Mix)?\)").unwrap());

#[must_use]
pub fn extract_instrumental(str: &str) -> (Cow<'_, str>, bool) {
    if !INSTRUMENTAL_REGEX.is_match(str) {
        return (Cow::Borrowed(str.trim()), false);
    }

    let returned_str = INSTRUMENTAL_REGEX
        .replace(str, " (Instrumental)")
        .trim()
        .to_owned();
    (Cow::Owned(returned_str), true)
}

pub trait LoftyTagTakeAccessors {
    fn title_and_artists(
        &mut self,
        title_key: ItemKey,
        artist_key: ItemKey,
        artists_key: ItemKey,
    ) -> (Option<String>, Vec<Entry<Artist>>);

    fn track_title_and_artists(&mut self) -> (Option<String>, Vec<Entry<Artist>>) {
        self.title_and_artists(
            ItemKey::TrackTitle,
            ItemKey::TrackArtist,
            ItemKey::TrackArtists,
        )
    }

    fn album_title_and_artists(&mut self) -> (Option<String>, Vec<Entry<Artist>>) {
        self.title_and_artists(
            ItemKey::AlbumTitle,
            ItemKey::AlbumArtist,
            ItemKey::AlbumArtists,
        )
    }

    fn date(&mut self) -> Option<DateTime<Utc>>;

    fn track_num(&mut self) -> Option<u32>;
    fn track_total(&mut self) -> Option<u32>;
    fn disc_num(&mut self) -> Option<u32>;
    fn disc_total(&mut self) -> Option<u32>;

    fn lyrics(&mut self) -> Option<LyricData>;

    fn album(
        &mut self,
        track_title: Option<&str>,
        track_artists: &[Entry<Artist>],
        track_num: Option<u32>,
        disc_num: Option<u32>,
    ) -> Option<(Entry<Album>, Vec<Entry<Artist>>)>;
}

impl LoftyTagTakeAccessors for Tag {
    fn title_and_artists(
        &mut self,
        title_key: ItemKey,
        artist_key: ItemKey,
        artists_key: ItemKey,
    ) -> (Option<String>, Vec<Entry<Artist>>) {
        let mut all_artists = Vec::new();

        self.take_strings(artist_key)
            .flat_map(artists_from_string)
            .for_each(|a| {
                if !all_artists.contains(&a) {
                    all_artists.push(a);
                }
            });

        self.take_strings(artists_key)
            .flat_map(artists_from_string)
            .for_each(|a| {
                if !all_artists.contains(&a) {
                    all_artists.push(a);
                }
            });

        let title = if let Some(title) = self.take_strings(title_key).next() {
            let (title, other_artists) = extract_from_featuring(&title);

            other_artists.into_iter().for_each(|a| {
                if !all_artists.contains(&a) {
                    all_artists.push(a);
                }
            });

            Some(title.into_owned())
        } else {
            None
        };

        (title, all_artists)
    }

    fn date(&mut self) -> Option<DateTime<Utc>> {
        self.take_strings(ItemKey::RecordingDate)
            .next()
            .and_then(|d| {
                Timestamp::parse(&mut d.as_bytes(), lofty::config::ParsingMode::Relaxed)
                    .ok()
                    .flatten()
            })
            .and_then(|ts| {
                let date = NaiveDate::from_ymd_opt(
                    i32::from(ts.year),
                    u32::from(ts.month.unwrap_or(0)),
                    u32::from(ts.day.unwrap_or(0)),
                )?;

                let time = NaiveTime::from_hms_opt(
                    u32::from(ts.hour.unwrap_or(0)),
                    u32::from(ts.minute.unwrap_or(0)),
                    u32::from(ts.second.unwrap_or(0)),
                )?;

                Some(NaiveDateTime::new(date, time).and_utc())
            })
    }

    fn track_num(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::TrackNumber)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn track_total(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::TrackTotal)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn disc_num(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::DiscNumber)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn disc_total(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::DiscTotal)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn lyrics(&mut self) -> Option<LyricData> {
        let synced = self.take_strings(ItemKey::Lyrics).next();
        let plain = self.take_strings(ItemKey::UnsyncLyrics).next();

        if let Some(lyrics) = synced
            && let Ok(lyric_data) = LyricData::infer_from_string(lyrics)
        {
            return Some(lyric_data);
        }

        if let Some(lyrics) = plain
            && let Ok(lyric_data) = LyricData::infer_from_string(lyrics)
        {
            return Some(lyric_data);
        }

        None
    }

    fn album(
        &mut self,
        track_title: Option<&str>,
        track_artists: &[Entry<Artist>],
        track_num: Option<u32>,
        disc_num: Option<u32>,
    ) -> Option<(Entry<Album>, Vec<Entry<Artist>>)> {
        let (album_title, mut artists) = self.album_title_and_artists();
        let track_total = self.track_total();
        let disc_total = self.disc_total();

        // Return None if the album has no title
        let Some(title) = album_title else {
            return None;
        };

        // Return None if the albums listed track total is 1 and the disc total is none or 1
        if track_total == Some(1) && disc_total.is_none_or(|d| d <= 1) {
            return None;
        }

        // Return None if title an album share the same name
        if track_title.is_some_and(|t| t == title) {
            return None;
        }

        // Returns None if the album artists are the same as the track artists
        if artists == track_artists {
            return None;
        }

        // Returns None if no album related number is greater than 1
        if track_num.is_none_or(|v| v <= 1)
            && track_total.is_none_or(|v| v <= 1)
            && disc_num.is_none_or(|v| v <= 1)
            && disc_total.is_none_or(|v| v <= 1)
        {
            return None;
        }

        let id = Id::from_string_hash(&title);
        let mut album =
            Album::new(title, artists.iter().map(Entry::id).collect(), Vec::new()).to_entry(id);
        album.track_total = track_total;
        album.disc_total = disc_total;

        for artist in &mut artists {
            artist.albums.push(id);
        }

        Some((album, artists))
    }
}

pub trait LoftyTagRefAccessors {
    fn set_main_artist(&mut self, main_artist: &Artist, key: ItemKey);
    fn set_main_track_artist(&mut self, main_artist: &Artist) {
        self.set_main_artist(main_artist, ItemKey::TrackArtist);
    }
    fn set_main_album_artist(&mut self, main_artist: &Artist) {
        self.set_main_artist(main_artist, ItemKey::AlbumArtist);
    }

    fn set_track_artists(&mut self, artists: &[Artist]) {
        self.set_artists(artists, ItemKey::TrackArtist);
    }
    fn set_album_artists(&mut self, artists: &[Artist]) {
        self.set_artists(artists, ItemKey::AlbumArtist);
    }

    fn set_title(&mut self, title: String);
    fn set_album(&mut self, album: Option<String>);

    fn has_album(&self) -> bool;

    fn set_lyrics(&mut self, lyric_data: &LyricData);

    fn set_cover_art(&mut self, source: &ImageArt) -> Result<(), LoftyError>;

    fn set_artists<'a>(&mut self, artists: impl IntoIterator<Item = &'a Artist>, key: ItemKey);
}

impl LoftyTagRefAccessors for Tag {
    fn set_main_artist(&mut self, main_artist: &Artist, key: ItemKey) {
        let mut artists = self.take_strings(key).to_vec();

        if let Some(artist_index) = artists.iter().position(|a| *a == main_artist.name()) {
            artists[..=artist_index].rotate_right(1);
        } else {
            artists.insert(0, main_artist.name().to_owned());
        }

        for artist in artists {
            self.push(TagItem::new(key, ItemValue::Text(artist)));
        }
    }

    fn set_title(&mut self, title: String) {
        let (title, _) = extract_from_featuring(&title);
        self.insert_text(ItemKey::TrackTitle, title.into_owned());
    }
    fn set_album(&mut self, album: Option<String>) {
        if let Some(album) = album {
            let (album, _) = extract_from_featuring(&album);
            self.insert_text(ItemKey::AlbumTitle, album.into_owned());
        } else {
            self.remove_key(ItemKey::AlbumTitle);
        }
    }

    fn has_album(&self) -> bool {
        self.get(ItemKey::AlbumTitle).is_some()
    }

    fn set_lyrics(&mut self, lyric_data: &LyricData) {
        match lyric_data {
            LyricData::Instrumental => (),
            LyricData::Plain(plain_lyrics) => {
                self.insert_text(ItemKey::UnsyncLyrics, (*plain_lyrics).to_string());
            }
            LyricData::Synced(synced_lyrics) => {
                self.insert_text(
                    ItemKey::Lyrics,
                    synced_lyrics.to_lyrics(LyricFormat::Lrc { a2: false }),
                );
            }
        }
    }

    fn set_artists<'a>(&mut self, artists: impl IntoIterator<Item = &'a Artist>, key: ItemKey) {
        match self.tag_type() {
            TagType::Ape | TagType::Id3v1 | TagType::Id3v2 | TagType::Mp4Ilst => {
                apply_single_artist(self, artists, ';', key);
            }
            TagType::VorbisComments => apply_multiple_artists(self, artists, key),
            TagType::RiffInfo | TagType::AiffText => apply_single_artist(self, artists, ';', key),
            _ => {
                unimplemented!(
                    "Support for this tag type has not been implemented as it is a new type"
                )
            }
        }
    }

    fn set_cover_art(&mut self, source: &ImageArt) -> Result<(), LoftyError> {
        self.set_picture(0, source.to_picture()?);
        Ok(())
    }
}

fn apply_multiple_artists<'a>(
    tags: &mut Tag,
    artists: impl IntoIterator<Item = &'a Artist>,
    key: ItemKey,
) {
    for artist in artists {
        tags.push_unchecked(TagItem::new(key, ItemValue::Text(artist.name().to_owned())));
    }
}

fn apply_single_artist<'a>(
    tags: &mut Tag,
    artists: impl IntoIterator<Item = &'a Artist>,
    sep: char,
    key: ItemKey,
) {
    let artists = artists
        .into_iter()
        .map(Artist::name)
        .to_vec()
        .join(&sep.to_string());

    tags.insert_text(key, artists);
}

pub trait LoftyTagExt {
    fn metadata_key_values<'a, I: IntoIterator<Item = &'a Artist>>(
        export_settings: &ExportConfig,
        track: &Track,
        track_artists: I,
        album: Option<(Album, I, Option<u32>, Option<u32>)>,
    ) -> Self;
}

impl LoftyTagExt for Tag {
    fn metadata_key_values<'a, I: IntoIterator<Item = &'a Artist>>(
        export_settings: &ExportConfig,
        track: &Track,
        track_artists: I,
        album: Option<(Album, I, Option<u32>, Option<u32>)>,
    ) -> Self {
        let track_artists = track_artists.into_iter().to_vec();

        let mut tags = match track.container().format {
            ContainerFormat::Flac | ContainerFormat::Ogg => Tag::new(TagType::VorbisComments),
            ContainerFormat::Ape => Tag::new(TagType::Ape),
            ContainerFormat::Mp3 | ContainerFormat::Wav | ContainerFormat::Aiff => {
                Tag::new(TagType::Id3v2)
            }
            ContainerFormat::Mp4 => Tag::new(TagType::Mp4Ilst),
        };

        if let Some((album, artists, track_num, disc_num)) = album {
            tags.insert_text(ItemKey::AlbumTitle, album.name().to_owned());
            tags.set_artists(artists, ItemKey::AlbumArtist);

            if let Some(track_total) = album.track_total {
                tags.set_track_total(track_total);
            }
            if let Some(disc_total) = album.disc_total {
                tags.set_disk_total(disc_total);
            }
            if let Some(track_num) = track_num {
                tags.set_track(track_num);
            }
            if let Some(disc_num) = disc_num {
                tags.set_disk_total(disc_num);
            }
        } else if export_settings.singles_as_albums {
            tags.insert_text(ItemKey::AlbumTitle, track.metadata.safe_title().to_owned());

            tags.set_artists(track_artists.iter().copied(), ItemKey::AlbumArtist);

            tags.set_track_total(1);
            tags.set_disk_total(1);
            tags.set_track(1);
            tags.set_disk_total(1);
        }

        tags.set_artists(track_artists, ItemKey::TrackArtist);

        if let Some(date) = track.metadata.date {
            tags.insert_text(
                ItemKey::RecordingDate,
                date.format("%Y-%m-%dT%H:%M:%S").to_string(),
            );
        }

        for genre in &track.metadata.genre {
            tags.set_genre(genre.to_owned());
        }

        if let Some(title) = &track.metadata.title {
            Accessor::set_title(&mut tags, title.to_owned());
        }

        if let Some(lyric_data) = &track.metadata.lyric_data {
            match lyric_data {
                LyricData::Instrumental => (),
                LyricData::Plain(lyrics) => {
                    tags.insert_text(ItemKey::UnsyncLyrics, lyrics.to_string());
                }
                LyricData::Synced(lyrics) => {
                    tags.insert_text(
                        ItemKey::Lyrics,
                        lyrics.to_lyrics(LyricFormat::Lrc { a2: false }),
                    );
                }
            }
        }

        if let Some(loudnorm_analysis) = track.loudnorm_analysis() {
            tags.insert_text(
                ItemKey::ReplayGainTrackGain,
                format!("{} dB", loudnorm_analysis.calculated_gain_db()),
            );
            tags.insert_text(
                ItemKey::ReplayGainTrackPeak,
                format!("{}", loudnorm_analysis.calculated_replay_gain_peak()),
            );
        }

        track.metadata.other.iter().for_each(|(k, v)| {
            if let Some(item_key) = ItemKey::from_key(TagType::VorbisComments, k) {
                tags.insert_text(item_key, v.to_owned());
            }
        });

        tags
    }
}
