//! Condition evaluation for ABAC
//!
//! Evaluates condition expressions against the current authorization context.

use std::net::IpAddr;

use ipnetwork::IpNetwork;

use iam_types::{Condition, ConditionExpr, Error, IamError, Result};

use crate::context::VariableContext;

/// Evaluate a condition against the given context
pub fn evaluate_condition(condition: &Condition, ctx: &VariableContext<'_>) -> Result<bool> {
    evaluate_expr(&condition.expression, ctx)
}

/// Evaluate a condition expression
fn evaluate_expr(expr: &ConditionExpr, ctx: &VariableContext<'_>) -> Result<bool> {
    match expr {
        ConditionExpr::StringEquals { key, value } => {
            let actual = resolve_key(key, ctx);
            let expected = ctx.substitute(value);
            Ok(actual.as_deref() == Some(&expected))
        }

        ConditionExpr::StringNotEquals { key, value } => {
            let actual = resolve_key(key, ctx);
            let expected = ctx.substitute(value);
            Ok(actual.as_deref() != Some(&expected))
        }

        ConditionExpr::StringLike { key, pattern } => {
            let actual = require_key(key, ctx)?;
            let pattern = ctx.substitute(pattern);
            Ok(glob_match::glob_match(&pattern, &actual))
        }

        ConditionExpr::StringNotLike { key, pattern } => {
            let actual = require_key(key, ctx)?;
            let pattern = ctx.substitute(pattern);
            Ok(!glob_match::glob_match(&pattern, &actual))
        }

        ConditionExpr::NumericEquals { key, value } => {
            let actual = require_key(key, ctx)?;
            let actual_num: i64 = actual.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as number for key '{}'",
                    actual, key
                )))
            })?;
            Ok(actual_num == *value)
        }

        ConditionExpr::NumericLessThan { key, value } => {
            let actual = require_key(key, ctx)?;
            let actual_num: i64 = actual.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as number",
                    actual
                )))
            })?;
            Ok(actual_num < *value)
        }

        ConditionExpr::NumericLessThanEquals { key, value } => {
            let actual = require_key(key, ctx)?;
            let actual_num: i64 = actual.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as number",
                    actual
                )))
            })?;
            Ok(actual_num <= *value)
        }

        ConditionExpr::NumericGreaterThan { key, value } => {
            let actual = require_key(key, ctx)?;
            let actual_num: i64 = actual.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as number",
                    actual
                )))
            })?;
            Ok(actual_num > *value)
        }

        ConditionExpr::NumericGreaterThanEquals { key, value } => {
            let actual = require_key(key, ctx)?;
            let actual_num: i64 = actual.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as number",
                    actual
                )))
            })?;
            Ok(actual_num >= *value)
        }

        ConditionExpr::IpAddress { key, cidr } => {
            let actual = require_key(key, ctx)?;
            let ip: IpAddr = actual.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as IP address",
                    actual
                )))
            })?;
            let network: IpNetwork = cidr.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as CIDR",
                    cidr
                )))
            })?;
            Ok(network.contains(ip))
        }

        ConditionExpr::NotIpAddress { key, cidr } => {
            let actual = require_key(key, ctx)?;
            let ip: IpAddr = actual.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as IP address",
                    actual
                )))
            })?;
            let network: IpNetwork = cidr.parse().map_err(|_| {
                Error::Iam(IamError::InvalidCondition(format!(
                    "Cannot parse '{}' as CIDR",
                    cidr
                )))
            })?;
            Ok(!network.contains(ip))
        }

        ConditionExpr::TimeBetween { start, end } => {
            let now = ctx.context.timestamp;

            // Parse start/end as either Unix timestamp or HH:MM format
            let start_ts = parse_time(start, now)?;
            let end_ts = parse_time(end, now)?;

            Ok(now >= start_ts && now <= end_ts)
        }

        ConditionExpr::Exists { key } => Ok(resolve_key(key, ctx).is_some()),

        ConditionExpr::StringEqualsAny { key, values } => {
            let actual = resolve_key(key, ctx);
            match actual {
                Some(actual_val) => {
                    let substituted: Vec<String> =
                        values.iter().map(|v| ctx.substitute(v)).collect();
                    Ok(substituted.contains(&actual_val))
                }
                None => Ok(false),
            }
        }

        ConditionExpr::Bool { key, value } => {
            let actual = require_key(key, ctx)?;
            let actual_bool = matches!(actual.to_lowercase().as_str(), "true" | "1" | "yes");
            Ok(actual_bool == *value)
        }

        ConditionExpr::And(exprs) => {
            for e in exprs {
                if !evaluate_expr(e, ctx)? {
                    return Ok(false);
                }
            }
            Ok(true)
        }

        ConditionExpr::Or(exprs) => {
            for e in exprs {
                if evaluate_expr(e, ctx)? {
                    return Ok(true);
                }
            }
            Ok(false)
        }

        ConditionExpr::Not(inner) => Ok(!evaluate_expr(inner, ctx)?),
    }
}

/// Resolve a key to its value, returning Option
fn resolve_key(key: &str, ctx: &VariableContext<'_>) -> Option<String> {
    ctx.resolve(key)
}

/// Resolve a key, returning an error if not found
fn require_key(key: &str, ctx: &VariableContext<'_>) -> Result<String> {
    ctx.resolve(key).ok_or_else(|| {
        Error::Iam(IamError::InvalidCondition(format!(
            "Key not found: {}",
            key
        )))
    })
}

/// Parse time string to Unix timestamp
fn parse_time(time_str: &str, reference_time: u64) -> Result<u64> {
    // Try parsing as Unix timestamp
    if let Ok(ts) = time_str.parse::<u64>() {
        return Ok(ts);
    }

    // Try parsing as HH:MM (relative to current day)
    if let Some((hours, minutes)) = time_str.split_once(':') {
        let hours: u64 = hours.parse().map_err(|_| {
            Error::Iam(IamError::InvalidCondition(format!(
                "Invalid time format: {}",
                time_str
            )))
        })?;
        let minutes: u64 = minutes.parse().map_err(|_| {
            Error::Iam(IamError::InvalidCondition(format!(
                "Invalid time format: {}",
                time_str
            )))
        })?;

        // Calculate seconds since midnight
        let time_of_day = hours * 3600 + minutes * 60;

        // Get current day start
        let day_start = (reference_time / 86400) * 86400;

        return Ok(day_start + time_of_day);
    }

    // Try parsing as ISO 8601
    // For simplicity, just return an error for unsupported formats
    Err(Error::Iam(IamError::InvalidCondition(format!(
        "Unsupported time format: {}",
        time_str
    ))))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::AuthzContext;
    use iam_types::{Principal, Resource};
    use std::net::{IpAddr, Ipv4Addr};

    fn test_context<'a>(
        principal: &'a Principal,
        resource: &'a Resource,
        context: &'a AuthzContext,
    ) -> VariableContext<'a> {
        VariableContext::new(principal, resource, context)
    }

    #[test]
    fn test_string_equals() {
        let principal = Principal::new_user("alice", "Alice");
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("alice");
        let ctx = AuthzContext::new();
        let var_ctx = test_context(&principal, &resource, &ctx);

        // Direct comparison
        let cond = Condition::string_equals("resource.owner", "alice");
        assert!(evaluate_condition(&cond, &var_ctx).unwrap());

        // Variable substitution
        let cond = Condition::string_equals("resource.owner", "${principal.id}");
        assert!(evaluate_condition(&cond, &var_ctx).unwrap());

        // Non-matching
        let cond = Condition::string_equals("resource.owner", "bob");
        assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
    }

    #[test]
    fn test_string_like() {
        let principal = Principal::new_user("alice", "Alice");
        let resource = Resource::new("instance", "vm-prod-001", "org-1", "proj-1");
        let ctx = AuthzContext::new();
        let var_ctx = test_context(&principal, &resource, &ctx);

        let cond = Condition::string_like("resource.id", "vm-prod-*");
        assert!(evaluate_condition(&cond, &var_ctx).unwrap());

        let cond = Condition::string_like("resource.id", "vm-dev-*");
        assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
    }

    #[test]
    fn test_ip_address() {
        let principal = Principal::new_user("alice", "Alice");
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1");
        let ctx = AuthzContext::new().with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 1, 50)));
        let var_ctx = test_context(&principal, &resource, &ctx);

        let cond = Condition::ip_address("request.source_ip", "10.0.0.0/8");
        assert!(evaluate_condition(&cond, &var_ctx).unwrap());

        let cond = Condition::ip_address("request.source_ip", "192.168.0.0/16");
        assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
    }

    #[test]
    fn test_and_condition() {
        let principal = Principal::new_user("alice", "Alice");
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("alice");
        let ctx = AuthzContext::new().with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
        let var_ctx = test_context(&principal, &resource, &ctx);

        let cond = Condition::and(vec![
            Condition::string_equals("resource.owner", "${principal.id}"),
            Condition::ip_address("request.source_ip", "10.0.0.0/8"),
        ]);
        assert!(evaluate_condition(&cond, &var_ctx).unwrap());

        // One condition fails
        let cond = Condition::and(vec![
            Condition::string_equals("resource.owner", "bob"),
            Condition::ip_address("request.source_ip", "10.0.0.0/8"),
        ]);
        assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
    }

    #[test]
    fn test_or_condition() {
        let principal = Principal::new_user("alice", "Alice");
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("bob");
        let ctx = AuthzContext::new();
        let var_ctx = test_context(&principal, &resource, &ctx);

        let cond = Condition::or(vec![
            Condition::string_equals("resource.owner", "${principal.id}"), // false
            Condition::string_equals("principal.id", "alice"),             // true
        ]);
        assert!(evaluate_condition(&cond, &var_ctx).unwrap());
    }

    #[test]
    fn test_not_condition() {
        let principal = Principal::new_user("alice", "Alice");
        let resource = Resource::new("instance", "vm-1", "org-1", "proj-1");
        let ctx = AuthzContext::new();
        let var_ctx = test_context(&principal, &resource, &ctx);

        let cond = Condition::not(Condition::string_equals("principal.id", "bob"));
        assert!(evaluate_condition(&cond, &var_ctx).unwrap());
    }
}
