use dyn_clonable::*;
use std::{collections::HashMap, path::PathBuf};

/// Represents the namespace for data Storage
///
/// OCA - TODO
/// OCAObjectsJSON - Json representation of OCA objects
/// CoreModel - Core model of OCA
/// OCARelations - Relations between OCA objects
/// OCAReferences - References to OCA objects
pub enum Namespace {
    OCA,
    /// Represents the namespace for OCA Bundle Model in JSON format
    OCABundlesJSON,
    /// Represents the namespace for OCA Objects (overlays and capture bases) in JSON format
    OCAObjectsJSON,
    CoreModel,
    OCARelations,
    OCAReferences,
}

impl Namespace {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::OCA => "oca",
            Self::OCABundlesJSON => "oca_bundles_json",
            Self::OCAObjectsJSON => "oca_objects_json",
            Self::CoreModel => "core_model",
            Self::OCARelations => "oca_relations",
            Self::OCAReferences => "oca_refs",
        }
    }
}

#[clonable]
pub trait DataStorage: Clone + Send {
    fn get(&self, namespace: Namespace, key: &str) -> Result<Option<Vec<u8>>, String>;
    fn get_all(&self, namespace: Namespace) -> Result<HashMap<String, Vec<u8>>, String>;
    fn insert(&mut self, namespace: Namespace, key: &str, value: &[u8]) -> Result<(), String>;
    fn new() -> Self
    where
        Self: Sized;
    fn config(&self, config: HashMap<String, String>) -> Self
    where
        Self: Sized;
    fn open(_path: &str) -> Self
    where
        Self: Sized,
    {
        panic!("DEPRECATED: use new() and config() instead of open()");
    }
}

#[derive(Clone)]
pub struct SledDataStorage {
    db: Option<sled::Db>,
}

#[derive(Clone)]
pub struct SledDataStorageConfig {
    pub path: String,
}

impl SledDataStorageConfig {
    pub fn build() -> SledDataStorageConfigBuilder {
        SledDataStorageConfigBuilder { path: None }
    }
}

pub struct SledDataStorageConfigBuilder {
    path: Option<PathBuf>,
}

impl SledDataStorageConfigBuilder {
    pub fn path(mut self, path: PathBuf) -> Self {
        self.path = Some(path);
        self
    }

    pub fn finalize(&self) -> Result<HashMap<String, String>, String> {
        let mut config = HashMap::new();

        match &self.path {
            Some(path) => config.insert(
                "path".to_string(),
                path.clone()
                    .into_os_string()
                    .into_string()
                    .map_err(|e| e.into_string().unwrap())?,
            ),
            None => return Err("path is required".to_string()),
        };

        Ok(config)
    }

    pub fn unwrap(&self) -> HashMap<String, String> {
        self.finalize().unwrap()
    }
}

impl DataStorage for SledDataStorage {
    fn new() -> Self {
        Self { db: None }
    }

    fn config(&self, config: HashMap<String, String>) -> Self {
        if let Some(path) = config.get("path")
            && let Ok(db) = sled::open(path)
        {
            return Self { db: Some(db) };
        }
        self.clone()
    }

    fn get(&self, namespace: Namespace, key: &str) -> Result<Option<Vec<u8>>, String> {
        if let Some(ref db) = self.db {
            let tree = db.open_tree(namespace.as_str().as_bytes()).unwrap();
            match tree.get(key.as_bytes()).unwrap() {
                Some(value) => Ok(Some(value.to_vec())),
                None => Ok(None),
            }
        } else {
            Err("Data Storage must be opened first".to_string())
        }
    }

    fn get_all(&self, namespace: Namespace) -> Result<HashMap<String, Vec<u8>>, String> {
        if let Some(ref db) = self.db {
            let mut all = HashMap::new();
            let tree = db.open_tree(namespace.as_str().as_bytes()).unwrap();
            let mut iter = tree.iter();
            while let Some(Ok((key, value))) = iter.next() {
                all.insert(String::from_utf8(key.to_vec()).unwrap(), value.to_vec());
            }

            Ok(all)
        } else {
            Err("Data Storage must be opened first".to_string())
        }
    }

    fn insert(&mut self, namespace: Namespace, key: &str, value: &[u8]) -> Result<(), String> {
        if let Some(ref db) = self.db {
            let tree = db.open_tree(namespace.as_str().as_bytes()).unwrap();
            match tree.insert(key.as_bytes(), value) {
                Ok(_) => Ok(()),
                Err(e) => Err(e.to_string()),
            }
        } else {
            Err("Data Storage must be opened first".to_string())
        }
    }
}

#[derive(Clone)]
pub struct InMemoryDataStorage {
    db: HashMap<String, HashMap<String, Vec<u8>>>,
}

impl DataStorage for InMemoryDataStorage {
    fn new() -> Self {
        Self { db: HashMap::new() }
    }

    fn config(&self, _config: HashMap<String, String>) -> Self {
        self.clone()
    }

    fn get(&self, namespace: Namespace, key: &str) -> Result<Option<Vec<u8>>, String> {
        let namespace_storage = match self.db.get(namespace.as_str()) {
            Some(namespace_storage) => namespace_storage,
            None => return Ok(None),
        };
        match namespace_storage.get(key) {
            Some(value) => Ok(Some(value.to_vec())),
            None => Ok(None),
        }
    }

    fn get_all(&self, namespace: Namespace) -> Result<HashMap<String, Vec<u8>>, String> {
        match self.db.get(namespace.as_str()) {
            Some(namespace_storage) => Ok(namespace_storage.clone()),
            None => Ok(HashMap::new()),
        }
    }

    fn insert(&mut self, namespace: Namespace, key: &str, value: &[u8]) -> Result<(), String> {
        let mut namespace_storage = match self.db.get(namespace.as_str()) {
            Some(namespace_storage) => namespace_storage.clone(),
            None => HashMap::new(),
        };
        namespace_storage.insert(key.to_string(), value.to_vec());
        self.db
            .insert(namespace.as_str().to_string(), namespace_storage);

        Ok(())
    }
}

#[derive(Clone)]
pub struct FileSystemStorage {
    dir: Option<PathBuf>,
}

#[derive(Clone)]
pub struct FileSystemStorageConfig {
    pub path: String,
}

impl FileSystemStorageConfig {
    pub fn build() -> FileSystemStorageConfigBuilder {
        FileSystemStorageConfigBuilder { path: None }
    }
}

pub struct FileSystemStorageConfigBuilder {
    path: Option<PathBuf>,
}

impl FileSystemStorageConfigBuilder {
    pub fn path(mut self, path: PathBuf) -> Self {
        self.path = Some(path);
        self
    }

    pub fn finalize(&self) -> Result<HashMap<String, String>, String> {
        let mut config = HashMap::new();

        match &self.path {
            Some(path) => config.insert(
                "path".to_string(),
                path.clone()
                    .into_os_string()
                    .into_string()
                    .map_err(|e| e.into_string().unwrap())?,
            ),
            None => return Err("path is required".to_string()),
        };

        Ok(config)
    }

    pub fn unwrap(&self) -> HashMap<String, String> {
        self.finalize().unwrap()
    }
}

impl DataStorage for FileSystemStorage {
    fn new() -> Self {
        Self { dir: None }
    }

    fn config(&self, config: HashMap<String, String>) -> Self {
        if let Some(path) = config.get("path") {
            return Self {
                dir: Some(PathBuf::from(path)),
            };
        }
        self.clone()
    }

    fn get(&self, namespace: Namespace, key: &str) -> Result<Option<Vec<u8>>, String> {
        if let Some(ref dir) = self.dir {
            let mut path = dir.clone();
            path.push(namespace.as_str());
            if path.try_exists().unwrap() {
                path.push(key);
                Ok(std::fs::read(path.clone()).ok())
            } else {
                Ok(None)
            }
        } else {
            Err("File path is required".to_string())
        }
    }

    fn get_all(&self, _namespace: Namespace) -> Result<HashMap<String, Vec<u8>>, String> {
        Err("Not implemented".to_string())
    }

    fn insert(&mut self, namespace: Namespace, key: &str, value: &[u8]) -> Result<(), String> {
        if let Some(ref dir) = self.dir {
            let mut path = dir.clone();
            path.push(namespace.as_str());
            if !path.try_exists().unwrap() {
                std::fs::create_dir_all(path.clone()).unwrap();
            }

            path.push(key);
            std::fs::write(path.clone(), value).map_err(|e| e.to_string())
        } else {
            Err("File path is required".to_string())
        }
    }
}
