use crate::error::Error::{
    GenericError, IncorrectWorkspaceKey, MissingWorkspaceKey, WorkspaceKeyDecryptionError,
};
use crate::error::{Error, Result};
use crate::master_key::MasterKey;
use crate::workspace_key::WorkspaceKey;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use log::{info, warn};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use yaak_models::models::{EncryptedKey, Workspace, WorkspaceMeta};
use yaak_models::query_manager::QueryManager;
use yaak_models::util::{generate_id_of_length, UpdateSource};

const KEY_USER: &str = "encryption-key";

#[derive(Debug, Clone)]
pub struct EncryptionManager {
    cached_master_key: Arc<Mutex<Option<MasterKey>>>,
    cached_workspace_keys: Arc<Mutex<HashMap<String, WorkspaceKey>>>,
    query_manager: QueryManager,
    app_id: String,
}

impl EncryptionManager {
    pub fn new(query_manager: QueryManager, app_id: impl Into<String>) -> Self {
        Self {
            cached_master_key: Default::default(),
            cached_workspace_keys: Default::default(),
            query_manager,
            app_id: app_id.into(),
        }
    }

    pub fn encrypt(&self, workspace_id: &str, data: &[u8]) -> Result<Vec<u8>> {
        let workspace_secret = self.get_workspace_key(workspace_id)?;
        workspace_secret.encrypt(data)
    }

    pub fn decrypt(&self, workspace_id: &str, data: &[u8]) -> Result<Vec<u8>> {
        let workspace_secret = self.get_workspace_key(workspace_id)?;
        workspace_secret.decrypt(data)
    }

    pub fn reveal_workspace_key(&self, workspace_id: &str) -> Result<String> {
        let key = self.get_workspace_key(workspace_id)?;
        key.to_human()
    }

    pub fn set_human_key(&self, workspace_id: &str, human_key: &str) -> Result<WorkspaceMeta> {
        let wkey = WorkspaceKey::from_human(human_key)?;

        let workspace = self.query_manager.connect().get_workspace(workspace_id)?;
        let encryption_key_challenge = match workspace.encryption_key_challenge {
            None => return self.set_workspace_key(workspace_id, &wkey),
            Some(c) => c,
        };

        let encryption_key_challenge = match BASE64_STANDARD.decode(encryption_key_challenge) {
            Ok(c) => c,
            Err(_) => return Err(GenericError("Failed to decode workspace challenge".to_string())),
        };

        if let Err(_) = wkey.decrypt(encryption_key_challenge.as_slice()) {
            return Err(IncorrectWorkspaceKey);
        };

        self.set_workspace_key(workspace_id, &wkey)
    }

    pub(crate) fn set_workspace_key(
        &self,
        workspace_id: &str,
        wkey: &WorkspaceKey,
    ) -> Result<WorkspaceMeta> {
        info!("Created workspace key for {workspace_id}");

        let encrypted_key = BASE64_STANDARD.encode(self.get_master_key()?.encrypt(wkey.raw_key())?);
        let encrypted_key = EncryptedKey { encrypted_key };
        let encryption_key_challenge = wkey.encrypt(generate_id_of_length(50).as_bytes())?;
        let encryption_key_challenge = Some(BASE64_STANDARD.encode(encryption_key_challenge));

        let workspace_meta = self.query_manager.with_tx::<WorkspaceMeta, Error>(|tx| {
            let workspace = tx.get_workspace(workspace_id)?;
            let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?;
            tx.upsert_workspace(
                &Workspace { encryption_key_challenge, ..workspace },
                &UpdateSource::Background,
            )?;

            Ok(tx.upsert_workspace_meta(
                &WorkspaceMeta { encryption_key: Some(encrypted_key.clone()), ..workspace_meta },
                &UpdateSource::Background,
            )?)
        })?;

        let mut cache = self.cached_workspace_keys.lock().unwrap();
        cache.insert(workspace_id.to_string(), wkey.clone());

        Ok(workspace_meta)
    }

    pub fn ensure_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
        let workspace_meta =
            self.query_manager.connect().get_or_create_workspace_meta(workspace_id)?;

        // Already exists
        if let Some(_) = workspace_meta.encryption_key {
            warn!("Tried to create workspace key when one already exists for {workspace_id}");
            return Ok(workspace_meta);
        }

        let wkey = WorkspaceKey::create()?;
        self.set_workspace_key(workspace_id, &wkey)
    }

    pub fn disable_encryption(&self, workspace_id: &str) -> Result<()> {
        info!("Disabling encryption for {workspace_id}");

        self.query_manager.with_tx::<(), Error>(|tx| {
            let workspace = tx.get_workspace(workspace_id)?;
            let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?;

            // Clear encryption challenge on workspace
            tx.upsert_workspace(
                &Workspace { encryption_key_challenge: None, ..workspace },
                &UpdateSource::Background,
            )?;

            // Clear encryption key on workspace meta
            tx.upsert_workspace_meta(
                &WorkspaceMeta { encryption_key: None, ..workspace_meta },
                &UpdateSource::Background,
            )?;

            Ok(())
        })?;

        // Remove from cache
        let mut cache = self.cached_workspace_keys.lock().unwrap();
        cache.remove(workspace_id);

        Ok(())
    }

    fn get_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceKey> {
        {
            let cache = self.cached_workspace_keys.lock().unwrap();
            if let Some(k) = cache.get(workspace_id) {
                return Ok(k.clone());
            }
        };

        let db = self.query_manager.connect();
        let workspace_meta = db.get_or_create_workspace_meta(workspace_id)?;

        let key = match workspace_meta.encryption_key {
            None => return Err(MissingWorkspaceKey),
            Some(k) => k,
        };

        let mkey = self.get_master_key()?;
        let decoded_key = BASE64_STANDARD
            .decode(key.encrypted_key)
            .map_err(|e| WorkspaceKeyDecryptionError(e.to_string()))?;
        let raw_key = mkey
            .decrypt(decoded_key.as_slice())
            .map_err(|e| WorkspaceKeyDecryptionError(e.to_string()))?;
        let wkey = WorkspaceKey::from_raw_key(raw_key.as_slice());

        Ok(wkey)
    }

    fn get_master_key(&self) -> Result<MasterKey> {
        // NOTE: This locks the key for the entire function which seems wrong, but this prevents
        // concurrent access from prompting the user for a keychain password multiple times.
        let mut master_secret = self.cached_master_key.lock().unwrap();
        if let Some(k) = master_secret.as_ref() {
            return Ok(k.to_owned());
        }

        let mkey = MasterKey::get_or_create(&self.app_id, KEY_USER)?;
        *master_secret = Some(mkey.clone());
        Ok(mkey)
    }
}
