use base64ct::{Base64, Encoding};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::{
    sqlite::{SqlitePool, SqliteRow},
    Row,
};
use std::{collections::HashSet, ops::Deref, path::PathBuf};
use thiserror::Error;
use uuid::Uuid;

mod filehandle;
mod fileinfo;

pub use filehandle::FileHandle;
pub use fileinfo::FileInfo;

#[derive(Debug, Error)]
pub enum WriteFileError {
    #[error("root file path does not exist")]
    RootNotFound,

    #[error("permission denied")]
    PermissionDenied,

    #[error("invalid path")]
    InvalidPath,

    #[error("no metadata available")]
    NoMetadata,

    #[error("file could not be loaded")]
    LoadError(#[from] ReadFileError),

    #[error("image conversion failed")]
    ImageError(#[from] image::ImageError),

    #[error("JSON error")]
    JSONError(#[from] serde_json::error::Error),

    #[error("IO error")]
    IOError(#[from] std::io::Error),
}

#[derive(Debug, Error)]
pub enum ReadFileError {
    #[error("file not found")]
    FileNotFound(PathBuf),

    #[error("path is not a file")]
    NotAFile,

    #[error("permission denied")]
    PermissionDenied,

    #[error("invalid path")]
    InvalidPath,

    #[error("JSON error")]
    JSONError(#[from] serde_json::error::Error),

    #[error("IO error")]
    IOError(#[from] std::io::Error),
}

#[derive(Debug, Error)]
pub enum AuthError {
    #[error("authentication token is duplicated")]
    DuplicateAuthToken,

    #[error("session token is duplicated")]
    DuplicateSessionToken,

    #[error("database failed")]
    SqlError(sqlx::Error),
}

impl From<sqlx::Error> for AuthError {
    fn from(err: sqlx::Error) -> AuthError {
        AuthError::SqlError(err)
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct Username(String);

impl From<String> for Username {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for Username {
    fn from(s: &str) -> Self {
        Self(s.to_owned())
    }
}

impl From<Username> for String {
    fn from(s: Username) -> Self {
        Self::from(&s)
    }
}

impl From<&Username> for String {
    fn from(s: &Username) -> Self {
        let Username(s) = s;
        Self::from(s)
    }
}

impl Deref for Username {
    type Target = String;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl sqlx::FromRow<'_, SqliteRow> for Username {
    fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
        let name: String = row.try_get("username")?;
        Ok(Username::from(name))
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct AuthToken(String);

impl From<String> for AuthToken {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for AuthToken {
    fn from(s: &str) -> Self {
        Self(s.to_owned())
    }
}

impl From<AuthToken> for PathBuf {
    fn from(s: AuthToken) -> Self {
        Self::from(&s)
    }
}

impl From<&AuthToken> for PathBuf {
    fn from(s: &AuthToken) -> Self {
        let AuthToken(s) = s;
        Self::from(s)
    }
}

impl Deref for AuthToken {
    type Target = String;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct SessionToken(String);

impl From<String> for SessionToken {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for SessionToken {
    fn from(s: &str) -> Self {
        Self(s.to_owned())
    }
}

impl From<SessionToken> for PathBuf {
    fn from(s: SessionToken) -> Self {
        Self::from(&s)
    }
}

impl From<&SessionToken> for PathBuf {
    fn from(s: &SessionToken) -> Self {
        let SessionToken(s) = s;
        Self::from(s)
    }
}

impl Deref for SessionToken {
    type Target = String;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct FileId(String);

impl From<String> for FileId {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for FileId {
    fn from(s: &str) -> Self {
        Self(s.to_owned())
    }
}

impl From<FileId> for PathBuf {
    fn from(s: FileId) -> Self {
        Self::from(&s)
    }
}

impl From<&FileId> for PathBuf {
    fn from(s: &FileId) -> Self {
        let FileId(s) = s;
        Self::from(s)
    }
}

impl Deref for FileId {
    type Target = String;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

pub trait FileRoot {
    fn root(&self) -> PathBuf;
}

pub struct Context(PathBuf);

impl FileRoot for Context {
    fn root(&self) -> PathBuf {
        self.0.clone()
    }
}

#[derive(Clone)]
pub struct AuthDB {
    pool: SqlitePool,
}

impl AuthDB {
    pub async fn new(path: PathBuf) -> Result<Self, sqlx::Error> {
        let migrator = sqlx::migrate!("./migrations");
        let pool = SqlitePool::connect(&format!("sqlite://{}", path.to_str().unwrap())).await?;
        migrator.run(&pool).await?;
        Ok(Self { pool })
    }

    pub async fn add_user(&self, username: Username) -> Result<AuthToken, AuthError> {
        let mut hasher = Sha256::new();
        hasher.update(Uuid::new_v4().hyphenated().to_string());
        hasher.update(username.to_string());
        let auth_token = Base64::encode_string(&hasher.finalize());

        let _ = sqlx::query("INSERT INTO users (username, token) VALUES ($1, $2)")
            .bind(username.to_string())
            .bind(auth_token.clone())
            .execute(&self.pool)
            .await?;

        Ok(AuthToken::from(auth_token))
    }

    pub async fn list_users(&self) -> Result<Vec<Username>, AuthError> {
        let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users")
            .fetch_all(&self.pool)
            .await?;

        Ok(usernames)
    }

    pub async fn authenticate(&self, token: AuthToken) -> Result<Option<SessionToken>, AuthError> {
        let results = sqlx::query("SELECT * FROM users WHERE token = $1")
            .bind(token.to_string())
            .fetch_all(&self.pool)
            .await?;

        if results.len() > 1 {
            return Err(AuthError::DuplicateAuthToken);
        }

        if results.is_empty() {
            return Ok(None);
        }

        let user_id: i64 = results[0].try_get("id")?;

        let mut hasher = Sha256::new();
        hasher.update(Uuid::new_v4().hyphenated().to_string());
        hasher.update(token.to_string());
        let session_token = Base64::encode_string(&hasher.finalize());

        let _ = sqlx::query("INSERT INTO sessions (token, user_id) VALUES ($1, $2)")
            .bind(session_token.clone())
            .bind(user_id)
            .execute(&self.pool)
            .await?;

        Ok(Some(SessionToken::from(session_token)))
    }

    pub async fn validate_session(
        &self,
        token: SessionToken,
    ) -> Result<Option<Username>, AuthError> {
        let rows = sqlx::query(
            "SELECT users.username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessions.token = $1",
        )
        .bind(token.to_string())
        .fetch_all(&self.pool)
        .await?;
        if rows.len() > 1 {
            return Err(AuthError::DuplicateSessionToken);
        }

        if rows.is_empty() {
            return Ok(None);
        }

        let username: String = rows[0].try_get("username")?;
        Ok(Some(Username::from(username)))
    }
}

pub struct Store {
    files_root: PathBuf,
}

impl Store {
    pub fn new(files_root: PathBuf) -> Self {
        Self { files_root }
    }

    pub fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
        let paths = std::fs::read_dir(&self.files_root)?;
        let info_files = paths
            .into_iter()
            .filter_map(|path| {
                let path_ = path.unwrap().path();
                if path_.extension().and_then(|s| s.to_str()) == Some("json") {
                    let stem = path_.file_stem().and_then(|s| s.to_str()).unwrap();
                    Some(FileId::from(stem))
                } else {
                    None
                }
            })
            .collect::<HashSet<FileId>>();
        Ok(info_files)
    }

    pub fn add_file(
        &mut self,
        filename: String,
        content: Vec<u8>,
    ) -> Result<FileHandle, WriteFileError> {
        let mut file = FileHandle::new(filename, self.files_root.clone())?;
        file.set_content(content)?;
        Ok(file)
    }

    pub fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
        FileHandle::load(id, &self.files_root)
    }

    pub fn delete_file(&mut self, id: &FileId) -> Result<(), WriteFileError> {
        let handle = FileHandle::load(id, &self.files_root)?;
        handle.delete();
        Ok(())
    }

    pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> {
        let mut path = self.files_root.clone();
        path.push(PathBuf::from(id));
        path.set_extension("json");
        FileInfo::load(path)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use cool_asserts::assert_matches;
    use std::{collections::HashSet, io::Read};
    use tempdir::TempDir;

    fn with_file<F>(test_fn: F)
    where
        F: FnOnce(Store, FileId, TempDir),
    {
        let tmp = TempDir::new("var").unwrap();

        let mut buf = Vec::new();
        let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
        file.read_to_end(&mut buf).unwrap();

        let mut store = Store::new(PathBuf::from(tmp.path()));
        let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();

        test_fn(store, file_record.id, tmp);
    }

    #[test]
    fn adds_files() {
        with_file(|store, id, tmp| {
            let file = store.get_file(&id).expect("to retrieve the file");

            assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);

            assert!(tmp.path().join(&(*id)).with_extension("png").exists());
            assert!(tmp.path().join(&(*id)).with_extension("json").exists());
            assert!(tmp.path().join(&(*id)).with_extension("tn.png").exists());
        });
    }

    #[test]
    fn sets_up_metadata_for_file() {
        with_file(|store, id, tmp| {
            assert!(tmp.path().join(&(*id)).with_extension("png").exists());
            let info = store.get_metadata(&id).expect("to retrieve the metadata");

            assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => {
                assert_eq!(size, 23777);
                assert_eq!(file_type, "image/png");
                assert_eq!(hash, "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee".to_owned());
                assert_eq!(extension, "png".to_owned());
            });
        });
    }

    /*
    #[test]
    fn sets_up_thumbnail_for_file() {
        with_file(|store, id| {
            let (_, thumbnail) = store.get_thumbnail(&id).expect("to retrieve the thumbnail");
            assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869);
        });
    }
    */

    #[test]
    fn deletes_associated_files() {
        with_file(|mut store, id, tmp| {
            store.delete_file(&id).expect("file to be deleted");

            assert!(!tmp.path().join(&(*id)).with_extension("png").exists());
            assert!(!tmp.path().join(&(*id)).with_extension("json").exists());
            assert!(!tmp.path().join(&(*id)).with_extension("tn.png").exists());
        });
    }

    #[test]
    fn lists_files_in_the_db() {
        with_file(|store, id, _| {
            let resolvers = store.list_files().expect("file listing to succeed");
            let ids = resolvers.into_iter().collect::<HashSet<FileId>>();

            assert_eq!(ids.len(), 1);
            assert!(ids.contains(&id));
        });
    }
}

#[cfg(test)]
mod authdb_test {
    use super::*;
    use cool_asserts::assert_matches;

    #[tokio::test]
    async fn can_create_and_list_users() {
        let db = AuthDB::new(PathBuf::from(":memory:"))
            .await
            .expect("a memory-only database will be created");
        let _ = db
            .add_user(Username::from("savanni"))
            .await
            .expect("user to be created");
        assert_matches!(db.list_users().await, Ok(names) => {
            let names = names.into_iter().collect::<HashSet<Username>>();
            assert!(names.contains(&Username::from("savanni")));
        })
    }

    #[tokio::test]
    async fn unknown_auth_token_returns_nothing() {
        let db = AuthDB::new(PathBuf::from(":memory:"))
            .await
            .expect("a memory-only database will be created");
        let _ = db
            .add_user(Username::from("savanni"))
            .await
            .expect("user to be created");

        let token = AuthToken::from("0000000000");

        assert_matches!(db.authenticate(token).await, Ok(None));
    }

    #[tokio::test]
    async fn auth_token_becomes_session_token() {
        let db = AuthDB::new(PathBuf::from(":memory:"))
            .await
            .expect("a memory-only database will be created");
        let token = db
            .add_user(Username::from("savanni"))
            .await
            .expect("user to be created");

        assert_matches!(db.authenticate(token).await, Ok(_));
    }

    #[tokio::test]
    async fn can_validate_session_token() {
        let db = AuthDB::new(PathBuf::from(":memory:"))
            .await
            .expect("a memory-only database will be created");
        let token = db
            .add_user(Username::from("savanni"))
            .await
            .expect("user to be created");
        let session = db
            .authenticate(token)
            .await
            .expect("token authentication should succeed")
            .expect("session token should be found");

        assert_matches!(
        db.validate_session(session).await,
        Ok(Some(username)) => {
            assert_eq!(username, Username::from("savanni"));
        });
    }
}