//! Ditto Root Directory Wrapper
//!
//! A utility module for creating, managing, and removing the Ditto directory
//! structures on the local file system

use_prelude!();
use std::{
    env, fs,
    path::{Path, PathBuf},
};

use crate::error::{DittoError, ErrorKind};

/// Interface provided by the Filesystem directory into which Ditto will store
/// its local data
pub trait DittoRoot: Send + Sync {
    /// Return the root path of the Ditto directory
    fn root_path(&self) -> &Path;

    /// Return the path of the data in the Ditto directory
    #[deprecated(note = "Use root_path instead")]
    fn data_path(&self) -> &Path {
        self.root_path()
    }

    /// Emits the Ditto root directory path as a null-terminated UTF-8 C-string
    fn root_dir_to_c_str(&self) -> Result<char_p::Box, DittoError> {
        Ok(char_p::new(self.root_path_as_str()?))
    }

    /// Emits the Ditto data directory path as a null-terminated UTF-8 C-string
    #[deprecated(note = "Use root_dir_to_c_str instead")]
    fn data_dir_to_c_str(&self) -> Result<char_p::Box, DittoError> {
        self.root_dir_to_c_str()
    }

    /// Return true if the Root path exists
    fn exists(&self) -> bool;

    /// Return true if the provided Root path is a valid path
    fn is_valid(&self) -> Result<(), DittoError>;

    /// Return the Root path as a `&str`
    fn root_path_as_str(&self) -> Result<&str, DittoError> {
        self.root_path().to_str().ok_or_else(|| {
            DittoError::new(
                ErrorKind::InvalidInput,
                "Path is not valid UTF-8".to_string(),
            )
        })
    }

    /// Returns the path of the Ditto data directory as an `&str`
    #[deprecated(note = "Use root_path_as_str instead")]
    fn data_path_as_str(&self) -> Result<&str, DittoError> {
        self.root_path_as_str()
    }
}

/// A persistent working directory where Ditto will store its data across restarts of the host
/// process on the local file system
pub struct PersistentRoot {
    root: PathBuf,
}

/// A Temporary Ditto root which will clean itself up on exit
pub struct TempRoot {
    root: PathBuf,
}

impl Drop for TempRoot {
    fn drop(&mut self) {
        // we need to step up one level
        let ditto_root = &mut self.root; // tempdir/jitter/ditto_root
        ditto_root.pop(); // tempdir/jitter <-- remove here
        debug!(path = %ditto_root.display(), "removing TempRoot");
        let _ = ::std::fs::remove_dir_all(&ditto_root);
    }
}

impl Default for PersistentRoot {
    fn default() -> Self {
        PersistentRoot::from_current_exe().unwrap() // This should never fail
    }
}

impl DittoRoot for PersistentRoot {
    fn root_path(&self) -> &Path {
        self.root.as_path()
    }
    fn exists(&self) -> bool {
        self.root.exists()
    }
    fn is_valid(&self) -> Result<(), DittoError> {
        Ok(()) // TODO
    }
}

// Constructors
impl PersistentRoot {
    /// Manually construct a root directory
    pub fn new(root_dir: impl Into<PathBuf>) -> Result<Self, DittoError> {
        let root = root_dir.into();
        #[allow(clippy::disallowed_methods)]
        std::fs::create_dir_all(&root).map_err(DittoError::from)?;
        let dir = PersistentRoot { root };

        if let Err(e) = dir.is_valid() {
            Err(e)
        } else {
            Ok(dir)
        }
    }

    /// Get a Ditto directory from the path of the current executable
    pub fn from_current_exe() -> Result<Self, DittoError> {
        let root_dir = env::current_exe()
            .ok()
            .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
            .ok_or_else(|| {
                DittoError::new(
                    ErrorKind::InvalidInput,
                    "Unable to resolve a default data directory on this platform".to_string(),
                )
            })?;
        Self::new(root_dir)
    }

    /// Create a Ditto directory from env vars
    pub fn from_env() -> Result<Self, DittoError> {
        let root_dir: PathBuf = env::var_os("DITTO_ROOT_PATH")
            .map(PathBuf::from)
            .ok_or_else(|| {
                DittoError::new(
                    ErrorKind::InvalidInput,
                    "The DITTO_ROOT_PATH env var is not set".to_string(),
                )
            })?;
        Self::new(root_dir)
    }
}

impl DittoRoot for TempRoot {
    fn root_path(&self) -> &Path {
        self.root.as_path()
    }
    fn exists(&self) -> bool {
        self.root.exists()
    }
    fn is_valid(&self) -> Result<(), DittoError> {
        Ok(()) // TODO
    }
}

impl TempRoot {
    // Temp randomization impl adapted from temp_dir crate but with a contemporary version of the
    // rand crate
    const NUM_RETRIES: u32 = 1 << 31;
    const NUM_RAND_CHARS: usize = 12;

    /// Create a new Ditto root which will be deleted when `TempRoot` is dropped
    pub fn new() -> Self {
        use std::iter;

        use rand::{distributions::Alphanumeric, Rng};
        let mut ditto_root;
        let tmpdir = std::env::temp_dir(); // the OS provided temp root from TMPDIR
        if !tmpdir.is_absolute() {
            let cur_dir = env::current_exe()
                .ok()
                .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
                .unwrap();
            ditto_root = cur_dir;
            ditto_root.push(tmpdir);
        } else {
            ditto_root = tmpdir;
        }

        let mut rng = rand::thread_rng();
        for _ in 0..Self::NUM_RETRIES {
            let jitter: String = iter::repeat(())
                .map(|()| rng.sample(Alphanumeric))
                .map(char::from)
                .take(Self::NUM_RAND_CHARS)
                .collect();
            ditto_root.push(jitter);
            #[allow(clippy::disallowed_methods)]
            match fs::create_dir_all(&ditto_root) {
                Ok(_) => {
                    ditto_root.push("ditto");
                    #[allow(clippy::disallowed_methods)]
                    fs::create_dir_all(&ditto_root).unwrap();
                    return TempRoot { root: ditto_root };
                }
                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
                Err(e) => {
                    panic!("Unable to create tempdir {:?}", e);
                }
            }
        }
        panic!("TempRoot {:?} already exists!", ditto_root.display());
    }
}

impl Default for TempRoot {
    fn default() -> Self {
        TempRoot::new()
    }
}

/// In v2 of the Ditto SDK, data used to be stored in a subdirectory named `ditto_data`.
///
/// This function will:
///   - if the `ditto_data` directory still exists
///     - drain its content to the parent directory
///     - delete the `ditto_data` directory
///   - else it will do nothing.
pub(crate) fn drain_ditto_data_dir(root: &Arc<dyn DittoRoot>) {
    let root: PathBuf = root.root_path().into();
    let old_data: PathBuf = root.join("ditto_data");
    if old_data.exists() {
        debug!(old_path = ?old_data, new_path = ?root, "migrating `ditto_data` to 'ditto' dir");
        let mut copy_options = fs_extra::dir::CopyOptions::new();
        copy_options.overwrite = true;
        // read_dir can only fail if:
        // * old_data is not a directory
        // * lacking permissions
        // * old_data do not exists
        // Unwrapping here is safe then.
        let data_children: Vec<_> = std::fs::read_dir(&old_data)
            .unwrap_or_else(|err| panic!("Failed to access {old_data:?}: {err}"))
            .filter_map(|child| child.ok())
            .map(|child| child.path())
            .collect();
        // Copy recursively all directories and their content to the root folder
        fs_extra::copy_items(&data_children, &root, &copy_options).unwrap();
        std::fs::remove_dir_all(old_data).unwrap();
    }
}
