use crate::prelude::*;
use lofty::prelude::Accessor;
use lofty::tag::{ItemKey, Tag};

/// Verify FLAC files have required metadata tags.
pub(crate) struct TagVerifier;

impl TagVerifier {
    /// Verify required tags on a FLAC file and return any tag issues.
    ///
    /// Checks both raw Vorbis tags (presence) and `ID3v2` tags (convertibility).
    /// Tags missing from Vorbis produce [`SourceIssue::MissingTags`].
    /// Tags present in Vorbis but lost during `ID3v2` conversion produce
    /// [`SourceIssue::InvalidTags`].
    pub(crate) fn execute(
        flac: &FlacFile,
        source: &Source,
    ) -> Result<Vec<SourceIssue>, Failure<TranscodeAction>> {
        let vorbis = match flac.vorbis_tags() {
            Ok(tags) => tags,
            Err(failure) if is_no_vorbis_comments(&failure) => {
                return Ok(vec![SourceIssue::NoTags {
                    path: flac.path.clone(),
                }]);
            }
            Err(failure) => return Err(failure),
        };
        let id3 = flac.id3_tags()?;
        let id3 = Tag::from(id3.clone());
        let vorbis_missing = collect_missing(vorbis, source, flac);
        let id3_missing = collect_missing(&id3, source, flac);
        let invalid: Vec<String> = id3_missing
            .into_iter()
            .filter(|tag| !vorbis_missing.contains(tag))
            .collect();
        let mut issues = Vec::new();
        if !vorbis_missing.is_empty() {
            issues.push(SourceIssue::MissingTags {
                path: flac.path.clone(),
                tags: vorbis_missing,
            });
        }
        if !invalid.is_empty() {
            issues.push(SourceIssue::InvalidTags {
                path: flac.path.clone(),
                tags: invalid,
            });
        }
        Ok(issues)
    }
}

/// Collect tag names that are missing from the given tags.
fn collect_missing(tags: &Tag, source: &Source, flac: &FlacFile) -> Vec<String> {
    let mut missing = Vec::new();
    missing.extend(check_artist_tag(tags));
    missing.extend(check_album_tag(tags));
    missing.extend(check_title_tag(tags));
    missing.extend(check_composer_tag(tags, source));
    missing.extend(check_track_number_tag(tags));
    missing.extend(check_disc_number_tag(tags, flac));
    missing
}

/// Check the artist tag is present.
pub(crate) fn check_artist_tag(tags: &Tag) -> Option<String> {
    if tags.artist().is_none() {
        return Some("artist".to_owned());
    }
    None
}

/// Check the album tag is present.
pub(crate) fn check_album_tag(tags: &Tag) -> Option<String> {
    if tags.album().is_none() {
        return Some("album".to_owned());
    }
    None
}

/// Check the title tag is present.
pub(crate) fn check_title_tag(tags: &Tag) -> Option<String> {
    if tags.title().is_none() {
        return Some("title".to_owned());
    }
    None
}

/// Check the composer tag is present when the source is classical with credited composers.
pub(crate) fn check_composer_tag(tags: &Tag, source: &Source) -> Option<String> {
    let is_classical = source.group.tags.contains(&"classical".to_owned());
    let has_composers = source
        .group
        .music_info
        .as_ref()
        .is_some_and(|info| !info.composers.is_empty());
    if is_classical && has_composers && tags.get(ItemKey::Composer).is_none() {
        return Some("composer".to_owned());
    }
    None
}

/// Check the track number tag is present as a string value.
pub(crate) fn check_track_number_tag(tags: &Tag) -> Option<String> {
    if tags.get_string(ItemKey::TrackNumber).is_none() {
        return Some("track_number".to_owned());
    }
    None
}

/// Check the disc number tag is present when the source is multi-disc.
pub(crate) fn check_disc_number_tag(tags: &Tag, flac: &FlacFile) -> Option<String> {
    let is_multi_disc = flac
        .disc_context
        .as_ref()
        .is_some_and(|ctx| ctx.is_multi_disc);
    if is_multi_disc && tags.get_string(ItemKey::DiscNumber).is_none() {
        return Some("disc_number".to_owned());
    }
    None
}

/// Walk the source chain looking for [`TagsError::NoVorbisComments`].
fn is_no_vorbis_comments(failure: &Failure<TranscodeAction>) -> bool {
    let mut source: Option<&(dyn Error + 'static)> = failure.source();
    while let Some(err) = source {
        if let Some(tags_err) = err.downcast_ref::<TagsError>() {
            return matches!(tags_err, TagsError::NoVorbisComments);
        }
        source = err.source();
    }
    false
}
