use std::fs::write;
use std::path::{Path, PathBuf};

use lofty::file::TaggedFileExt;
use lofty::mp4::AtomData;
use lofty::mp4::{Atom, AtomIdent, Ilst};
use lofty::ogg::VorbisComments;
use lofty::probe::Probe;
use lofty::tag::TagType;
use lofty::{
    TextEncoding,
    config::WriteOptions,
    file::AudioFile,
    id3::v2::{
        BinaryFrame, Frame, FrameId, Id3v2Tag, SyncTextContentType, SynchronizedTextFrame,
        TimestampFormat, UnsynchronizedTextFrame,
    },
};
use lyrx::Lyrics;
use std::result::Result::Err;
use std::result::Result::Ok;

use crate::lyrics_api::Response;

pub struct Lyric {
    response: Response,
    path: PathBuf,
}

pub struct LyricError {
    message: String,
}

impl LyricError {
    pub fn new(message: &str) -> Self {
        Self {
            message: message.to_string(),
        }
    }

    pub fn message(&self) -> &str {
        self.message.as_ref()
    }
}

impl Lyric {
    pub fn new(response: Response, path: PathBuf) -> Self {
        Self { response, path }
    }

    pub fn save_to_file(&self) -> Result<(), LyricError> {
        let path_str = self.path.to_string_lossy();

        match &self.response {
            Response::SyncedLyrics { synced, .. } => save_synced_lyrics(&path_str, synced),
            Response::UnsyncedLyrics(plain) => save_plain_lyrics(&path_str, plain),
            Response::IsInstrumental => save_instrumental(&path_str),
            Response::None => Ok(()),
        }
    }

    pub fn embed(&self) -> Result<(), LyricError> {
        let path_str = self.path.to_string_lossy();

        let (plain, synced) = match &self.response {
            Response::SyncedLyrics { synced, plain } => (plain.as_str(), synced.as_str()),
            Response::UnsyncedLyrics(plain) => (plain.as_str(), ""),
            Response::IsInstrumental => {
                return Err(LyricError::new("Track is instrumental, skip embed"));
            }
            Response::None => unreachable!(),
        };

        embed_lyrics(&path_str, plain, synced)
    }
}

fn write_file(track_path: PathBuf, lyrics: &str) -> Result<(), LyricError> {
    match write(track_path, lyrics) {
        Ok(()) => Ok(()),
        Err(e) => Err(LyricError::new(&e.to_string())),
    }
}

fn save_plain_lyrics(track_path: &str, lyrics: &str) -> Result<(), LyricError> {
    let txt_path = build_txt_path(track_path);

    if txt_path.exists() {
        Err(LyricError::new("File with plain lyrics already exists"))
    } else {
        write_file(txt_path, lyrics)
    }
}

fn save_synced_lyrics(track_path: &str, lyrics: &str) -> Result<(), LyricError> {
    let lrc_path = build_lrc_path(track_path);

    if lrc_path.exists() {
        Err(LyricError::new("File with synced lyrics already exists"))
    } else {
        write_file(lrc_path, lyrics)
    }
}

fn save_instrumental(track_path: &str) -> Result<(), LyricError> {
    let lrc_path = build_lrc_path(track_path);

    if lrc_path.exists() {
        Err(LyricError::new("File with lyrics already exists"))
    } else {
        write_file(lrc_path, "[au: instrumental]")
    }
}

fn build_txt_path(track_path: &str) -> PathBuf {
    let path = Path::new(track_path);

    path.with_extension("txt")
}

fn build_lrc_path(track_path: &str) -> PathBuf {
    let path = Path::new(track_path);

    path.with_extension("lrc")
}

fn embed_lyrics(
    track_path: &str,
    plain_lyrics: &str,
    synced_lyrics: &str,
) -> Result<(), LyricError> {
    match get_tag_type(track_path) {
        Ok(TagType::Id3v2) => embed_lyrics_id3v2(track_path, plain_lyrics, synced_lyrics),
        Ok(TagType::VorbisComments) => embed_lyrics_vorbis(track_path, plain_lyrics, synced_lyrics),
        Ok(TagType::Mp4Ilst) => embed_lyrics_mp4ilst(track_path, plain_lyrics, synced_lyrics),
        Ok(_) => Err(LyricError::new("Unsupported tag")),
        Err(e) => Err(e),
    }
}

fn get_tag_type(track_path: &str) -> Result<TagType, LyricError> {
    let file_probe = match Probe::open(track_path) {
        Ok(p) => p,
        Err(e) => {
            return Err(LyricError::new(&format!("Error to open file {:?}", e)));
        }
    };

    let file = match file_probe.guess_file_type() {
        Ok(t) => match t.read() {
            Ok(f) => f,
            Err(e) => {
                return Err(LyricError::new(&format!(
                    "Error to read parsed file {:?}",
                    e
                )));
            }
        },
        Err(e) => {
            return Err(LyricError::new(&format!(
                "Error to parse file type {:?}:",
                e
            )));
        }
    };

    Ok(file.primary_tag_type())
}

fn embed_lyrics_mp4ilst(
    track_path: &str,
    plain_lyrics: &str,
    synced_lyrics: &str,
) -> Result<(), LyricError> {
    let file_probe = Probe::open(track_path).unwrap();
    let mut file = file_probe.guess_file_type().unwrap().read().unwrap();
    // If primary tag is None, return error
    let Some(mut primary_tag) = file.remove(file.primary_tag_type()) else {
        return Err(LyricError::new("Mp4Ilst not found"));
    };

    let mut mp4ilst_comments: Ilst = primary_tag.into();

    // Mp4a does not support both lyrics, then lscribe will choose one of them
    // Ofc you can create custom field
    // But no application will see it
    let atom = if !synced_lyrics.is_empty() {
        Atom::new(
            AtomIdent::Fourcc(*b"\xA9lyr"),
            AtomData::UTF8(synced_lyrics.to_string()),
        )
    } else if !plain_lyrics.is_empty() {
        Atom::new(
            AtomIdent::Fourcc(*b"\xA9lyr"),
            AtomData::UTF8(plain_lyrics.to_string()),
        )
    } else {
        return Err(LyricError::new("Both lyrics are empty"));
    };
    mp4ilst_comments.insert(atom);
    primary_tag = mp4ilst_comments.into();
    file.insert_tag(primary_tag);
    file.save_to_path(track_path, WriteOptions::default())
        .unwrap();

    Ok(())
}

fn embed_lyrics_vorbis(
    track_path: &str,
    plain_lyrics: &str,
    synced_lyrics: &str,
) -> Result<(), LyricError> {
    let file_probe = Probe::open(track_path).unwrap();
    let mut file = file_probe.guess_file_type().unwrap().read().unwrap();
    // If primary tag is None, return error
    let Some(mut primary_tag) = file.remove(file.primary_tag_type()) else {
        return Err(LyricError::new("VorbisComments not found"));
    };

    let mut vorbis_comments: VorbisComments = primary_tag.into();

    if !plain_lyrics.is_empty() {
        vorbis_comments.insert("UNSYNCEDLYRICS".to_string(), plain_lyrics.to_string());
    } else {
        let _ = vorbis_comments.remove("UNSYNCEDLYRICS");
    }

    if !synced_lyrics.is_empty() {
        vorbis_comments.insert("LYRICS".to_string(), synced_lyrics.to_string());
    } else {
        let _ = vorbis_comments.remove("LYRICS");
    }

    primary_tag = vorbis_comments.into();
    file.insert_tag(primary_tag);
    file.save_to_path(track_path, WriteOptions::default())
        .unwrap();

    Ok(())
}

fn embed_lyrics_id3v2(
    track_path: &str,
    plain_lyrics: &str,
    synced_lyrics: &str,
) -> Result<(), LyricError> {
    let file_probe = Probe::open(track_path).unwrap();
    let mut file = file_probe.guess_file_type().unwrap().read().unwrap();
    // If primary tag is None, return error
    let Some(mut primary_tag) = file.remove(file.primary_tag_type()) else {
        return Err(LyricError::new("ID3v2 not found"));
    };
    let mut id3v2: Id3v2Tag = primary_tag.into();

    insert_uslt_frame(&mut id3v2, plain_lyrics)?;
    insert_sylt_frame(&mut id3v2, synced_lyrics)?;

    primary_tag = id3v2.into();
    file.insert_tag(primary_tag);
    file.save_to_path(track_path, WriteOptions::default())
        .unwrap();

    Ok(())
}

fn insert_uslt_frame(id3v2: &mut Id3v2Tag, plain_lyrics: &str) -> Result<(), LyricError> {
    if !plain_lyrics.is_empty() {
        let frame = UnsynchronizedTextFrame::new(
            TextEncoding::UTF8,
            *b"XXX",
            String::new(),
            plain_lyrics.to_string(),
        );
        id3v2.insert(Frame::UnsynchronizedText(frame));
    } else {
        let _ = id3v2.remove(&FrameId::new("USLT").unwrap());
    }
    Ok(())
}

fn insert_sylt_frame(id3v2: &mut Id3v2Tag, synced_lyrics: &str) -> Result<(), LyricError> {
    if !synced_lyrics.is_empty() {
        let timestamps = synced_lyrics_to_sylt_vec(synced_lyrics)?;

        let frame = SynchronizedTextFrame::new(
            TextEncoding::UTF8,
            *b"XXX",
            TimestampFormat::MS,
            SyncTextContentType::Lyrics,
            None,
            timestamps,
        );

        let frame_bytes = frame.as_bytes(WriteOptions::default()).unwrap();
        id3v2.insert(Frame::Binary(BinaryFrame::new(
            FrameId::new("SYLT").unwrap(),
            frame_bytes,
        )));
    } else {
        let _ = id3v2.remove(&FrameId::new("SYLT").unwrap());
    }
    Ok(())
}

fn synced_lyrics_to_sylt_vec(synced_lyrics: &str) -> Result<Vec<(u32, String)>, LyricError> {
    let lyrics = Lyrics::from_str(synced_lyrics).ok();
    if let Some(lyrics) = lyrics {
        let converted = lyrics.to_vec();
        Ok(converted)
    } else {
        Err(LyricError::new("Error to convert lyrics"))
    }
}
