
use async_session::{Result, Session, SessionStore};
use anyhow::anyhow;
use async_trait::async_trait;
use std::{
    fs,
    path::Path,
};
use walkdir::{DirEntry, WalkDir};
use binascii::bin2hex;

#[allow(unused)]
fn is_hidden(entry: &DirEntry) -> bool {
    entry.file_name()
        .to_str()
        .map(|s| s.starts_with("."))
        .unwrap_or(false)
}
#[derive(Debug, Clone)]
pub struct FileStore {
    pub sess_path: String,
    pub ses_file: String,
}
#[async_trait]
impl SessionStore for FileStore {
    async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
        let id = Session::id_from_cookie_value(&cookie_value)?;
        log::trace!("loading session by id `{}`", &id);
        //dbg!("loading session by id `{}`", &id);
        self.load_session_file(&id).await
    }
    async fn store_session(&self, session: Session) -> Result<Option<String>> {
        log::trace!("storing session by id `{}`", session.id());
        let id_filename = match self.get_path(session.id()) {
            Ok(res) => res,
            Err(e) => {
                return Err(e);
            }
        };
        // let mut out_buffer = [0u8; 100];
        // let id_filename = if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
        //    std::str::from_utf8(res)?.to_owned()
        // } else {
        //    return Ok(None);
        // };
        let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
        if ! Path::new(&sess_id_path).exists() {
            fs::create_dir(&sess_id_path)?;
        }
        let content_session = serde_json::to_string(&session)?;
        fs::write(&format!("{}/{}",&sess_id_path, self.ses_file), content_session)?;
        //session.reset_data_changed(); // do not need is it is serialized in file write 
        Ok(session.into_cookie_value())
    }
    async fn destroy_session(&self, session: Session) -> Result {
        log::trace!("destroying session by id `{}`", session.id());
        match self.get_path(session.id()) {
            Ok(res) => match self.get_session_id_path(&res) {
                Ok(session_id_path) => Ok(fs::remove_file(&session_id_path)?),
                Err(e) => Err(e),
            },
            Err(e) => Err(e) 
        }
        // let mut out_buffer = [0u8; 100];
        // if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
        //     let id_filename = std::str::from_utf8(res)?.to_owned();
        //     Ok(fs::remove_file(
        //         &format!("{}/{}/{}",self.sess_path, &id_filename, self.ses_file)
        //     )?)
        // } else {
        //     Ok(())
        // }
    }
    async fn clear_store(&self) -> Result {
        log::trace!("clearing memory store");
        let sess_path = format!("{}", self.sess_path);
        fs::remove_dir_all(&sess_path)?;
        fs::create_dir(&sess_path)?;
        Ok(())
    }
}
impl FileStore {
    /// Create a new instance of FilesStore
    pub fn check_paths(&self) -> Result {
        if ! Path::new(&self.sess_path).exists() {
            fs::create_dir(&self.sess_path)?;
        }
        Ok(())
    }
    pub fn get_path(&self,id: &str) -> Result<String> {
        let mut out_buffer = [0u8; 100];
        match bin2hex(&id.as_bytes(),&mut out_buffer) {
            Ok(res) => Ok(std::str::from_utf8(res)?.to_owned()),
            Err(e) => {
                Err(anyhow!("Filename path {} not generated: {:?}", &id, &e))
            }
        } 
    }
    pub fn get_session_id_path(&self,id_filename: &str) -> Result<String> {
        let session_id_path = format!("{}/{}/{}",self.sess_path, id_filename, &self.ses_file);
        if ! Path::new(&session_id_path).exists() {
           Err(anyhow!("Filename path {} not found: {}", id_filename, &session_id_path ))
        } else {
            Ok(session_id_path)
        }
    }
    /// As session Id from `async_session` comes in base64 it will be not valid for OS filename 
    /// `bin2hex` pass id to hex as bytes and from there to string or viceversa
    pub async fn load_session_file(&self, id: &str) -> Result<Option<Session>> {
        let session_id_path = match self.get_path(id) {
            Ok(res) => match self.get_session_id_path(&res) {
                Ok(path) => path,
                Err(e) => return Err(e),
            },
            Err(e) => {
                return Err(e);
            }
        };
        // let mut out_buffer = [0u8; 100];
        // let id_filename = if let Ok(res) = bin2hex(&id.as_bytes(),&mut out_buffer) {
        //    std::str::from_utf8(res)?.to_owned()
        // } else {
        //    return Ok(None);
        // };
        // let session_id_path = format!("{}/{}/{}",self.sess_path, &id_filename, &self.ses_file);
        // dbg!(&session_id_path);
        if ! Path::new(&session_id_path).exists() {
            dbg!("No path: {}", &session_id_path);
            // let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
            // if ! Path::new(&sess_id_path).exists() {
            //     fs::create_dir(&sess_id_path)?;
            // }
            // create
        }
        if let Ok(session_content) = fs::read_to_string(&session_id_path) {
            // match serde_json::from_str::<serde_json::Value>(&session_content) {
            match serde_json::from_str::<Session>(&session_content) {
                Ok(session) => {
                    Ok(session.validate())
                },
                Err(e) => { 
                    dbg!("Error loading session content from {}: {}",&session_id_path, e);
                    //log::error!("Error loading session content from {}: {}",&session_id_path, e);
                    Ok(None)
                }
            }
        } else {
            Ok(None)
        }
    }
    #[allow(dead_code)]
    pub async fn cleanup(&self) -> Result {
        log::trace!("cleaning up file store...");
        let mut count: usize = 0;
        let sess_path = format!("{}", self.sess_path);
        let walker = WalkDir::new(&sess_path).into_iter();
        for entry in walker.filter_entry(|e| !is_hidden(e)) {
            match entry {
                Ok(dir_entry) => {
                    // println!("{}", &dir_entry.path().display());
                    if ! Path::new(&dir_entry.path()).is_dir() {
                        continue;
                    }
                    let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
                    let id_path = format!("{}",&dir_entry.path().display());
                    let id = id_path.replace(&sess_path,"");
                    if let Some(session) = self.load_session_file(&id).await.unwrap_or_default() {
                        if session.is_expired() {
                            let _ = fs::remove_file(&session_file);
                            log::trace!("found {} expired session",&id_path);
                            count +=1;
                        }
                    }
                },
                Err(e) =>  println!("Error on {}: {}", &sess_path, e)
            }
        }
        log::trace!("found {} expired session {} cleaned",&sess_path, count);
        Ok(())
    }
    #[allow(dead_code)]
    pub async fn count(&self) -> usize {
        let mut count: usize = 0;
        let sess_path = format!("{}", self.sess_path);
        let walker = WalkDir::new(&sess_path).into_iter();
        for entry in walker.filter_entry(|e| !is_hidden(e)) {
            match entry {
                Ok(dir_entry) => {
                    // println!("{}", &dir_entry.path().display());
                    if ! Path::new(&dir_entry.path()).is_dir() {
                        continue;
                    }
                    let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
                    if Path::new(&session_file).exists() {
                        count +=1;
                    }
                },
                Err(e) =>  println!("Error on {}: {}", &sess_path, e)
            }
        }
        count
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use async_std::task;
    use std::time::Duration;
    const TEST_SESS_FILESTORE: &str = "/tmp/test_sess_filestore";
    const TEST_IDS_FILESTORE: &str = "/tmp/test_ids_filestore";

    #[async_std::test]
    async fn creating_a_new_session_with_no_expiry() -> Result {
        let sess_path_store = format!("{}_0", TEST_SESS_FILESTORE);
        let ids_path_store = format!("{}_0", TEST_IDS_FILESTORE);
        let _ = fs::remove_dir_all(&sess_path_store);
        let _ = fs::remove_dir_all(&ids_path_store);
        let store = FileStore {
            sess_path: sess_path_store.to_owned(),
            ses_file: String::from("session"),
        };
        store.check_paths()?;
        let mut session = Session::new();
        session.insert("key", "Hello")?;
        let cloned = session.clone();
        let cookie_value = store.store_session(session).await?.unwrap();
        assert!(true);
        let loaded_session = store.load_session(cookie_value).await?.unwrap();
        assert_eq!(cloned.id(), loaded_session.id());
        assert_eq!("Hello", &loaded_session.get::<String>("key").unwrap());
        assert!(!loaded_session.is_expired());
        assert!(loaded_session.validate().is_some());
        let _ = fs::remove_dir_all(&sess_path_store);
        let _ = fs::remove_dir_all(&ids_path_store);
        Ok(())
    }
    #[async_std::test]
    async fn updating_a_session() -> Result {
        let sess_path_store = format!("{}_1", TEST_SESS_FILESTORE);
        let _ = fs::remove_dir_all(&sess_path_store);
        let store = FileStore {
            sess_path: sess_path_store.to_owned(),
            ses_file: String::from("session"),
        };
        store.check_paths()?;

        let mut session = Session::new();

        session.insert("key", "value")?;
        let cookie_value = store.store_session(session).await?.unwrap();

        let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
        session.insert("key", "other value")?;

        assert_eq!(store.store_session(session).await?, None);
        let session = store.load_session(cookie_value).await?.unwrap();
        assert_eq!(&session.get::<String>("key").unwrap(), "other value");
        fs::remove_dir_all(&sess_path_store)?;
        Ok(())
    }

    #[async_std::test]
    async fn updating_a_session_extending_expiry() -> Result {
        let sess_path_store = format!("{}_2", TEST_SESS_FILESTORE);
        let _ = fs::remove_dir_all(&sess_path_store);
        let store = FileStore {
            sess_path: sess_path_store.to_owned(),
            ses_file: String::from("session"),
        };
        store.check_paths()?;

        let mut session = Session::new();
        session.expire_in(Duration::from_secs(1));
        let original_expires = session.expiry().unwrap().clone();
        let cookie_value = store.store_session(session).await?.unwrap();

        let mut session = store.load_session(cookie_value.clone()).await?.unwrap();

        assert_eq!(session.expiry().unwrap(), &original_expires);
        session.expire_in(Duration::from_secs(3));
        let new_expires = session.expiry().unwrap().clone();
        assert_eq!(None, store.store_session(session).await?);

        let session = store.load_session(cookie_value.clone()).await?.unwrap();
        assert_eq!(session.expiry().unwrap(), &new_expires);

        task::sleep(Duration::from_secs(3)).await;
        assert_eq!(None, store.load_session(cookie_value).await?);
        fs::remove_dir_all(&sess_path_store)?;
        Ok(())
    }

    #[async_std::test]
    async fn creating_a_new_session_with_expiry() -> Result {
        let sess_path_store = format!("{}_3", TEST_SESS_FILESTORE);
        let _ = fs::remove_dir_all(&sess_path_store);
        let store = FileStore {
            sess_path: sess_path_store.to_owned(),
            ses_file: String::from("session"),
        };
        store.check_paths()?;

        let mut session = Session::new();
        session.expire_in(Duration::from_secs(3));
        session.insert("key", "value")?;
        let cloned = session.clone();

        let cookie_value = store.store_session(session).await?.unwrap();

        let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
        assert_eq!(cloned.id(), loaded_session.id());
        assert_eq!("value", &*loaded_session.get::<String>("key").unwrap());

        assert!(!loaded_session.is_expired());

        task::sleep(Duration::from_secs(3)).await;
        assert_eq!(None, store.load_session(cookie_value).await?);
        fs::remove_dir_all(&sess_path_store)?;
        Ok(())
    }

    #[async_std::test]
    async fn destroying_a_single_session() -> Result {
        let sess_path_store = format!("{}_4", TEST_SESS_FILESTORE);
        let _ = fs::remove_dir_all(&sess_path_store);
        let store = FileStore {
            sess_path: sess_path_store.to_owned(),
            ses_file: String::from("session"),
        };
        store.check_paths()?;

        for _ in 0..3i8 {
            store.store_session(Session::new()).await?;
        }
        let cookie = store.store_session(Session::new()).await?.unwrap();
        assert_eq!(4, store.count().await);
        let session = store.load_session(cookie.clone()).await?.unwrap();
        store.destroy_session(session.clone()).await?;
        assert!(store.load_session(cookie).await.is_err());
        assert_eq!(3, store.count().await);

        // attempting to destroy the session again IS an ERROR, file should be deleted before
        assert!(store.destroy_session(session).await.is_err());
        fs::remove_dir_all(&sess_path_store)?;
        Ok(())
    }

    #[async_std::test]
    async fn clearing_the_whole_store() -> Result {
        let sess_path_store = format!("{}_5", TEST_SESS_FILESTORE);
        let _ = fs::remove_dir_all(&sess_path_store);
        let store = FileStore {
            sess_path: sess_path_store.to_owned(),
            ses_file: String::from("session"),
        };
        store.check_paths()?;

        for _ in 0..3i8 {
            store.store_session(Session::new()).await?;
        }
        assert_eq!(3, store.count().await);
        store.clear_store().await.unwrap();
        assert_eq!(0, store.count().await);
        fs::remove_dir_all(&sess_path_store)?;
        Ok(())
    }
}