//! Authorization context
//!
//! Provides context for policy evaluation including principal, resource, and request metadata.

use std::collections::HashMap;
use std::net::IpAddr;

use iam_types::{Principal, Resource};

/// Context for authorization evaluation
#[derive(Debug, Clone)]
pub struct AuthzContext {
    /// Source IP address of the request
    pub source_ip: Option<IpAddr>,

    /// Request timestamp (Unix seconds)
    pub timestamp: u64,

    /// Request metadata
    pub metadata: HashMap<String, String>,

    /// HTTP method (if applicable)
    pub http_method: Option<String>,

    /// Request path (if applicable)
    pub request_path: Option<String>,
}

impl AuthzContext {
    /// Create a new context with the current timestamp
    pub fn new() -> Self {
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs();

        Self {
            source_ip: None,
            timestamp,
            metadata: HashMap::new(),
            http_method: None,
            request_path: None,
        }
    }

    /// Set the source IP
    pub fn with_source_ip(mut self, ip: IpAddr) -> Self {
        self.source_ip = Some(ip);
        self
    }

    /// Set the timestamp
    pub fn with_timestamp(mut self, timestamp: u64) -> Self {
        self.timestamp = timestamp;
        self
    }

    /// Add metadata
    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }

    /// Set HTTP method
    pub fn with_http_method(mut self, method: impl Into<String>) -> Self {
        self.http_method = Some(method.into());
        self
    }

    /// Set request path
    pub fn with_request_path(mut self, path: impl Into<String>) -> Self {
        self.request_path = Some(path.into());
        self
    }

    /// Get a value for condition evaluation
    pub fn get_value(&self, key: &str) -> Option<String> {
        match key {
            "request.source_ip" => self.source_ip.map(|ip| ip.to_string()),
            "request.time" => Some(self.timestamp.to_string()),
            "request.method" => self.http_method.clone(),
            "request.path" => self.request_path.clone(),
            key if key.starts_with("request.metadata.") => {
                let meta_key = &key["request.metadata.".len()..];
                self.metadata.get(meta_key).cloned()
            }
            _ => None,
        }
    }
}

impl Default for AuthzContext {
    fn default() -> Self {
        Self::new()
    }
}

/// Variable context for condition evaluation
/// Combines principal, resource, and request context
pub struct VariableContext<'a> {
    pub principal: &'a Principal,
    pub resource: &'a Resource,
    pub context: &'a AuthzContext,
}

impl<'a> VariableContext<'a> {
    /// Create a new variable context
    pub fn new(
        principal: &'a Principal,
        resource: &'a Resource,
        context: &'a AuthzContext,
    ) -> Self {
        Self {
            principal,
            resource,
            context,
        }
    }

    /// Resolve a variable key to its value
    pub fn resolve(&self, key: &str) -> Option<String> {
        if let Some(prop) = key.strip_prefix("principal.") {
            self.resolve_principal(prop)
        } else if let Some(prop) = key.strip_prefix("resource.") {
            self.resource.get_property(prop)
        } else if key.starts_with("request.") {
            self.context.get_value(key)
        } else {
            None
        }
    }

    fn resolve_principal(&self, prop: &str) -> Option<String> {
        match prop {
            "id" => Some(self.principal.id.clone()),
            "kind" => Some(self.principal.kind.to_string()),
            "name" => Some(self.principal.name.clone()),
            "org_id" => self.principal.org_id.clone(),
            "project_id" => self.principal.project_id.clone(),
            "node_id" => self.principal.node_id.clone(),
            "email" => self.principal.email.clone(),
            key if key.starts_with("metadata.") => {
                let meta_key = &key["metadata.".len()..];
                self.principal.metadata.get(meta_key).cloned()
            }
            _ => None,
        }
    }

    /// Substitute variables in a string (${var} syntax)
    pub fn substitute(&self, template: &str) -> String {
        let mut result = template.to_string();
        let mut start = 0;

        while let Some(var_start) = result[start..].find("${") {
            let absolute_start = start + var_start;
            if let Some(var_end) = result[absolute_start..].find('}') {
                let absolute_end = absolute_start + var_end;
                let var_name = &result[absolute_start + 2..absolute_end];

                if let Some(value) = self.resolve(var_name) {
                    result = format!(
                        "{}{}{}",
                        &result[..absolute_start],
                        value,
                        &result[absolute_end + 1..]
                    );
                    start = absolute_start + value.len();
                } else {
                    // Variable not found, keep as is
                    start = absolute_end + 1;
                }
            } else {
                break;
            }
        }

        result
    }
}

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

    #[test]
    fn test_context_values() {
        let ctx = AuthzContext::new()
            .with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))
            .with_metadata("tenant", "acme");

        assert_eq!(ctx.get_value("request.source_ip"), Some("10.0.0.1".into()));
        assert_eq!(
            ctx.get_value("request.metadata.tenant"),
            Some("acme".into())
        );
    }

    #[test]
    fn test_variable_substitution() {
        let principal = Principal::new_user("alice", "Alice Smith");
        let resource = Resource::new("instance", "vm-123", "org-1", "proj-1").with_owner("alice");
        let context = AuthzContext::new();

        let var_ctx = VariableContext::new(&principal, &resource, &context);

        // Test simple substitution
        assert_eq!(var_ctx.substitute("user-${principal.id}"), "user-alice");

        // Test multiple substitutions
        assert_eq!(
            var_ctx.substitute("${resource.kind}/${resource.id}"),
            "instance/vm-123"
        );

        // Test unknown variable (kept as is)
        assert_eq!(var_ctx.substitute("${unknown.var}"), "${unknown.var}");
    }
}
