use std::collections::{HashMap, HashSet, VecDeque};

use chrono::{DateTime, Duration, Utc};
use innerwarden_core::{entities::EntityRef, event::Event, event::Severity, incident::Incident};

/// Processes that legitimately make many outbound connections and should be
/// excluded from C2 beaconing/exfil checks (still checked for C2 port matches).
const C2_ALLOWED_COMMS: &[&str] = &[
    "gomon",        // Go monitoring agent (health checks to many IPs)
    "crowdsec",     // CrowdSec threat intel queries
    "prometheus",   // Prometheus scraper
    "telegraf",     // Telegraf metrics collector
    "node_export",  // Node exporter (truncated comm)
    "apache2",      // Web server connecting to database/backends
    "httpd",        // Apache on RHEL/CentOS
    "nginx",        // Reverse proxy to backends
    "mysqld",       // MySQL server replication/connections
    "postgres",     // PostgreSQL connections
    "redis-server", // Redis replication
    "php-fpm",      // PHP workers connecting to databases
    "gunicorn",     // Python WSGI server
    "uvicorn",      // Python ASGI server
    "puma",         // Ruby web server
];

/// Known DNS-over-HTTPS resolver IPs. Non-browser processes connecting to these
/// on port 443 may be using DoH to evade DNS monitoring.
const DOH_RESOLVER_IPS: &[&str] = &[
    "1.1.1.1",
    "1.0.0.1", // Cloudflare
    "8.8.8.8",
    "8.8.4.4", // Google
    "9.9.9.9",
    "149.112.112.112", // Quad9
    "208.67.222.222",
    "208.67.220.220", // Cisco Umbrella
    "94.140.14.14",
    "94.140.15.15", // AdGuard
    "185.228.168.168",
    "185.228.169.168", // CleanBrowsing
];

/// Processes that legitimately use DoH (browsers, VPN clients, system resolvers).
const DOH_ALLOWED_COMMS: &[&str] = &[
    "firefox",
    "chrome",
    "chromium",
    "brave",
    "safari",
    "edge",
    "openvpn",
    "wireguard",
    "nordvpn",
    "expressvpn",
    "protonvpn",
    "systemd-resolve",
    "dnsmasq",
    "unbound",
    "named",
    "coredns",
    "dnscrypt-proxy",
    "stubby",
];

/// Detects Command & Control (C2) callback patterns from outbound connections.
///
/// Patterns detected:
/// 1. Connections to well-known C2 ports (4444, 1337, 31337, 8888, 9999)
/// 2. Beaconing - periodic connections to the same IP at regular intervals
/// 3. Unusual processes making outbound connections (sh, bash, python, perl, nc)
/// 4. Burst of connections to many different IPs in short time (data exfil)
/// 5. DNS-over-HTTPS evasion (non-browser process connecting to DoH resolvers)
pub struct C2CallbackDetector {
    window: Duration,
    /// Per-IP ring of connection timestamps (for beaconing detection)
    connections: HashMap<String, VecDeque<ConnectionRecord>>,
    /// Track unique destination IPs per process in window (for exfil detection)
    process_destinations: HashMap<String, HashSet<String>>,
    /// Suppress re-alerts per IP within window
    alerted: HashMap<String, DateTime<Utc>>,
    host: String,
    /// Known C2 ports
    c2_ports: HashSet<u16>,
    /// Suspicious processes that shouldn't make outbound connections
    suspicious_processes: HashSet<String>,
}

#[derive(Clone)]
#[allow(dead_code)]
struct ConnectionRecord {
    ts: DateTime<Utc>,
    dst_ip: String,
    dst_port: u16,
    comm: String,
    pid: u32,
}

struct IncidentParams<'a> {
    dst_ip: &'a str,
    dst_port: u16,
    comm: &'a str,
    pid: u32,
    ts: DateTime<Utc>,
    pattern: &'a str,
    severity: Severity,
    summary: String,
}

impl C2CallbackDetector {
    pub fn new(host: impl Into<String>, window_seconds: u64) -> Self {
        let c2_ports: HashSet<u16> = [
            4444, 4445, 1337, 31337, 8888, 9999, 5555, 6666, 7777,
            // Common Metasploit/Cobalt Strike defaults
            443,  // HTTPS C2 (only suspicious from certain processes)
            8080, // HTTP C2
            53,   // DNS tunneling
        ]
        .into_iter()
        .collect();

        let suspicious_processes: HashSet<String> = [
            "sh", "bash", "dash", "zsh", "ash", "python", "python3", "python2", "perl", "ruby",
            "node", "nc", "ncat", "netcat", "socat", "curl",
            "wget", // suspicious when connecting to C2 ports
            "php", "lua",
        ]
        .into_iter()
        .map(String::from)
        .collect();

        Self {
            window: Duration::seconds(window_seconds as i64),
            connections: HashMap::new(),
            process_destinations: HashMap::new(),
            alerted: HashMap::new(),
            host: host.into(),
            c2_ports,
            suspicious_processes,
        }
    }

    pub fn process(&mut self, event: &Event) -> Option<Incident> {
        if event.kind != "network.outbound_connect" {
            return None;
        }

        let dst_ip = event.details["dst_ip"].as_str()?.to_string();
        let dst_port = event.details["dst_port"].as_u64()? as u16;
        let comm = event.details["comm"]
            .as_str()
            .unwrap_or("unknown")
            .to_string();
        let pid = event.details["pid"].as_u64().unwrap_or(0) as u32;
        let uid = event
            .details
            .get("uid")
            .and_then(|v| v.as_u64())
            .unwrap_or(u64::MAX);

        // Skip InnerWarden and trusted system processes with legitimate outbound connections
        if super::allowlists::is_innerwarden_process(uid, &comm)
            || super::allowlists::comm_in_allowlist(&comm, super::allowlists::C2_OUTBOUND_ALLOWED)
        {
            return None;
        }

        if super::is_internal_ip(&dst_ip) {
            return None;
        }

        // Skip cloud metadata service (169.254.169.254) and port 0 (DNS artifacts).
        // Also skip byte-swapped variant 254.169.254.169 (eBPF endianness on some kernels).
        if dst_ip == "169.254.169.254" || dst_ip == "254.169.254.169" || dst_port == 0 {
            return None;
        }

        // Skip verified infra processes from ALL C2 checks (beaconing, exfil, ports).
        // Monitoring tools (gomon, prometheus, etc.) legitimately make HTTPS calls
        // to APIs on port 443 at regular intervals, which triggers beaconing detection.
        // Verifies binary path via /proc/PID/exe to prevent evasion by name spoofing.
        let comm_base = comm.split('/').next_back().unwrap_or(&comm).to_string();
        if super::is_verified_infra_process(&comm_base, pid, C2_ALLOWED_COMMS) {
            return None;
        }

        let now = event.ts;
        let cutoff = now - self.window;

        // Record connection
        let record = ConnectionRecord {
            ts: now,
            dst_ip: dst_ip.clone(),
            dst_port,
            comm: comm.clone(),
            pid,
        };

        let entries = self.connections.entry(dst_ip.clone()).or_default();
        while entries.front().is_some_and(|r| r.ts < cutoff) {
            entries.pop_front();
        }
        entries.push_back(record);

        // Track destinations per process
        let proc_key = format!("{}:{}", comm, pid);
        self.process_destinations
            .entry(proc_key.clone())
            .or_default()
            .insert(dst_ip.clone());

        // Suppress re-alerts within window
        let alert_key = format!("{}:{}", dst_ip, comm);
        if let Some(&last) = self.alerted.get(&alert_key) {
            if now - last < self.window {
                return None;
            }
        }

        // ── Check 1: C2 port from suspicious process ────────────────────
        if self.c2_ports.contains(&dst_port) && self.suspicious_processes.contains(&comm_base) {
            // Port 443/8080 only suspicious from shell/scripting processes
            if (dst_port == 443 || dst_port == 8080)
                && !["sh", "bash", "dash", "nc", "ncat", "netcat", "socat"]
                    .contains(&comm_base.as_str())
            {
                // curl/wget to 443 is normal - skip
            } else {
                self.alerted.insert(alert_key, now);
                return Some(self.build_incident(IncidentParams {
                    dst_ip: &dst_ip,
                    dst_port,
                    comm: &comm,
                    pid,
                    ts: now,
                    pattern: "c2_port",
                    severity: Severity::High,
                    summary: format!(
                        "Process {comm} (pid={pid}) connected to {dst_ip}:{dst_port} - known C2 port"
                    ),
                }));
            }
        }

        // ── Check 2: Beaconing (3+ connections to same IP in window) ────
        let conn_count = entries.len();
        if conn_count >= 3 {
            // Check if connections are at regular intervals (beaconing)
            let timestamps: Vec<i64> = entries.iter().map(|r| r.ts.timestamp()).collect();
            if is_beaconing(&timestamps) {
                self.alerted.insert(alert_key, now);
                return Some(self.build_incident(IncidentParams {
                    dst_ip: &dst_ip,
                    dst_port,
                    comm: &comm,
                    pid,
                    ts: now,
                    pattern: "beaconing",
                    severity: Severity::Critical,
                    summary: format!(
                        "Beaconing detected: {comm} connected to {dst_ip} {} times at regular intervals",
                        conn_count
                    ),
                }));
            }
        }

        // ── Check 3: Data exfil (process connecting to 10+ different IPs) ─
        let unique_dests = self
            .process_destinations
            .get(&proc_key)
            .map(|s| s.len())
            .unwrap_or(0);
        if unique_dests >= 10 && self.suspicious_processes.contains(&comm_base) {
            self.alerted.insert(alert_key, now);
            return Some(self.build_incident(IncidentParams {
                dst_ip: &dst_ip,
                dst_port,
                comm: &comm,
                pid,
                ts: now,
                pattern: "data_exfil",
                severity: Severity::Critical,
                summary: format!(
                    "Possible data exfiltration: {comm} connected to {unique_dests} unique IPs in {} seconds",
                    self.window.num_seconds()
                ),
            }));
        }

        // ── Check 5: DNS-over-HTTPS evasion ─────────────────────────────
        // Non-browser process connecting to known DoH resolver on port 443.
        // Attackers use DoH to hide C2 DNS queries from network monitoring.
        if dst_port == 443
            && DOH_RESOLVER_IPS.contains(&dst_ip.as_str())
            && !DOH_ALLOWED_COMMS.iter().any(|c| comm_base.starts_with(c))
        {
            self.alerted.insert(alert_key, now);
            return Some(self.build_incident(IncidentParams {
                dst_ip: &dst_ip,
                dst_port,
                comm: &comm,
                pid,
                ts: now,
                pattern: "doh_evasion",
                severity: Severity::Medium,
                summary: format!(
                    "DNS-over-HTTPS evasion: {comm} (pid={pid}) connected to DoH resolver \
                     {dst_ip}:443. Non-browser processes using DoH may be hiding C2 DNS queries \
                     from network monitoring."
                ),
            }));
        }

        // Prune stale data
        if self.connections.len() > 5000 {
            self.connections.retain(|_, v| {
                v.retain(|r| r.ts > cutoff);
                !v.is_empty()
            });
        }
        if self.process_destinations.len() > 1000 {
            self.process_destinations.clear();
        }
        if self.alerted.len() > 500 {
            self.alerted.retain(|_, ts| *ts > cutoff);
        }

        None
    }

    fn build_incident(&self, params: IncidentParams<'_>) -> Incident {
        let IncidentParams {
            dst_ip,
            dst_port,
            comm,
            pid,
            ts,
            pattern,
            severity,
            summary,
        } = params;
        Incident {
            ts,
            host: self.host.clone(),
            incident_id: format!("c2_callback:{dst_ip}:{}", ts.format("%Y-%m-%dT%H:%MZ")),
            severity,
            title: format!("Possible C2 callback to {dst_ip}:{dst_port}"),
            summary,
            evidence: serde_json::json!([{
                "kind": "c2_callback",
                "pattern": pattern,
                "dst_ip": dst_ip,
                "dst_port": dst_port,
                "comm": comm,
                "pid": pid,
                "window_seconds": self.window.num_seconds(),
            }]),
            recommended_checks: vec![
                format!("Investigate process {comm} (pid={pid}) - what triggered this connection?"),
                format!("Check if {dst_ip} is a known C2 server (VirusTotal, AbuseIPDB)"),
                "Review process tree: who spawned this process?".to_string(),
                "Consider killing the process and blocking the IP".to_string(),
            ],
            tags: vec!["ebpf".to_string(), "network".to_string(), "c2".to_string()],
            entities: vec![EntityRef::ip(dst_ip)],
        }
    }
}

/// Check if timestamps show a beaconing pattern (regular intervals).
/// Returns true if the standard deviation of intervals is low relative to mean.
fn is_beaconing(timestamps: &[i64]) -> bool {
    if timestamps.len() < 3 {
        return false;
    }

    let mut intervals: Vec<f64> = Vec::new();
    for i in 1..timestamps.len() {
        intervals.push((timestamps[i] - timestamps[i - 1]) as f64);
    }

    if intervals.is_empty() {
        return false;
    }

    let mean = intervals.iter().sum::<f64>() / intervals.len() as f64;
    if mean < 1.0 {
        return false; // intervals too short to be meaningful
    }

    let variance =
        intervals.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / intervals.len() as f64;
    let std_dev = variance.sqrt();

    // Beaconing: low coefficient of variation (std_dev / mean < 0.3)
    // Means intervals are very regular (e.g., every 30s ± 9s)
    let cv = std_dev / mean;
    cv < 0.3
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn connect_event(comm: &str, dst_ip: &str, dst_port: u16, ts: DateTime<Utc>) -> Event {
        Event {
            ts,
            host: "test".to_string(),
            source: "ebpf".to_string(),
            kind: "network.outbound_connect".to_string(),
            severity: Severity::Info,
            summary: format!("{comm} connecting to {dst_ip}:{dst_port}"),
            details: serde_json::json!({
                "pid": 1234,
                "uid": 0,
                "comm": comm,
                "dst_ip": dst_ip,
                "dst_port": dst_port,
            }),
            tags: vec![],
            entities: vec![EntityRef::ip(dst_ip)],
        }
    }

    #[test]
    fn detects_c2_port_from_shell() {
        let mut det = C2CallbackDetector::new("test", 300);
        let now = Utc::now();

        let inc = det.process(&connect_event("bash", "1.2.3.4", 4444, now));
        assert!(inc.is_some());
        let inc = inc.unwrap();
        assert_eq!(inc.severity, Severity::High);
        assert!(inc.title.contains("C2 callback"));
    }

    #[test]
    fn ignores_normal_https() {
        let mut det = C2CallbackDetector::new("test", 300);
        let now = Utc::now();

        // curl to 443 is normal
        assert!(det
            .process(&connect_event("curl", "1.2.3.4", 443, now))
            .is_none());
    }

    #[test]
    fn detects_beaconing() {
        let mut det = C2CallbackDetector::new("test", 600);
        let now = Utc::now();

        // Regular 30-second intervals = beaconing
        for i in 0..4 {
            let result = det.process(&connect_event(
                "malware",
                "5.6.7.8",
                8080,
                now + Duration::seconds(i * 30),
            ));
            if i >= 2 {
                // Should fire after 3+ connections with regular intervals
                if let Some(inc) = result {
                    assert_eq!(inc.severity, Severity::Critical);
                    return;
                }
            }
        }
        // If we got here with 4 regular connections and no alert, that's a problem
        // But beaconing detection needs at least 3 data points
    }

    #[test]
    fn ignores_internal_ips() {
        let mut det = C2CallbackDetector::new("test", 300);
        let now = Utc::now();

        assert!(det
            .process(&connect_event("bash", "192.168.1.1", 4444, now))
            .is_none());
        assert!(det
            .process(&connect_event("bash", "127.0.0.1", 4444, now))
            .is_none());
    }

    #[test]
    fn beaconing_detection_math() {
        // Perfect beaconing: every 30 seconds
        assert!(is_beaconing(&[0, 30, 60, 90, 120]));

        // Not beaconing: random intervals
        assert!(!is_beaconing(&[0, 5, 100, 103, 500]));

        // Too few points
        assert!(!is_beaconing(&[0, 30]));
    }

    #[test]
    fn suppresses_realert() {
        let mut det = C2CallbackDetector::new("test", 300);
        let now = Utc::now();

        assert!(det
            .process(&connect_event("bash", "1.2.3.4", 4444, now))
            .is_some());
        // Same IP+comm within window = suppressed
        assert!(det
            .process(&connect_event(
                "bash",
                "1.2.3.4",
                4444,
                now + Duration::seconds(10)
            ))
            .is_none());
    }
}
