//! Policy evaluation engine
//!
//! The core authorization engine that evaluates whether a principal
//! is allowed to perform an action on a resource.

use std::sync::Arc;

use iam_store::{BindingStore, GroupStore, RoleStore};
use iam_types::{
    PolicyBinding, Principal, PrincipalKind, PrincipalRef, Resource, Result, Role, Scope,
};

use crate::cache::PolicyCache;
use crate::condition::evaluate_condition;
use crate::context::{AuthzContext, VariableContext};

/// Authorization request
#[derive(Debug, Clone)]
pub struct AuthzRequest {
    /// Principal making the request
    pub principal: Principal,
    /// Action being performed (e.g., "compute:instances:create")
    pub action: String,
    /// Resource being accessed
    pub resource: Resource,
    /// Request context
    pub context: AuthzContext,
}

impl AuthzRequest {
    /// Create a new authorization request
    pub fn new(principal: Principal, action: impl Into<String>, resource: Resource) -> Self {
        Self {
            principal,
            action: action.into(),
            resource,
            context: AuthzContext::new(),
        }
    }

    /// Set the context
    pub fn with_context(mut self, context: AuthzContext) -> Self {
        self.context = context;
        self
    }
}

/// Authorization decision
#[derive(Debug, Clone)]
pub enum AuthzDecision {
    /// Access is allowed
    Allow,
    /// Access is denied with reason
    Deny { reason: String },
}

/// Authorization decision with match metadata
#[derive(Debug, Clone)]
pub struct AuthzEvaluation {
    /// Decision result
    pub decision: AuthzDecision,
    /// Matched binding ID (if allowed)
    pub matched_binding: Option<String>,
    /// Matched role reference (if allowed)
    pub matched_role: Option<String>,
}

impl AuthzEvaluation {
    fn allow(binding_id: impl Into<String>, role_ref: impl Into<String>) -> Self {
        Self {
            decision: AuthzDecision::Allow,
            matched_binding: Some(binding_id.into()),
            matched_role: Some(role_ref.into()),
        }
    }

    fn deny(reason: impl Into<String>) -> Self {
        Self {
            decision: AuthzDecision::Deny {
                reason: reason.into(),
            },
            matched_binding: None,
            matched_role: None,
        }
    }
}

impl AuthzDecision {
    /// Check if the decision is Allow
    pub fn is_allowed(&self) -> bool {
        matches!(self, AuthzDecision::Allow)
    }

    /// Check if the decision is Deny
    pub fn is_denied(&self) -> bool {
        matches!(self, AuthzDecision::Deny { .. })
    }
}

/// Policy evaluator configuration
#[derive(Debug, Clone)]
pub struct PolicyEvaluatorConfig {
    /// Whether to use caching
    pub use_cache: bool,
    /// Maximum number of bindings to evaluate
    pub max_bindings: usize,
    /// Enable debug logging
    pub debug: bool,
}

impl Default for PolicyEvaluatorConfig {
    fn default() -> Self {
        Self {
            use_cache: true,
            max_bindings: 1000,
            debug: false,
        }
    }
}

/// Policy evaluator (PDP - Policy Decision Point)
pub struct PolicyEvaluator {
    binding_store: Arc<BindingStore>,
    role_store: Arc<RoleStore>,
    group_store: Option<Arc<GroupStore>>,
    cache: Arc<PolicyCache>,
    config: PolicyEvaluatorConfig,
}

impl PolicyEvaluator {
    /// Create a new policy evaluator
    pub fn new(
        binding_store: Arc<BindingStore>,
        role_store: Arc<RoleStore>,
        cache: Arc<PolicyCache>,
    ) -> Self {
        Self {
            binding_store,
            role_store,
            group_store: None,
            cache,
            config: PolicyEvaluatorConfig::default(),
        }
    }

    /// Create with group store for group expansion
    pub fn with_group_store(
        binding_store: Arc<BindingStore>,
        role_store: Arc<RoleStore>,
        group_store: Arc<GroupStore>,
        cache: Arc<PolicyCache>,
    ) -> Self {
        Self {
            binding_store,
            role_store,
            group_store: Some(group_store),
            cache,
            config: PolicyEvaluatorConfig::default(),
        }
    }

    /// Set configuration
    pub fn with_config(mut self, config: PolicyEvaluatorConfig) -> Self {
        self.config = config;
        self
    }

    /// Evaluate an authorization request
    pub async fn evaluate(&self, req: &AuthzRequest) -> Result<AuthzDecision> {
        Ok(self.evaluate_with_match(req).await?.decision)
    }

    /// Evaluate an authorization request with match metadata
    pub async fn evaluate_with_match(&self, req: &AuthzRequest) -> Result<AuthzEvaluation> {
        // Default deny
        let mut evaluation = AuthzEvaluation::deny("No matching policy");

        // Get resource scope
        let resource_scope = Scope::resource(
            &req.resource.id,
            &req.resource.project_id,
            &req.resource.org_id,
        );

        // Get effective bindings for the principal
        let bindings = self
            .get_effective_bindings(&req.principal.to_ref(), &resource_scope)
            .await?;

        if bindings.is_empty() {
            return Ok(AuthzEvaluation::deny(format!(
                "No bindings found for principal {}",
                req.principal.to_ref()
            )));
        }

        // Evaluate each binding
        for binding in bindings.iter().take(self.config.max_bindings) {
            // Skip disabled or expired bindings
            let now = req.context.timestamp;
            if !binding.is_active(now) {
                continue;
            }

            // Get the role
            let role = match self.get_role(&binding.role_ref).await? {
                Some(r) => r,
                None => {
                    tracing::warn!("Role not found: {}", binding.role_ref);
                    continue;
                }
            };

            // Evaluate binding condition (if any)
            if let Some(cond) = &binding.condition {
                let var_ctx = VariableContext::new(&req.principal, &req.resource, &req.context);
                if !evaluate_condition(cond, &var_ctx)? {
                    continue;
                }
            }

            // Evaluate role permissions
            if self.evaluate_role(&role, req)? {
                evaluation = AuthzEvaluation::allow(binding.id.clone(), binding.role_ref.clone());
                break;
            }
        }

        Ok(evaluation)
    }

    /// Check if a specific action is allowed
    pub async fn is_allowed(
        &self,
        principal: &Principal,
        action: &str,
        resource: &Resource,
    ) -> Result<bool> {
        let req = AuthzRequest::new(principal.clone(), action, resource.clone());
        let decision = self.evaluate(&req).await?;
        Ok(decision.is_allowed())
    }

    /// Check if a specific action is allowed with context
    pub async fn is_allowed_with_context(
        &self,
        principal: &Principal,
        action: &str,
        resource: &Resource,
        context: AuthzContext,
    ) -> Result<bool> {
        let req =
            AuthzRequest::new(principal.clone(), action, resource.clone()).with_context(context);
        let decision = self.evaluate(&req).await?;
        Ok(decision.is_allowed())
    }

    /// Evaluate role permissions against the request
    fn evaluate_role(&self, role: &Role, req: &AuthzRequest) -> Result<bool> {
        let var_ctx = VariableContext::new(&req.principal, &req.resource, &req.context);

        for permission in &role.permissions {
            // Check action pattern
            if !matches_action(&permission.action, &req.action) {
                continue;
            }

            // Check resource pattern
            let resource_path = req.resource.to_path();
            let pattern = var_ctx.substitute(&permission.resource_pattern);
            if !matches_resource(&pattern, &resource_path) {
                continue;
            }

            // Check permission condition (if any)
            if let Some(cond) = &permission.condition {
                if !evaluate_condition(cond, &var_ctx)? {
                    continue;
                }
            }

            // All checks passed - permission matches
            return Ok(true);
        }

        Ok(false)
    }

    /// Get effective bindings for a principal (with caching)
    ///
    /// If group_store is configured, this will also include bindings
    /// for any groups the principal belongs to.
    async fn get_effective_bindings(
        &self,
        principal: &PrincipalRef,
        scope: &Scope,
    ) -> Result<Vec<PolicyBinding>> {
        // Check cache first
        if self.config.use_cache {
            if let Some(bindings) = self.cache.get_bindings(principal) {
                // Filter to scope
                let effective: Vec<_> = bindings
                    .into_iter()
                    .filter(|b| b.scope.contains(scope))
                    .collect();
                return Ok(effective);
            }
        }

        // Fetch bindings for the principal
        let mut bindings = self
            .binding_store
            .get_effective_bindings(principal, scope)
            .await?;

        // If group store is available, expand group memberships
        if let Some(group_store) = &self.group_store {
            // Get groups the principal belongs to
            let groups = group_store.list_groups(principal).await?;

            // Get bindings for each group
            for group_id in groups {
                let group_ref = PrincipalRef::new(PrincipalKind::Group, &group_id);
                let group_bindings = self
                    .binding_store
                    .get_effective_bindings(&group_ref, scope)
                    .await?;
                bindings.extend(group_bindings);
            }
        }

        // Cache if enabled
        if self.config.use_cache {
            self.cache.put_bindings(principal, bindings.clone());
        }

        Ok(bindings)
    }

    /// Get a role (with caching)
    async fn get_role(&self, role_ref: &str) -> Result<Option<Role>> {
        let name = role_ref.strip_prefix("roles/").unwrap_or(role_ref);

        // Check cache first
        if self.config.use_cache {
            if let Some(role) = self.cache.get_role(name) {
                return Ok(Some(role));
            }
        }

        // Fetch from store
        let role = self.role_store.get(name).await?;

        // Cache if enabled and found
        if self.config.use_cache {
            if let Some(ref r) = role {
                self.cache.put_role(r.clone());
            }
        }

        Ok(role)
    }

    /// Invalidate cache for a principal
    pub fn invalidate_principal(&self, principal: &PrincipalRef) {
        self.cache.invalidate_bindings(principal);
    }

    /// Invalidate cache for a role
    pub fn invalidate_role(&self, role_name: &str) {
        self.cache.invalidate_role(role_name);
    }

    /// Invalidate all caches
    pub fn invalidate_all(&self) {
        self.cache.invalidate_all();
    }
}

/// Match action pattern against action
/// Supports wildcards: "*" matches everything, "compute:*" matches all compute actions
fn matches_action(pattern: &str, action: &str) -> bool {
    if pattern == "*" {
        return true;
    }

    if !pattern.contains('*') {
        return pattern == action;
    }

    // Split by ":"
    let pattern_parts: Vec<&str> = pattern.split(':').collect();
    let action_parts: Vec<&str> = action.split(':').collect();

    // Compare each part
    for (i, p) in pattern_parts.iter().enumerate() {
        if *p == "*" {
            // Wildcard matches rest
            if i == pattern_parts.len() - 1 {
                return true;
            }
            continue;
        }

        if i >= action_parts.len() {
            return false;
        }

        if *p != action_parts[i] {
            return false;
        }
    }

    pattern_parts.len() == action_parts.len()
}

/// Match resource pattern against resource path
fn matches_resource(pattern: &str, path: &str) -> bool {
    if pattern == "*" {
        return true;
    }

    // Handle trailing /* as "match all remaining" ONLY if there are no other wildcards
    // This allows patterns like "project/p1/*" to match "project/p1/instance/vm-1"
    if let Some(prefix) = pattern.strip_suffix("/*") {
        // Only use special handling if prefix has no wildcards
        if !prefix.contains('*') && !prefix.contains('?') {
            return path.starts_with(prefix)
                && path.len() > prefix.len()
                && path.as_bytes()[prefix.len()] == b'/';
        }
    }

    // Use glob matching for other patterns
    glob_match::glob_match(pattern, path)
}

#[cfg(test)]
mod tests {
    use super::*;
    use iam_store::Backend;
    use iam_types::Permission;

    fn test_stores() -> (Arc<BindingStore>, Arc<RoleStore>) {
        let backend = Arc::new(Backend::memory());
        let binding_store = Arc::new(BindingStore::new(backend.clone()));
        let role_store = Arc::new(RoleStore::new(backend));
        (binding_store, role_store)
    }

    #[test]
    fn test_action_matching() {
        assert!(matches_action("*", "compute:instances:create"));
        assert!(matches_action("compute:*", "compute:instances:create"));
        assert!(matches_action(
            "compute:instances:*",
            "compute:instances:create"
        ));
        assert!(matches_action(
            "compute:instances:create",
            "compute:instances:create"
        ));
        assert!(!matches_action(
            "compute:instances:delete",
            "compute:instances:create"
        ));
        assert!(!matches_action("storage:*", "compute:instances:create"));
    }

    #[test]
    fn test_resource_matching() {
        assert!(matches_resource("*", "project/p1/instance/vm-1"));
        assert!(matches_resource(
            "project/*/instance/*",
            "project/p1/instance/vm-1"
        ));
        assert!(matches_resource(
            "project/p1/*",
            "project/p1/instance/vm-1"
        ));
        assert!(!matches_resource(
            "project/p2/*",
            "project/p1/instance/vm-1"
        ));
    }

    #[tokio::test]
    async fn test_evaluator_allow() {
        let (binding_store, role_store) = test_stores();
        let cache = Arc::new(PolicyCache::default_config());

        // Initialize builtin roles
        role_store.init_builtin_roles().await.unwrap();

        // Create a binding for alice as SystemAdmin
        let alice = PrincipalRef::user("alice");
        binding_store
            .create(&PolicyBinding::new(
                "b1",
                alice.clone(),
                "roles/SystemAdmin",
                Scope::System,
            ))
            .await
            .unwrap();

        let evaluator = PolicyEvaluator::new(binding_store, role_store, cache);

        let principal = Principal::new_user("alice", "Alice");
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1");

        let req = AuthzRequest::new(principal, "compute:instances:delete", resource);
        let decision = evaluator.evaluate(&req).await.unwrap();

        assert!(decision.is_allowed());
    }

    #[tokio::test]
    async fn test_evaluator_resource_scoped_binding() {
        let (binding_store, role_store) = test_stores();
        let cache = Arc::new(PolicyCache::default_config());

        // Initialize builtin roles
        role_store.init_builtin_roles().await.unwrap();

        // Bind alice to a single resource
        let alice = PrincipalRef::user("alice");
        binding_store
            .create(&PolicyBinding::new(
                "b1",
                alice.clone(),
                "roles/SystemAdmin",
                Scope::resource("vm-1", "proj-1", "org-1"),
            ))
            .await
            .unwrap();

        let evaluator = PolicyEvaluator::new(binding_store, role_store, cache);
        let principal = Principal::new_user("alice", "Alice");

        // Matching resource should be allowed
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1");
        let decision = evaluator
            .evaluate(&AuthzRequest::new(
                principal.clone(),
                "compute:instances:delete",
                resource,
            ))
            .await
            .unwrap();
        assert!(decision.is_allowed());

        // Different resource should be denied
        let resource = Resource::new("instance", "vm-2", "org-1", "proj-1");
        let decision = evaluator
            .evaluate(&AuthzRequest::new(
                principal,
                "compute:instances:delete",
                resource,
            ))
            .await
            .unwrap();
        assert!(decision.is_denied());
    }

    #[tokio::test]
    async fn test_evaluator_deny() {
        let (binding_store, role_store) = test_stores();
        let cache = Arc::new(PolicyCache::default_config());

        role_store.init_builtin_roles().await.unwrap();

        // No bindings for bob
        let evaluator = PolicyEvaluator::new(binding_store, role_store, cache);

        let principal = Principal::new_user("bob", "Bob");
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1");

        let req = AuthzRequest::new(principal, "compute:instances:delete", resource);
        let decision = evaluator.evaluate(&req).await.unwrap();

        assert!(decision.is_denied());
    }

    #[tokio::test]
    async fn test_evaluator_with_condition() {
        let (binding_store, role_store) = test_stores();
        let cache = Arc::new(PolicyCache::default_config());

        // Create a custom role with condition
        // Note: Resource path format is org/{org_id}/project/{project_id}/{kind}/{id}
        let role = Role::new(
            "OwnerOnly",
            Scope::project("*", "*"),
            vec![
                Permission::new("compute:instances:*", "org/*/project/*/instance/*")
                    .with_condition(iam_types::Condition::string_equals(
                        "resource.owner",
                        "${principal.id}",
                    )),
            ],
        );
        role_store.create_internal(&role).await.unwrap();

        // Create binding
        let alice = PrincipalRef::user("alice");
        binding_store
            .create(&PolicyBinding::new(
                "b1",
                alice,
                "roles/OwnerOnly",
                Scope::project("proj-1", "org-1"),
            ))
            .await
            .unwrap();

        let evaluator = PolicyEvaluator::new(binding_store, role_store, cache);

        let principal = Principal::new_user("alice", "Alice");

        // Alice owns this resource - should be allowed
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("alice");
        let decision = evaluator
            .is_allowed(&principal, "compute:instances:delete", &resource)
            .await
            .unwrap();
        assert!(decision);

        // Alice doesn't own this resource - should be denied
        let resource = Resource::new("instance", "vm-2", "org-1", "proj-1").with_owner("bob");
        let decision = evaluator
            .is_allowed(&principal, "compute:instances:delete", &resource)
            .await
            .unwrap();
        assert!(!decision);
    }
}
