//! Policy types for IAM
//!
//! Policies bind principals to roles within a specific scope.

use serde::{Deserialize, Serialize};

use crate::condition::Condition;
use crate::principal::PrincipalRef;
use crate::scope::Scope;

/// A binding that assigns a role to a principal within a scope
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyBinding {
    /// Unique identifier for this binding
    pub id: String,

    /// Reference to the principal (e.g., "user:alice", "group:developers")
    pub principal_ref: PrincipalRef,

    /// Reference to the role (e.g., "roles/ProjectAdmin")
    pub role_ref: String,

    /// Scope where this binding applies
    pub scope: Scope,

    /// Optional condition for this binding (time-limited, IP-restricted, etc.)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub condition: Option<Condition>,

    /// Creation timestamp (TSO format)
    pub created_at: u64,

    /// Last update timestamp (TSO format)
    pub updated_at: u64,

    /// Who created this binding
    pub created_by: String,

    /// Optional expiration timestamp (TSO format)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<u64>,

    /// Whether the binding is enabled
    pub enabled: bool,
}

impl PolicyBinding {
    /// Create a new policy binding
    pub fn new(
        id: impl Into<String>,
        principal_ref: PrincipalRef,
        role_ref: impl Into<String>,
        scope: Scope,
    ) -> Self {
        Self {
            id: id.into(),
            principal_ref,
            role_ref: role_ref.into(),
            scope,
            condition: None,
            created_at: 0,
            updated_at: 0,
            created_by: String::new(),
            expires_at: None,
            enabled: true,
        }
    }

    /// Set the condition
    pub fn with_condition(mut self, condition: Condition) -> Self {
        self.condition = Some(condition);
        self
    }

    /// Set the creator
    pub fn with_created_by(mut self, created_by: impl Into<String>) -> Self {
        self.created_by = created_by.into();
        self
    }

    /// Set expiration
    pub fn with_expires_at(mut self, expires_at: u64) -> Self {
        self.expires_at = Some(expires_at);
        self
    }

    /// Disable the binding
    pub fn disabled(mut self) -> Self {
        self.enabled = false;
        self
    }

    /// Check if the binding is expired (given current TSO timestamp)
    pub fn is_expired(&self, now: u64) -> bool {
        self.expires_at.is_some_and(|exp| now > exp)
    }

    /// Check if the binding is active (enabled and not expired)
    pub fn is_active(&self, now: u64) -> bool {
        self.enabled && !self.is_expired(now)
    }

    /// Generate a storage key for this binding
    ///
    /// Key format: `iam/bindings/scope/{scope}/principal/{principal_kind}:{principal_id}/{binding_id}`
    pub fn storage_key(&self) -> String {
        format!(
            "iam/bindings/scope/{}/principal/{}/{}",
            self.scope.to_key(),
            self.principal_ref,
            self.id
        )
    }

    /// Generate a secondary index key by principal
    ///
    /// Key format: `iam/bindings/by-principal/{principal_kind}:{principal_id}/{binding_id}`
    pub fn principal_index_key(&self) -> String {
        format!(
            "iam/bindings/by-principal/{}/{}",
            self.principal_ref, self.id
        )
    }

    /// Generate a secondary index key by role
    ///
    /// Key format: `iam/bindings/by-role/{role_ref}/{binding_id}`
    pub fn role_index_key(&self) -> String {
        format!("iam/bindings/by-role/{}/{}", self.role_ref, self.id)
    }
}

/// Request to create a new binding
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateBindingRequest {
    /// Principal to bind
    pub principal_ref: PrincipalRef,

    /// Role to assign
    pub role_ref: String,

    /// Scope for the binding
    pub scope: Scope,

    /// Optional condition
    #[serde(skip_serializing_if = "Option::is_none")]
    pub condition: Option<Condition>,

    /// Optional expiration (seconds from now)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl_seconds: Option<u64>,
}

/// Result of checking a binding's effectiveness
#[derive(Debug, Clone)]
pub struct BindingEffect {
    /// The binding that was checked
    pub binding: PolicyBinding,

    /// Whether the binding is currently effective
    pub effective: bool,

    /// Reason if not effective
    pub reason: Option<String>,
}

/// Aggregated policy for a principal
#[derive(Debug, Clone, Default)]
pub struct EffectivePolicy {
    /// All active bindings for the principal
    pub bindings: Vec<PolicyBinding>,

    /// Roles referenced by the bindings
    pub roles: Vec<String>,

    /// Scopes where the principal has permissions
    pub scopes: Vec<Scope>,
}

impl EffectivePolicy {
    /// Create an empty effective policy
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a binding to the effective policy
    pub fn add_binding(&mut self, binding: PolicyBinding) {
        if !self.roles.contains(&binding.role_ref) {
            self.roles.push(binding.role_ref.clone());
        }
        if !self.scopes.contains(&binding.scope) {
            self.scopes.push(binding.scope.clone());
        }
        self.bindings.push(binding);
    }

    /// Check if the principal has any bindings
    pub fn has_bindings(&self) -> bool {
        !self.bindings.is_empty()
    }

    /// Get bindings for a specific scope
    pub fn bindings_for_scope(&self, scope: &Scope) -> Vec<&PolicyBinding> {
        self.bindings
            .iter()
            .filter(|b| b.scope.contains(scope) || scope.contains(&b.scope))
            .collect()
    }
}

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

    #[test]
    fn test_binding_creation() {
        let binding = PolicyBinding::new(
            "binding-1",
            PrincipalRef::user("alice"),
            "roles/ProjectAdmin",
            Scope::project("my-project", "my-org"),
        );

        assert_eq!(binding.id, "binding-1");
        assert_eq!(binding.principal_ref.kind, PrincipalKind::User);
        assert_eq!(binding.principal_ref.id, "alice");
        assert!(binding.enabled);
    }

    #[test]
    fn test_binding_expiration() {
        let binding = PolicyBinding::new(
            "binding-1",
            PrincipalRef::user("alice"),
            "roles/ReadOnly",
            Scope::System,
        )
        .with_expires_at(1000);

        assert!(!binding.is_expired(500));
        assert!(binding.is_expired(1001));
        assert!(binding.is_active(500));
        assert!(!binding.is_active(1001));
    }

    #[test]
    fn test_binding_disabled() {
        let binding = PolicyBinding::new(
            "binding-1",
            PrincipalRef::user("alice"),
            "roles/ReadOnly",
            Scope::System,
        )
        .disabled();

        assert!(!binding.enabled);
        assert!(!binding.is_active(0));
    }

    #[test]
    fn test_storage_key() {
        let binding = PolicyBinding::new(
            "binding-123",
            PrincipalRef::user("alice"),
            "roles/ProjectAdmin",
            Scope::project("proj-1", "org-1"),
        );

        let key = binding.storage_key();
        assert!(key.contains("project/proj-1"));
        assert!(key.contains("user:alice"));
        assert!(key.contains("binding-123"));
    }

    #[test]
    fn test_effective_policy() {
        let mut policy = EffectivePolicy::new();

        policy.add_binding(PolicyBinding::new(
            "b1",
            PrincipalRef::user("alice"),
            "roles/ProjectAdmin",
            Scope::project("proj-1", "org-1"),
        ));

        policy.add_binding(PolicyBinding::new(
            "b2",
            PrincipalRef::user("alice"),
            "roles/ReadOnly",
            Scope::project("proj-2", "org-1"),
        ));

        assert!(policy.has_bindings());
        assert_eq!(policy.bindings.len(), 2);
        assert_eq!(policy.roles.len(), 2);
        assert_eq!(policy.scopes.len(), 2);
    }

    #[test]
    fn test_binding_serialization() {
        let binding = PolicyBinding::new(
            "binding-1",
            PrincipalRef::service_account("compute-agent"),
            "roles/ServiceRole-ComputeAgent",
            Scope::System,
        )
        .with_condition(Condition::ip_address("request.source_ip", "10.0.0.0/8"))
        .with_created_by("admin");

        let json = serde_json::to_string(&binding).unwrap();
        let parsed: PolicyBinding = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed.id, "binding-1");
        assert!(parsed.condition.is_some());
        assert_eq!(parsed.created_by, "admin");
    }
}
