use globset::{Glob, GlobSet, GlobSetBuilder};
use serde::{Deserialize, Serialize};

use crate::{error::KeeperError, types::PendingOp};

// --------------------------------------------------------------------------
// Schema mirror — matches keeper_policy.ncl PolicyDef and MatchRule exactly
// --------------------------------------------------------------------------

fn default_wildcard() -> String {
    "*".to_string()
}

fn default_wildcard_vec() -> Vec<String> {
    vec!["*".to_string()]
}

fn default_version() -> u32 {
    1
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchRule {
    #[serde(default = "default_wildcard")]
    pub op_type: String,
    #[serde(default = "default_wildcard_vec")]
    pub image_patterns: Vec<String>,
    #[serde(default = "default_wildcard_vec")]
    pub target_patterns: Vec<String>,
    #[serde(default = "default_wildcard_vec")]
    pub scope_patterns: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyDef {
    #[serde(default = "default_version")]
    pub version: u32,
    #[serde(default)]
    pub auto_sign: Vec<MatchRule>,
    #[serde(default)]
    pub require_manual: Vec<MatchRule>,
}

// --------------------------------------------------------------------------
// Compiled rule — GlobSets pre-built for O(1) matching per op
// --------------------------------------------------------------------------

struct CompiledRule {
    op_type: String,
    target_set: GlobSet,
    image_set: GlobSet,
}

/// Policy decision for a given pending op.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
    AutoSign,
    RequireManual,
    HoldPending,
}

impl Decision {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::AutoSign => "auto_sign",
            Self::RequireManual => "require_manual",
            Self::HoldPending => "hold_pending",
        }
    }
}

/// Compiled policy matcher. Create once per policy load; call `decide` per op.
pub struct PolicyMatcher {
    auto_sign: Vec<CompiledRule>,
    require_manual: Vec<CompiledRule>,
}

impl PolicyMatcher {
    pub fn from_policy(policy: &PolicyDef) -> Result<Self, KeeperError> {
        Ok(Self {
            auto_sign: compile_rules(&policy.auto_sign)?,
            require_manual: compile_rules(&policy.require_manual)?,
        })
    }

    /// Evaluate the policy against a pending op.
    /// Rules are checked top-to-bottom; first matching rule wins.
    /// auto_sign rules are evaluated before require_manual rules.
    pub fn decide(&self, op: &PendingOp) -> Decision {
        for rule in &self.auto_sign {
            if rule_matches(rule, op) {
                return Decision::AutoSign;
            }
        }
        for rule in &self.require_manual {
            if rule_matches(rule, op) {
                return Decision::RequireManual;
            }
        }
        Decision::HoldPending
    }
}

fn compile_rules(rules: &[MatchRule]) -> Result<Vec<CompiledRule>, KeeperError> {
    rules.iter().map(compile_rule).collect()
}

fn compile_rule(rule: &MatchRule) -> Result<CompiledRule, KeeperError> {
    let target_set = build_glob_set(&rule.target_patterns)?;
    let image_set = build_glob_set(&rule.image_patterns)?;
    Ok(CompiledRule {
        op_type: rule.op_type.clone(),
        target_set,
        image_set,
    })
}

fn build_glob_set(patterns: &[String]) -> Result<GlobSet, KeeperError> {
    let mut builder = GlobSetBuilder::new();
    for p in patterns {
        let glob = Glob::new(p)
            .map_err(|e| KeeperError::Glob(format!("invalid glob '{p}': {e}")))?;
        builder.add(glob);
    }
    builder
        .build()
        .map_err(|e| KeeperError::Glob(e.to_string()))
}

fn rule_matches(rule: &CompiledRule, op: &PendingOp) -> bool {
    let op_type_ok = rule.op_type == "*" || rule.op_type == op.op_type;
    let target_ok = rule.target_set.is_match(&op.target);
    let image_ok = op.image.is_empty() || rule.image_set.is_match(&op.image);
    op_type_ok && target_ok && image_ok
}

// --------------------------------------------------------------------------
// Policy file loading
// --------------------------------------------------------------------------

/// Load PolicyDef by running `nickel export` on policy_path.
/// The policy file must conform to keeper_policy.ncl — no functions allowed.
pub fn load_policy(policy_path: &std::path::Path) -> Result<PolicyDef, KeeperError> {
    let output = std::process::Command::new("nickel")
        .arg("export")
        .arg(policy_path)
        .output()
        .map_err(|e| KeeperError::Policy(format!("nickel not found in PATH: {e}")))?;

    if !output.status.success() {
        return Err(KeeperError::Policy(format!(
            "nickel export '{}' failed: {}",
            policy_path.display(),
            String::from_utf8_lossy(&output.stderr).trim()
        )));
    }

    let policy: PolicyDef = serde_json::from_slice(&output.stdout)?;
    if policy.version != 1 {
        return Err(KeeperError::Policy(format!(
            "unsupported policy schema version {}; expected 1",
            policy.version
        )));
    }
    Ok(policy)
}

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

    fn make_op(op_type: &str, target: &str) -> PendingOp {
        PendingOp {
            op_type: op_type.to_string(),
            target: target.to_string(),
            sub: "test".to_string(),
            expected_state_version: "v1".to_string(),
            image: String::new(),
            params: serde_json::Value::Null,
        }
    }

    #[test]
    fn auto_sign_wildcard_matches_any_op() {
        let policy = PolicyDef {
            version: 1,
            auto_sign: vec![MatchRule {
                op_type: "*".to_string(),
                target_patterns: vec!["dev-*".to_string()],
                image_patterns: vec!["*".to_string()],
                scope_patterns: vec!["*".to_string()],
            }],
            require_manual: vec![],
        };
        let matcher = PolicyMatcher::from_policy(&policy).unwrap();
        assert_eq!(matcher.decide(&make_op("scale", "dev-vapora")), Decision::AutoSign);
        assert_eq!(matcher.decide(&make_op("deploy", "dev-api")), Decision::AutoSign);
        assert_eq!(matcher.decide(&make_op("scale", "prod-vapora")), Decision::HoldPending);
    }

    #[test]
    fn require_manual_checked_after_auto_sign_miss() {
        let policy = PolicyDef {
            version: 1,
            auto_sign: vec![MatchRule {
                op_type: "scale".to_string(),
                target_patterns: vec!["staging-*".to_string()],
                image_patterns: vec!["*".to_string()],
                scope_patterns: vec!["*".to_string()],
            }],
            require_manual: vec![MatchRule {
                op_type: "deploy".to_string(),
                target_patterns: vec!["staging-*".to_string()],
                image_patterns: vec!["*".to_string()],
                scope_patterns: vec!["*".to_string()],
            }],
        };
        let matcher = PolicyMatcher::from_policy(&policy).unwrap();
        assert_eq!(matcher.decide(&make_op("scale", "staging-x")), Decision::AutoSign);
        assert_eq!(matcher.decide(&make_op("deploy", "staging-x")), Decision::RequireManual);
        assert_eq!(matcher.decide(&make_op("restart", "staging-x")), Decision::HoldPending);
    }

    #[test]
    fn invalid_glob_returns_error() {
        let policy = PolicyDef {
            version: 1,
            auto_sign: vec![MatchRule {
                op_type: "*".to_string(),
                target_patterns: vec!["[invalid".to_string()],
                image_patterns: vec!["*".to_string()],
                scope_patterns: vec!["*".to_string()],
            }],
            require_manual: vec![],
        };
        assert!(PolicyMatcher::from_policy(&policy).is_err());
    }
}
