use serde::Serialize;
use serde::de::DeserializeOwned;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Debug, Clone)]
pub struct Persistent<T> {
    state: Arc<Mutex<T>>,
    file_path: PathBuf,
}

impl<T> Persistent<T> {
    #[tracing::instrument(skip(state))]
    pub fn new(state: T, file_path: PathBuf) -> Self {
        Self {
            state: Arc::new(Mutex::new(state)),
            file_path,
        }
    }

    /// Instantiates state from file system
    #[tracing::instrument]
    pub async fn load(file_path: &Path) -> Result<Self, Error>
    where
        T: DeserializeOwned,
    {
        let content = tokio::fs::read_to_string(&file_path).await.map_err(|source| Error::Read {
            file_path: file_path.to_owned(),
            source,
        })?;

        let t = ron::from_str(&content).map_err(|source| Error::Deserialize {
            file_path: file_path.to_owned(),
            source,
        })?;

        let persistent = Self {
            state: Arc::new(Mutex::new(t)),
            file_path: file_path.to_owned(),
        };

        Ok(persistent)
    }

    // TODO: This is pretty poor - clones the entire state on access
    /// Returns state
    #[tracing::instrument(skip(self))]
    pub async fn get(&self) -> T
    where
        T: Clone,
    {
        let state = self.state.lock().await;
        state.clone()
    }

    /// Returns state passed through given function
    #[tracing::instrument(skip_all)]
    pub async fn with<F, R>(&self, f: F) -> R
    where
        F: FnOnce(&T) -> R,
    {
        let state = self.state.lock().await;
        f(&state)
    }

    /// Updates and persists state
    #[tracing::instrument(skip_all)]
    pub async fn update<F>(&self, f: F) -> Result<(), Error>
    where
        F: FnOnce(&mut T),
        T: Serialize,
    {
        let mut state = self.state.lock().await;
        f(&mut state);
        Self::persist(&self.file_path, &state).await?;
        Ok(())
    }

    /// Sets and persists state
    #[tracing::instrument(skip_all)]
    pub async fn set(&self, new_state: T) -> Result<(), Error>
    where
        T: Serialize,
    {
        self.update(move |state| {
            *state = new_state;
        })
        .await
    }

    /// Persist.
    ///
    /// Implicitly called by `set` and `update`.
    #[tracing::instrument(skip_all, err)]
    pub async fn persist(file_path: &Path, state: &T) -> Result<(), Error>
    where
        T: Serialize,
    {
        tracing::debug!("Persisting state");
        let serialized = ron::ser::to_string_pretty(state, ron::ser::PrettyConfig::default()).map_err(|source| Error::Serialize { source })?;
        tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
            file_path: file_path.to_owned(),
            source,
        })?;
        Ok(())
    }
}

#[derive(Debug, derive_more::Error, derive_more::Display)]
#[display("Persistent state error: {_variant}")]
pub enum Error {
    #[display("Failed to read file: {}", file_path.display())]
    Read { file_path: PathBuf, source: std::io::Error },

    #[display("Failed to deserialize state from file: {}", file_path.display())]
    Deserialize { file_path: PathBuf, source: ron::error::SpannedError },

    #[display("Failed to serialize state")]
    Serialize { source: ron::Error },

    #[display("Failed to write file: {}", file_path.display())]
    Write { file_path: PathBuf, source: std::io::Error },
}
