use std::{convert::Infallible, sync::Arc};

use chrono::{DateTime, Utc};
use lunar_lib::{
    database::{EntryIdExt, EntryIdIteratorExt, TransactionError},
    formatter::{FormatTable, Taggable},
    paths::sys::sanitize_str,
};
use serde::{Deserialize, Serialize};

use crate::{
    database::LibraryDb,
    library::{
        album::{Album, AlbumId},
        artist::{Artist, ArtistGroup, ArtistId},
        track::{UNKNOWN_TITLE, cover_art::CoverArt, lyric_data::LyricData},
    },
};

/// Track metadata. Defines metadata to be read from and stored on files
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct TrackMeta {
    pub(crate) album: Option<AlbumId>,
    pub(crate) artists: Vec<ArtistId>,
    pub date: Option<DateTime<Utc>>,
    pub genre: Vec<String>,
    pub lyric_data: Option<LyricData>,
    pub art: Option<CoverArt>,
    pub other: Vec<(String, String)>,
    pub title: Option<String>,
}

impl TrackMeta {
    #[must_use]
    pub fn new() -> Self {
        Self {
            title: None,
            artists: Vec::new(),
            album: None,

            date: None,
            genre: Vec::new(),
            art: None,

            lyric_data: None,

            other: Vec::new(),
        }
    }

    #[must_use]
    pub fn artists_raw(&self) -> &[ArtistId] {
        &self.artists
    }

    pub fn artists(&self, db: &LibraryDb) -> Result<Vec<Artist>, TransactionError> {
        self.artists.iter().copied().db_get_batch(db)
    }

    pub fn artists_cache(&self, db: &LibraryDb) -> Result<Vec<Arc<Artist>>, TransactionError> {
        self.artists.iter().copied().cache_get_batch(db)
    }

    #[must_use]
    pub fn album_raw(&self) -> Option<AlbumId> {
        self.album
    }

    pub fn album(&self, db: &LibraryDb) -> Result<Option<Album>, TransactionError> {
        if let Some(album_id) = self.album {
            Ok(Some(
                album_id.db_get(db)?.expect("Dangling album reference"),
            ))
        } else {
            Ok(None)
        }
    }

    pub fn album_cache(&self, db: &LibraryDb) -> Result<Option<Arc<Album>>, TransactionError> {
        if let Some(album_id) = self.album {
            Ok(Some(
                album_id.cache_get(db)?.expect("Dangling album reference"),
            ))
        } else {
            Ok(None)
        }
    }

    pub fn main_artist(&self, db: &LibraryDb) -> Result<Option<Artist>, TransactionError> {
        self.artists.main_artist(db)
    }

    #[must_use]
    pub fn safe_title(&self) -> &str {
        self.title.as_deref().unwrap_or(UNKNOWN_TITLE)
    }
}

impl Taggable for TrackMeta {
    type Err = Infallible;

    fn fill_table(&self, table: &mut FormatTable) -> Result<(), Self::Err> {
        if let Some(value) = self.date {
            table.add_entry("date", value.to_string());
        }
        if let Some(value) = &self.title {
            table.add_entry("title", sanitize_str(value));
        }
        if !self.genre.is_empty() {
            table.add_entry("genre", sanitize_str(self.genre.join(";")));
        }

        Ok(())
    }
}

pub struct TrackAlbumInfo {
    pub album: Album,
    pub track_num: Option<u32>,
    pub disc_num: Option<u32>,
}
