//! Project storage.

use std::sync::Arc;

use iam_types::{Error, IamError, Project, Result};

use crate::backend::{Backend, CasResult, JsonStore, StorageBackend};

mod keys {
    pub const PROJECTS: &str = "iam/projects/";
    pub const BY_ORG: &str = "iam/projects/by-org/";
}

pub struct ProjectStore {
    backend: Arc<Backend>,
}

impl JsonStore for ProjectStore {
    fn backend(&self) -> &Backend {
        &self.backend
    }
}

impl ProjectStore {
    pub fn new(backend: Arc<Backend>) -> Self {
        Self { backend }
    }

    pub async fn create(&self, project: &Project) -> Result<u64> {
        let key = self.primary_key(&project.org_id, &project.id);
        let bytes =
            serde_json::to_vec(project).map_err(|e| Error::Serialization(e.to_string()))?;
        match self.backend.cas(key.as_bytes(), 0, &bytes).await? {
            CasResult::Success(version) => {
                self.create_indexes(project).await?;
                Ok(version)
            }
            CasResult::Conflict { .. } => Err(Error::Iam(IamError::ProjectAlreadyExists(
                project.key(),
            ))),
            CasResult::NotFound => Err(Error::Internal("Unexpected CAS result".into())),
        }
    }

    pub async fn create_if_missing(&self, project: &Project) -> Result<bool> {
        let key = self.primary_key(&project.org_id, &project.id);
        let bytes =
            serde_json::to_vec(project).map_err(|e| Error::Serialization(e.to_string()))?;
        match self.backend.cas(key.as_bytes(), 0, &bytes).await? {
            CasResult::Success(_) => {
                self.create_indexes(project).await?;
                Ok(true)
            }
            CasResult::Conflict { .. } => Ok(false),
            CasResult::NotFound => Err(Error::Internal("Unexpected CAS result".into())),
        }
    }

    pub async fn get(&self, org_id: &str, id: &str) -> Result<Option<Project>> {
        Ok(self
            .get_json::<Project>(self.primary_key(org_id, id).as_bytes())
            .await?
            .map(|v| v.0))
    }

    pub async fn get_with_version(&self, org_id: &str, id: &str) -> Result<Option<(Project, u64)>> {
        self.get_json::<Project>(self.primary_key(org_id, id).as_bytes())
            .await
    }

    pub async fn update(&self, project: &Project, expected_version: u64) -> Result<u64> {
        let key = self.primary_key(&project.org_id, &project.id);
        let bytes =
            serde_json::to_vec(project).map_err(|e| Error::Serialization(e.to_string()))?;
        match self.backend.cas(key.as_bytes(), expected_version, &bytes).await? {
            CasResult::Success(version) => {
                self.create_indexes(project).await?;
                Ok(version)
            }
            CasResult::Conflict { expected, actual } => {
                Err(Error::Storage(iam_types::StorageError::CasConflict { expected, actual }))
            }
            CasResult::NotFound => Err(Error::Iam(IamError::ProjectNotFound(project.key()))),
        }
    }

    pub async fn delete(&self, org_id: &str, id: &str) -> Result<bool> {
        if let Some(project) = self.get(org_id, id).await? {
            let deleted = self.backend.delete(self.primary_key(org_id, id).as_bytes()).await?;
            if deleted {
                self.delete_indexes(&project).await?;
            }
            Ok(deleted)
        } else {
            Ok(false)
        }
    }

    pub async fn list(&self) -> Result<Vec<Project>> {
        let pairs = self.backend.scan_prefix(keys::PROJECTS.as_bytes(), 10_000).await?;
        let mut projects = Vec::new();
        for pair in pairs {
            if String::from_utf8_lossy(&pair.key).starts_with(keys::BY_ORG) {
                continue;
            }
            let project: Project = serde_json::from_slice(&pair.value)
                .map_err(|e| Error::Serialization(e.to_string()))?;
            projects.push(project);
        }
        Ok(projects)
    }

    pub async fn list_by_org(&self, org_id: &str) -> Result<Vec<Project>> {
        let prefix = format!("{}{}/", keys::BY_ORG, org_id);
        let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10_000).await?;
        let mut projects = Vec::new();
        for pair in pairs {
            let project_id = String::from_utf8_lossy(&pair.value).to_string();
            if let Some(project) = self.get(org_id, &project_id).await? {
                projects.push(project);
            }
        }
        Ok(projects)
    }

    pub async fn exists(&self, org_id: &str, id: &str) -> Result<bool> {
        Ok(self
            .backend
            .get(self.primary_key(org_id, id).as_bytes())
            .await?
            .is_some())
    }

    fn primary_key(&self, org_id: &str, id: &str) -> String {
        format!("{}{}/{}", keys::PROJECTS, org_id, id)
    }

    async fn create_indexes(&self, project: &Project) -> Result<()> {
        let key = format!("{}{}/{}", keys::BY_ORG, project.org_id, project.id);
        self.backend.put(key.as_bytes(), project.id.as_bytes()).await?;
        Ok(())
    }

    async fn delete_indexes(&self, project: &Project) -> Result<()> {
        let key = format!("{}{}/{}", keys::BY_ORG, project.org_id, project.id);
        self.backend.delete(key.as_bytes()).await?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn list_by_org_is_isolated() {
        let backend = Arc::new(Backend::memory());
        let store = ProjectStore::new(backend);
        store
            .create(&Project::new("proj-1", "org-1", "Project 1"))
            .await
            .unwrap();
        store
            .create(&Project::new("proj-1", "org-2", "Project 1"))
            .await
            .unwrap();
        let org1 = store.list_by_org("org-1").await.unwrap();
        let org2 = store.list_by_org("org-2").await.unwrap();
        assert_eq!(org1.len(), 1);
        assert_eq!(org2.len(), 1);
        assert_eq!(org1[0].org_id, "org-1");
        assert_eq!(org2[0].org_id, "org-2");
    }
}
