use std::{
    io::Write,
    sync::{Arc, RwLock},
};

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::{
    dirs::{Dirs, DirsError},
    models::{CodeChallengeMethod, CodeVerifier},
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    #[serde(with = "arc_rwlock_serde")]
    token: Arc<RwLock<Option<String>>>,
    #[serde(with = "arc_rwlock_serde")]
    toast_shown: Arc<RwLock<bool>>,
    #[serde(skip)]
    code_verifier: Arc<RwLock<Option<CodeVerifier>>>,
    #[serde(skip)]
    code_challenge_method: Arc<RwLock<Option<CodeChallengeMethod>>>,
    #[serde(skip)]
    open_browser: Arc<RwLock<bool>>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            token: Default::default(),
            toast_shown: Default::default(),
            code_verifier: Default::default(),
            code_challenge_method: Default::default(),
            open_browser: Arc::new(RwLock::new(true)),
        }
    }
}

impl PartialEq for Config {
    fn eq(&self, _: &Self) -> bool {
        true
    }
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Dirs(#[from] DirsError),
    #[error(transparent)]
    Toml(#[from] toml::ser::Error),
    #[error(transparent)]
    Config(#[from] config::ConfigError),
    #[error(transparent)]
    Unknown(#[from] anyhow::Error),
}

impl Config {
    pub fn set_token(&self, token: Option<String>) -> Result<(), ConfigError> {
        *self.token.write().unwrap() = token;
        self.write()?;
        Ok(())
    }

    pub fn token(&self) -> Option<String> {
        self.token.read().unwrap().clone()
    }
}

impl Config {
    pub(crate) fn toast_shown(&self) -> bool {
        *self.toast_shown.read().unwrap()
    }

    pub(crate) fn set_toast_shown(&self, value: bool) -> Result<(), ConfigError> {
        *self.toast_shown.write().unwrap() = value;
        self.write()?;
        Ok(())
    }
}

impl Config {
    pub fn set_code_verifier(
        &self,
        code_verifier: Option<CodeVerifier>,
    ) -> Result<(), ConfigError> {
        *self.code_verifier.write().unwrap() = code_verifier;
        Ok(())
    }

    pub fn set_code_challenge_method(
        &self,
        code_challenge_method: Option<CodeChallengeMethod>,
    ) -> Result<(), ConfigError> {
        *self.code_challenge_method.write().unwrap() = code_challenge_method;
        Ok(())
    }

    pub fn code_verifier(&self) -> Option<CodeVerifier> {
        self.code_verifier.read().unwrap().clone()
    }
}

impl Config {
    pub fn open_browser(&self) -> bool {
        *self.open_browser.read().unwrap()
    }

    pub fn set_open_browser(&self, value: bool) -> Result<(), ConfigError> {
        *self.open_browser.write().unwrap() = value;
        Ok(())
    }
}

impl Config {
    pub(crate) fn new() -> Result<Self, ConfigError> {
        let config = config::Config::builder()
            .add_source(
                config::File::with_name(Dirs::get_config_file()?.to_str().unwrap()).required(false),
            )
            .build()
            .unwrap();

        let token = config.get_string("token").ok();
        let toast_shown = config.get_bool("toast_shown").unwrap_or_default();
        let config = Self::default();

        config.set_token(token)?;
        config.set_toast_shown(toast_shown)?;

        config.write()?;

        Ok(config)
    }

    fn write(&self) -> Result<(), ConfigError> {
        let toml = toml::to_string_pretty(self)?;
        let mut file = std::fs::File::create(&Dirs::get_config_file()?)?;
        file.write_all(toml.as_bytes())?;
        Ok(())
    }
}

mod arc_rwlock_serde {
    use serde::de::Deserializer;
    use serde::ser::Serializer;
    use serde::{Deserialize, Serialize};
    use std::sync::{Arc, RwLock};

    pub fn serialize<S, T>(val: &Arc<RwLock<T>>, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
        T: Serialize,
    {
        T::serialize(&*val.read().unwrap(), s)
    }

    pub fn deserialize<'de, D, T>(d: D) -> Result<Arc<RwLock<T>>, D::Error>
    where
        D: Deserializer<'de>,
        T: Deserialize<'de>,
    {
        Ok(Arc::new(RwLock::new(T::deserialize(d)?)))
    }
}
