//! Policy binding storage
//!
//! Stores and retrieves policy bindings that assign roles to principals.

use std::sync::Arc;

use iam_types::{Error, IamError, PolicyBinding, PrincipalRef, Result, Scope};

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

/// Key prefixes for binding storage
mod keys {
    /// Primary key: by scope and principal
    /// Format: iam/bindings/scope/{scope}/principal/{principal}/{id}
    pub const BINDINGS_BY_SCOPE: &str = "iam/bindings/scope/";

    /// Secondary index: by principal
    /// Format: iam/bindings/by-principal/{principal}/{id}
    pub const BINDINGS_BY_PRINCIPAL: &str = "iam/bindings/by-principal/";

    /// Secondary index: by role
    /// Format: iam/bindings/by-role/{role}/{id}
    pub const BINDINGS_BY_ROLE: &str = "iam/bindings/by-role/";
}

/// Store for policy bindings
pub struct BindingStore {
    backend: Arc<Backend>,
}

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

impl BindingStore {
    /// Create a new binding store
    pub fn new(backend: Arc<Backend>) -> Self {
        Self { backend }
    }

    /// Create a new binding
    pub async fn create(&self, binding: &PolicyBinding) -> Result<u64> {
        let key = self.make_primary_key(binding);

        // Use CAS with version 0 to ensure uniqueness
        let bytes = serde_json::to_vec(binding).map_err(|e| Error::Serialization(e.to_string()))?;

        match self.backend.cas(key.as_bytes(), 0, &bytes).await? {
            CasResult::Success(version) => {
                // Create secondary indexes; on failure, roll back primary to avoid dangling data
                if let Err(e) = self.create_indexes(binding).await {
                    let _ = self.backend.delete(key.as_bytes()).await;
                    return Err(e);
                }
                Ok(version)
            }
            CasResult::Conflict { .. } => Err(Error::Iam(IamError::BindingAlreadyExists(
                binding.id.clone(),
            ))),
            CasResult::NotFound => {
                // This shouldn't happen with version 0
                Err(Error::Internal("Unexpected CAS result".into()))
            }
        }
    }

    /// Get a binding by ID
    pub async fn get(
        &self,
        scope: &Scope,
        principal: &PrincipalRef,
        id: &str,
    ) -> Result<Option<PolicyBinding>> {
        let key = format!(
            "{}{}/principal/{}/{}",
            keys::BINDINGS_BY_SCOPE,
            scope.to_key(),
            principal,
            id
        );

        match self.get_json::<PolicyBinding>(key.as_bytes()).await? {
            Some((binding, _)) => Ok(Some(binding)),
            None => Ok(None),
        }
    }

    /// Get a binding by ID only (searches across all scopes)
    pub async fn get_by_id(&self, id: &str) -> Result<Option<PolicyBinding>> {
        // Search across all scopes - this is expensive, prefer get() when possible
        let prefix = keys::BINDINGS_BY_SCOPE;
        let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10000).await?;

        for pair in pairs {
            let key_str = String::from_utf8_lossy(&pair.key);
            if key_str.ends_with(&format!("/{}", id)) {
                let binding: PolicyBinding = serde_json::from_slice(&pair.value)
                    .map_err(|e| Error::Serialization(e.to_string()))?;
                return Ok(Some(binding));
            }
        }

        Ok(None)
    }

    /// Get a binding with its storage version
    pub async fn get_by_id_with_version(&self, id: &str) -> Result<Option<(PolicyBinding, u64)>> {
        let prefix = keys::BINDINGS_BY_SCOPE;
        let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10000).await?;

        for pair in pairs {
            let key_str = String::from_utf8_lossy(&pair.key);
            if key_str.ends_with(&format!("/{}", id)) {
                let binding: PolicyBinding = serde_json::from_slice(&pair.value)
                    .map_err(|e| Error::Serialization(e.to_string()))?;
                return Ok(Some((binding, pair.version)));
            }
        }

        Ok(None)
    }

    /// Update a binding
    pub async fn update(&self, binding: &PolicyBinding, expected_version: u64) -> Result<u64> {
        let existing = self
            .get(&binding.scope, &binding.principal_ref, &binding.id)
            .await?
            .ok_or_else(|| Error::Iam(IamError::BindingNotFound(binding.id.clone())))?;

        let key = self.make_primary_key(binding);
        let bytes = serde_json::to_vec(binding).map_err(|e| Error::Serialization(e.to_string()))?;

        match self
            .backend
            .cas(key.as_bytes(), expected_version, &bytes)
            .await?
        {
            CasResult::Success(version) => {
                self.delete_indexes(&existing).await?;
                self.create_indexes(binding).await?;
                Ok(version)
            }
            CasResult::Conflict { expected, actual } => {
                Err(Error::Storage(iam_types::StorageError::CasConflict {
                    expected,
                    actual,
                }))
            }
            CasResult::NotFound => Err(Error::Iam(IamError::BindingNotFound(binding.id.clone()))),
        }
    }

    /// Delete a binding
    pub async fn delete(&self, scope: &Scope, principal: &PrincipalRef, id: &str) -> Result<bool> {
        // First get the binding to know what indexes to delete
        if let Some(binding) = self.get(scope, principal, id).await? {
            let key = self.make_primary_key(&binding);

            // Delete primary key
            let deleted = self.backend.delete(key.as_bytes()).await?;

            if deleted {
                // Delete secondary indexes
                self.delete_indexes(&binding).await?;
            }

            Ok(deleted)
        } else {
            Ok(false)
        }
    }

    /// List bindings for a principal
    pub async fn list_by_principal(&self, principal: &PrincipalRef) -> Result<Vec<PolicyBinding>> {
        let prefix = format!("{}{}/", keys::BINDINGS_BY_PRINCIPAL, principal);
        let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10000).await?;

        let mut bindings = Vec::new();
        for pair in pairs {
            // Index stores binding ID, need to fetch actual binding
            let id = String::from_utf8_lossy(&pair.value);
            if let Some(binding) = self.get_by_id(&id).await? {
                bindings.push(binding);
            }
        }

        Ok(bindings)
    }

    /// List bindings for a scope
    pub async fn list_by_scope(&self, scope: &Scope) -> Result<Vec<PolicyBinding>> {
        let prefix = format!("{}{}/", keys::BINDINGS_BY_SCOPE, scope.to_key());
        let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10000).await?;

        let mut bindings = Vec::new();
        for pair in pairs {
            let binding: PolicyBinding = serde_json::from_slice(&pair.value)
                .map_err(|e| Error::Serialization(e.to_string()))?;
            bindings.push(binding);
        }

        Ok(bindings)
    }

    /// List all bindings across scopes (used for admin listing)
    pub async fn list_all(&self) -> Result<Vec<PolicyBinding>> {
        let pairs = self
            .backend
            .scan_prefix(keys::BINDINGS_BY_SCOPE.as_bytes(), 10000)
            .await?;

        let mut bindings = Vec::new();
        for pair in pairs {
            let binding: PolicyBinding = serde_json::from_slice(&pair.value)
                .map_err(|e| Error::Serialization(e.to_string()))?;
            bindings.push(binding);
        }

        Ok(bindings)
    }

    /// Rebuild secondary indexes for all bindings (best-effort maintenance tool)
    pub async fn rebuild_indexes(&self) -> Result<usize> {
        let bindings = self.list_all().await?;
        let mut repaired = 0;
        for binding in bindings {
            self.create_indexes(&binding).await?;
            repaired += 1;
        }
        Ok(repaired)
    }

    /// List bindings for a role
    pub async fn list_by_role(&self, role_ref: &str) -> Result<Vec<PolicyBinding>> {
        let prefix = format!("{}{}/", keys::BINDINGS_BY_ROLE, role_ref);
        let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10000).await?;

        let mut bindings = Vec::new();
        for pair in pairs {
            // Index stores binding ID, need to fetch actual binding
            let id = String::from_utf8_lossy(&pair.value);
            if let Some(binding) = self.get_by_id(&id).await? {
                bindings.push(binding);
            }
        }

        Ok(bindings)
    }

    /// Get all bindings that apply to a principal in a given scope
    /// This considers scope hierarchy (e.g., System scope contains all)
    pub async fn get_effective_bindings(
        &self,
        principal: &PrincipalRef,
        target_scope: &Scope,
    ) -> Result<Vec<PolicyBinding>> {
        let all_bindings = self.list_by_principal(principal).await?;

        // Filter to bindings whose scope contains the target scope
        let effective: Vec<_> = all_bindings
            .into_iter()
            .filter(|b| b.scope.contains(target_scope) && b.enabled)
            .collect();

        Ok(effective)
    }

    // Helper methods

    fn make_primary_key(&self, binding: &PolicyBinding) -> String {
        format!(
            "{}{}/principal/{}/{}",
            keys::BINDINGS_BY_SCOPE,
            binding.scope.to_key(),
            binding.principal_ref,
            binding.id
        )
    }

    async fn create_indexes(&self, binding: &PolicyBinding) -> Result<()> {
        // Principal index
        let principal_key = format!(
            "{}{}/{}",
            keys::BINDINGS_BY_PRINCIPAL,
            binding.principal_ref,
            binding.id
        );
        self.backend
            .put(principal_key.as_bytes(), binding.id.as_bytes())
            .await?;

        // Role index
        let role_key = format!(
            "{}{}/{}",
            keys::BINDINGS_BY_ROLE,
            binding.role_ref,
            binding.id
        );
        self.backend
            .put(role_key.as_bytes(), binding.id.as_bytes())
            .await?;

        Ok(())
    }

    async fn delete_indexes(&self, binding: &PolicyBinding) -> Result<()> {
        // Principal index
        let principal_key = format!(
            "{}{}/{}",
            keys::BINDINGS_BY_PRINCIPAL,
            binding.principal_ref,
            binding.id
        );
        self.backend.delete(principal_key.as_bytes()).await?;

        // Role index
        let role_key = format!(
            "{}{}/{}",
            keys::BINDINGS_BY_ROLE,
            binding.role_ref,
            binding.id
        );
        self.backend.delete(role_key.as_bytes()).await?;

        Ok(())
    }
}

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

    fn test_backend() -> Arc<Backend> {
        Arc::new(Backend::memory())
    }

    #[tokio::test]
    async fn test_binding_crud() {
        let store = BindingStore::new(test_backend());

        let binding = PolicyBinding::new(
            "binding-1",
            PrincipalRef::user("alice"),
            "roles/ProjectAdmin",
            Scope::project("proj-1", "org-1"),
        );

        // Create
        let version = store.create(&binding).await.unwrap();
        assert!(version > 0);

        // Get
        let fetched = store
            .get(
                &Scope::project("proj-1", "org-1"),
                &PrincipalRef::user("alice"),
                "binding-1",
            )
            .await
            .unwrap();
        assert!(fetched.is_some());
        assert_eq!(fetched.unwrap().id, "binding-1");

        // Delete
        let deleted = store
            .delete(
                &Scope::project("proj-1", "org-1"),
                &PrincipalRef::user("alice"),
                "binding-1",
            )
            .await
            .unwrap();
        assert!(deleted);

        // Verify deleted
        let fetched = store
            .get(
                &Scope::project("proj-1", "org-1"),
                &PrincipalRef::user("alice"),
                "binding-1",
            )
            .await
            .unwrap();
        assert!(fetched.is_none());
    }

    #[tokio::test]
    async fn test_list_by_principal() {
        let store = BindingStore::new(test_backend());

        let alice = PrincipalRef::user("alice");

        store
            .create(&PolicyBinding::new(
                "b1",
                alice.clone(),
                "roles/Admin",
                Scope::System,
            ))
            .await
            .unwrap();

        store
            .create(&PolicyBinding::new(
                "b2",
                alice.clone(),
                "roles/ReadOnly",
                Scope::project("p1", "org-1"),
            ))
            .await
            .unwrap();

        store
            .create(&PolicyBinding::new(
                "b3",
                PrincipalRef::user("bob"),
                "roles/Admin",
                Scope::System,
            ))
            .await
            .unwrap();

        let bindings = store.list_by_principal(&alice).await.unwrap();
        assert_eq!(bindings.len(), 2);
    }

    #[tokio::test]
    async fn test_effective_bindings() {
        let store = BindingStore::new(test_backend());

        let alice = PrincipalRef::user("alice");

        // System-wide binding
        store
            .create(&PolicyBinding::new(
                "b1",
                alice.clone(),
                "roles/SystemAdmin",
                Scope::System,
            ))
            .await
            .unwrap();

        // Project-specific binding
        store
            .create(&PolicyBinding::new(
                "b2",
                alice.clone(),
                "roles/ProjectAdmin",
                Scope::project("proj-1", "org-1"),
            ))
            .await
            .unwrap();

        // Check effective bindings for project scope
        let effective = store
            .get_effective_bindings(&alice, &Scope::project("proj-1", "org-1"))
            .await
            .unwrap();

        // Should include both system (contains project) and project-specific
        assert_eq!(effective.len(), 2);
    }
}
