use std::{
    cell::{Cell, RefCell},
    collections::{HashMap, HashSet},
    ops::AddAssign,
    path::PathBuf,
    time::{Duration, Instant},
};

use anyhow::{Context, Result, bail};
use bon::bon;
use crossbeam::channel::{SendError, Sender, bounded};
use rmpc_mpd::{
    client::Client,
    commands::{Song, State, Status},
    mpd_client::MpdClient,
};
use rmpc_shared::version::Version;

use crate::{
    AppEvent,
    MpdCommand,
    MpdQuery,
    MpdQueryResult,
    WorkRequest,
    config::{
        Config,
        album_art::ImageMethod,
        tabs::{PaneType, TabName},
    },
    core::scheduler::{Scheduler, time_provider::DefaultTimeProvider},
    shared::{
        events::ClientRequest,
        keys::KeyResolver,
        lrc::{Lrc, LrcIndex},
        macros::{status_error, status_warn},
        mpd_client_ext::MpdClientExt,
        mpd_query::MpdQuerySync,
        ring_vec::RingVec,
        ytdlp::YtDlpManager,
    },
    ui::{StatusMessage, input::InputManager},
};

pub const FETCH_SONG_STICKERS: &str = "fetch_song_stickers";
pub const LIKE_STICKER: &str = "like";
pub const RATING_STICKER: &str = "rating";

#[derive(derive_more::Debug)]
pub struct Ctx {
    pub(crate) mpd_version: Version,
    pub(crate) config: std::sync::Arc<Config>,
    pub(crate) status: Status,
    #[cfg(test)]
    pub(crate) current_song: Option<Song>,
    #[cfg(not(test))]
    current_song: Option<Song>,
    pub(crate) queue: Vec<Song>,
    #[cfg(test)]
    pub(crate) stickers: HashMap<String, HashMap<String, String>>,
    #[cfg(not(test))]
    stickers: HashMap<String, HashMap<String, String>>,
    pub(crate) active_tab: TabName,
    pub(crate) supported_commands: HashSet<String>,
    pub(crate) db_update_start: Option<Instant>,
    #[debug(skip)]
    pub(crate) app_event_sender: Sender<AppEvent>,
    #[debug(skip)]
    pub(crate) work_sender: Sender<WorkRequest>,
    #[debug(skip)]
    pub(crate) client_request_sender: Sender<ClientRequest>,
    pub(crate) needs_render: Cell<bool>,
    pub(crate) stickers_to_fetch: RefCell<HashSet<String>>,
    #[debug(skip)]
    pub(crate) lrc_index: LrcIndex,
    pub(crate) rendered_frames: u64,
    #[debug(skip)]
    pub(crate) scheduler: Scheduler<(Sender<AppEvent>, Sender<ClientRequest>), DefaultTimeProvider>,
    pub(crate) messages: RingVec<10, StatusMessage>,
    pub(crate) last_status_update: Instant,
    pub(crate) song_played: Option<Duration>,
    pub(crate) stickers_supported: StickersSupport,
    pub(crate) input: InputManager,
    pub(crate) key_resolver: KeyResolver,
    pub(crate) ytdlp_manager: YtDlpManager,
    pub(crate) cached_queue_time_total: Duration,
}

#[bon]
impl Ctx {
    pub(crate) fn try_new(
        client: &mut Client<'_>,
        mut config: Config,
        app_event_sender: Sender<AppEvent>,
        work_sender: Sender<WorkRequest>,
        client_request_sender: Sender<ClientRequest>,
        mut scheduler: Scheduler<(Sender<AppEvent>, Sender<ClientRequest>), DefaultTimeProvider>,
    ) -> Result<Self> {
        let supported_commands: HashSet<String> = client.supported_commands.clone();
        let stickers_supported = if supported_commands.contains("sticker") {
            StickersSupport::Supported
        } else {
            StickersSupport::Unsupported
        };
        log::debug!(supported_commands:? = supported_commands; "Supported commands by server");

        let status = client.get_status()?;
        let queue = client.playlist_info()?.unwrap_or_default();
        let current_song = client.get_current_song()?;
        let cached_queue_time_total = queue.iter().filter_map(|s| s.duration).sum();

        if !supported_commands.contains("albumart") || !supported_commands.contains("readpicture") {
            config.album_art.method = ImageMethod::None;
            status_warn!("Album art is disabled because it is not supported by MPD");
        }

        log::trace!(config:? = config; "Resolved config");

        let key_resolver = KeyResolver::new(&config);

        let active_tab = config.tabs.names.first().context("Expected at least one tab")?.clone();
        scheduler.start();
        Ok(Self {
            ytdlp_manager: YtDlpManager::new(work_sender.clone()),
            mpd_version: client.version(),
            lrc_index: LrcIndex::default(),
            config: std::sync::Arc::new(config),
            status,
            queue,
            current_song,
            stickers: HashMap::new(),
            active_tab,
            supported_commands,
            db_update_start: None,
            app_event_sender,
            work_sender,
            scheduler,
            client_request_sender,
            needs_render: Cell::new(false),
            stickers_to_fetch: RefCell::new(HashSet::new()),
            rendered_frames: 0,
            messages: RingVec::default(),
            song_played: None,
            last_status_update: Instant::now(),
            stickers_supported,
            input: InputManager::default(),
            key_resolver,
            cached_queue_time_total,
        })
    }

    // TODO: Error comes from crossebeam, try to remove later if it gets solved
    // upstream
    #[allow(clippy::result_large_err)]
    pub(crate) fn render(&self) -> Result<(), SendError<AppEvent>> {
        if self.needs_render.get() {
            return Ok(());
        }

        self.needs_render.replace(true);
        self.app_event_sender.send(AppEvent::RequestRender)
    }

    pub(crate) fn finish_frame(&mut self) {
        self.needs_render.replace(false);
        self.rendered_frames.add_assign(1);

        let stickers = self.stickers_to_fetch.take();
        if !stickers.is_empty() {
            match self.stickers_supported {
                StickersSupport::Unsupported => {
                    self.stickers_supported = StickersSupport::UnsupportedAndChecked;
                    // Shoot a dummy sticker request to MPD to see what error we get to determine
                    // what exactly is wrong.
                    self.command(|_, client| {
                        if let Err(err) = client.sticker("", "test") {
                            status_error!(
                                "Stickers are not supported by MPD server: '{}'",
                                err.detail_or_display()
                            );
                        } else {
                            status_error!("Stickers are not supported by MPD server");
                        }
                        Ok(())
                    });
                }
                StickersSupport::UnsupportedAndChecked => {}
                StickersSupport::Supported => {
                    let uris = stickers.into_iter().collect();
                    log::debug!(uris:?; "Fetching stickers after frame");
                    self.query().id(FETCH_SONG_STICKERS).replace_id(FETCH_SONG_STICKERS).query(
                        |client| {
                            let stickers = client.fetch_song_stickers(uris)?;
                            Ok(MpdQueryResult::SongStickers(stickers))
                        },
                    );
                }
            }
        }
    }

    pub(crate) fn query_sync<T: Send + Sync + 'static>(
        &self,
        on_done: impl FnOnce(&mut Client<'_>) -> Result<T> + Send + 'static,
    ) -> Result<T> {
        let (tx, rx) = bounded(1);
        let query = MpdQuerySync {
            callback: Box::new(|client| Ok(MpdQueryResult::Any(Box::new((on_done)(client)?)))),
            tx,
        };

        if let Err(err) = self.client_request_sender.send(ClientRequest::QuerySync(query)) {
            log::error!(error:? = err; "Failed to send query request");
            bail!("Failed to send sync query request");
        }

        if let MpdQueryResult::Any(any) = rx.recv()? {
            if let Ok(val) = any.downcast::<T>() {
                return Ok(*val);
            }
            bail!("Received unknown type answer for sync query request");
        }

        bail!("Received unknown MpdQueryResult for sync query request");
    }

    #[builder(finish_fn(name = query))]
    pub(crate) fn query(
        &self,
        #[builder(finish_fn)] on_done: impl FnOnce(&mut Client<'_>) -> Result<MpdQueryResult>
        + Send
        + 'static,
        id: &'static str,
        target: Option<PaneType>,
        replace_id: Option<&'static str>,
    ) {
        let query = MpdQuery { id, target, replace_id, callback: Box::new(on_done) };
        if let Err(err) = self.client_request_sender.send(ClientRequest::Query(query)) {
            log::error!(error:? = err; "Failed to send query request");
        }
    }

    pub(crate) fn command(
        &self,
        callback: impl FnOnce(&Sender<AppEvent>, &mut Client<'_>) -> Result<()> + Send + 'static,
    ) {
        if let Err(err) = self
            .client_request_sender
            .send(ClientRequest::Command(MpdCommand { callback: Box::new(callback) }))
        {
            log::error!(error:? = err; "Failed to send command request");
        }
    }

    pub(crate) fn set_current_song(&mut self, song: Option<Song>) {
        self.current_song = song;
    }

    pub(crate) fn current_song(&self) -> Option<&Song> {
        if self.status.state == State::Stop {
            return None;
        }
        self.current_song.as_ref()
    }

    pub(crate) fn current_song_index(&self) -> Option<usize> {
        if self.status.state == State::Stop {
            return None;
        }
        self.status.song
    }

    pub(crate) fn find_current_lyrics_path(&self) -> Option<PathBuf> {
        let song = self.current_song()?;
        let lyrics_dir = self.config.lyrics_dir.as_ref()?;
        let path = crate::shared::lrc::get_lrc_path(lyrics_dir, &song.file)
            .ok()
            .filter(|p| p.is_file())
            .or_else(|| self.lrc_index.find_entry(song).map(|(path, _)| path.to_path_buf()));

        let artist = song.metadata.get("artist").map(|v| v.last())?;
        let title = song.metadata.get("title").map(|v| v.last())?;
        let album = song.metadata.get("album").map(|v| v.last());
        match &path {
            Some(path) => log::debug!(artist, title, album; "Lyrics found at {}", path.display()),
            None => log::debug!(artist, title, album; "No lyrics found"),
        }

        path
    }

    pub(crate) fn find_lrc(&self) -> Result<Option<(PathBuf, Lrc)>> {
        let Some(path) = self.find_current_lyrics_path() else { return Ok(None) };
        let lrc = std::fs::read_to_string(&path)?.parse()?;
        Ok(Some((path, lrc)))
    }

    pub(crate) fn song_stickers(&self, uri: &str) -> Option<&HashMap<String, String>> {
        if matches!(self.stickers_supported, StickersSupport::UnsupportedAndChecked) {
            return None;
        }
        let stickers = self.stickers.get(uri);

        if stickers.is_none() {
            self.stickers_to_fetch.borrow_mut().insert(uri.to_owned());
        }

        stickers
    }

    /// Search for song stickers only if they are supported, does not trigger
    /// check for reason.
    pub(crate) fn song_stickers_if_supported(&self, uri: &str) -> Option<&HashMap<String, String>> {
        if !matches!(self.stickers_supported, StickersSupport::Supported) {
            return None;
        }

        let stickers = self.stickers.get(uri);

        if stickers.is_none() {
            self.stickers_to_fetch.borrow_mut().insert(uri.to_owned());
        }

        stickers
    }

    pub(crate) fn set_song_stickers(
        &mut self,
        uri: String,
        stickers: HashMap<String, String>,
    ) -> Option<HashMap<String, String>> {
        self.stickers.insert(uri, stickers)
    }

    pub(crate) fn set_stickers(&mut self, stickers: HashMap<String, HashMap<String, String>>) {
        self.stickers = stickers;
    }

    pub(crate) fn stickers(&self) -> &HashMap<String, HashMap<String, String>> {
        &self.stickers
    }
}

#[derive(Debug, Clone, Copy)]
pub enum StickersSupport {
    Supported,
    Unsupported,
    UnsupportedAndChecked,
}

impl From<StickersSupport> for bool {
    fn from(value: StickersSupport) -> Self {
        match value {
            StickersSupport::Supported => true,
            StickersSupport::Unsupported => false,
            StickersSupport::UnsupportedAndChecked => false,
        }
    }
}
