use std::sync::Arc;
use std::time::Duration;

use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use std::collections::HashMap;
use sysinfo::{Networks, System};
use tauri::async_runtime::{self, JoinHandle};
use tokio::sync::Notify;

#[derive(thiserror::Error, Debug)]
pub enum AgentError {
    #[error("Falha ao obter hostname da máquina")]
    Hostname,
    #[error("Nenhum identificador de hardware disponível (MAC/serial)")]
    MissingIdentifiers,
    #[error("URL de API inválida")]
    InvalidApiUrl,
    #[error("Falha HTTP: {0}")]
    Http(#[from] reqwest::Error),
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineOs {
    pub name: String,
    pub version: Option<String>,
    pub architecture: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineMetrics {
    pub collected_at: DateTime<Utc>,
    pub cpu_logical_cores: usize,
    pub cpu_physical_cores: Option<usize>,
    pub cpu_usage_percent: f32,
    pub load_average_one: Option<f64>,
    pub load_average_five: Option<f64>,
    pub load_average_fifteen: Option<f64>,
    pub memory_total_bytes: u64,
    pub memory_used_bytes: u64,
    pub memory_used_percent: f32,
    pub uptime_seconds: u64,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineInventory {
    pub cpu_brand: Option<String>,
    pub host_identifier: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MachineProfile {
    pub hostname: String,
    pub os: MachineOs,
    pub mac_addresses: Vec<String>,
    pub serial_numbers: Vec<String>,
    pub inventory: MachineInventory,
    pub metrics: MachineMetrics,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct HeartbeatPayload {
    machine_token: String,
    status: Option<String>,
    hostname: Option<String>,
    os: Option<MachineOs>,
    metrics: Option<MachineMetrics>,
    metadata: Option<serde_json::Value>,
}

fn collect_mac_addresses() -> Vec<String> {
    let mut macs = Vec::new();
    let mut networks = Networks::new();
    networks.refresh_list();
    networks.refresh();

    for (_, data) in networks.iter() {
        let bytes = data.mac_address().0;
        if bytes.iter().all(|byte| *byte == 0) {
            continue;
        }

        let formatted = bytes
            .iter()
            .map(|byte| format!("{:02x}", byte))
            .collect::<Vec<_>>()
            .join(":");

        if !macs.contains(&formatted) {
            macs.push(formatted);
        }
    }

    macs
}

#[cfg(target_os = "linux")]
fn collect_serials_platform() -> Vec<String> {
    let mut out = Vec::new();
    for path in [
        "/sys/class/dmi/id/product_uuid",
        "/sys/class/dmi/id/product_serial",
        "/sys/class/dmi/id/board_serial",
        "/etc/machine-id",
    ] {
        if let Ok(raw) = std::fs::read_to_string(path) {
            let s = raw.trim().to_string();
            if !s.is_empty() && !out.contains(&s) {
                out.push(s);
            }
        }
    }
    out
}

#[cfg(any(target_os = "windows", target_os = "macos"))]
fn collect_serials_platform() -> Vec<String> {
    // Fase 1: sem coleta nativa; será implementada via WMI/ioreg na fase 2.
    Vec::new()
}

fn collect_serials() -> Vec<String> {
    collect_serials_platform()
}

fn collect_network_addrs() -> Vec<serde_json::Value> {
    // Mapa name -> mac via sysinfo (mais estável que get_if_addrs para MAC)
    let mut mac_by_name: HashMap<String, String> = HashMap::new();
    let mut networks = Networks::new();
    networks.refresh_list();
    networks.refresh();
    for (name, data) in networks.iter() {
        let bytes = data.mac_address().0;
        if bytes.iter().any(|b| *b != 0) {
            let mac = bytes
                .iter()
                .map(|b| format!("{:02x}", b))
                .collect::<Vec<_>>()
                .join(":");
            mac_by_name.insert(name.to_string(), mac);
        }
    }

    let mut entries = Vec::new();
    if let Ok(ifaces) = get_if_addrs::get_if_addrs() {
        for iface in ifaces {
            let name = iface.name.clone();
            let addr = iface.ip();
            let ip = addr.to_string();
            let mac = mac_by_name.get(&name).cloned();
            entries.push(json!({
                "name": name,
                "mac": mac,
                "ip": ip,
            }));
        }
    }
    entries
}

fn collect_disks(_system: &System) -> Vec<serde_json::Value> {
    let mut out = Vec::new();
    let disks = sysinfo::Disks::new_with_refreshed_list();
    for disk in disks.list() {
        let name = disk.name().to_string_lossy().to_string();
        let mount = disk.mount_point().to_string_lossy().to_string();
        let fs = disk.file_system().to_string_lossy().to_string();
        let total = disk.total_space();
        let avail = disk.available_space();
        out.push(json!({
            "name": if name.is_empty() { mount.clone() } else { name },
            "mountPoint": mount,
            "fs": fs,
            "totalBytes": total,
            "availableBytes": avail,
        }));
    }

    out
}

fn parse_u64(value: &serde_json::Value) -> Option<u64> {
    if let Some(num) = value.as_u64() {
        return Some(num);
    }
    if let Some(num) = value.as_f64() {
        if num.is_finite() && num >= 0.0 {
            return Some(num as u64);
        }
    }
    if let Some(text) = value.as_str() {
        if let Ok(parsed) = text.trim().parse::<u64>() {
            return Some(parsed);
        }
    }
    None
}

fn push_gpu(
    list: &mut Vec<serde_json::Value>,
    name: Option<&str>,
    memory: Option<u64>,
    vendor: Option<&str>,
    driver: Option<&str>,
) {
    if let Some(name) = name {
        if name.trim().is_empty() {
            return;
        }
        let mut obj = serde_json::Map::new();
        obj.insert("name".into(), json!(name.trim()));
        if let Some(memory) = memory {
            obj.insert("memoryBytes".into(), json!(memory));
        }
        if let Some(vendor) = vendor {
            if !vendor.trim().is_empty() {
                obj.insert("vendor".into(), json!(vendor.trim()));
            }
        }
        if let Some(driver) = driver {
            if !driver.trim().is_empty() {
                obj.insert("driver".into(), json!(driver.trim()));
            }
        }
        list.push(serde_json::Value::Object(obj));
    }
}

fn build_inventory_metadata(system: &System) -> serde_json::Value {
    let cpu_brand = system
        .cpus()
        .first()
        .map(|cpu| cpu.brand().to_string())
        .filter(|brand| !brand.trim().is_empty());
    // sysinfo 0.31 já retorna bytes em total_memory/used_memory
    let mem_total_bytes = system.total_memory();
    let network = collect_network_addrs();
    let disks = collect_disks(system);
    let mut inventory = json!({
        "cpu": { "brand": cpu_brand.clone() },
        "memory": { "totalBytes": mem_total_bytes },
        "network": network,
        "disks": disks,
    });

    if let Some(obj) = inventory.as_object_mut() {
        let mut hardware = serde_json::Map::new();
        if let Some(brand) = cpu_brand.clone() {
            if !brand.trim().is_empty() {
                hardware.insert("cpuType".into(), json!(brand.trim()));
            }
        }
        if let Some(physical) = system.physical_core_count() {
            hardware.insert("physicalCores".into(), json!(physical));
        }
        hardware.insert("logicalCores".into(), json!(system.cpus().len()));
        if mem_total_bytes > 0 {
            hardware.insert("memoryBytes".into(), json!(mem_total_bytes));
            hardware.insert("memory".into(), json!(mem_total_bytes));
        }
        if !hardware.is_empty() {
            obj.insert("hardware".into(), serde_json::Value::Object(hardware));
        }
    }

    #[cfg(target_os = "linux")]
    {
        // Softwares instalados (dpkg ou rpm)
        let software = collect_software_linux();
        if let Some(obj) = inventory.as_object_mut() {
            obj.insert("software".into(), software);
        }

        // Serviços ativos (systemd)
        let services = collect_services_linux();
        if let Some(obj) = inventory.as_object_mut() {
            obj.insert("services".into(), services);
        }

        // Informações estendidas (lsblk/lspci/lsusb/smartctl)
        let extended = collect_linux_extended();
        if let Some(obj) = inventory.as_object_mut() {
            obj.insert("extended".into(), extended);
        }
    }

    #[cfg(target_os = "windows")]
    {
        let mut extended = collect_windows_extended();
        // Fallback: se osInfo vier vazio, preenche com dados do sysinfo
        if let Some(win) = extended.get_mut("windows").and_then(|v| v.as_object_mut()) {
            let needs_os_info = match win.get("osInfo") {
                Some(v) => v.as_object().map(|m| m.is_empty()).unwrap_or(true),
                None => true,
            };
            if needs_os_info {
                let mut osmap = serde_json::Map::new();
                if let Some(name) = System::name() { osmap.insert("ProductName".into(), json!(name)); }
                if let Some(ver) = System::os_version() { osmap.insert("Version".into(), json!(ver)); }
                if let Some(build) = System::kernel_version() { osmap.insert("BuildNumber".into(), json!(build)); }
                win.insert("osInfo".into(), serde_json::Value::Object(osmap));
            }
        }
        if let Some(obj) = inventory.as_object_mut() {
            obj.insert("extended".into(), extended);
        }
    }

    #[cfg(target_os = "macos")]
    {
        let extended = collect_macos_extended();
        if let Some(obj) = inventory.as_object_mut() {
            obj.insert("extended".into(), extended);
        }
    }

    // Normalização de software/serviços no topo do inventário
    if let Some(obj) = inventory.as_object_mut() {
        let extended_snapshot = obj
            .get("extended")
            .and_then(|v| v.as_object())
            .cloned();
        // Merge software
        let mut software: Vec<serde_json::Value> = Vec::new();
        if let Some(existing) = obj.get("software").and_then(|v| v.as_array()) {
            software.extend(existing.iter().cloned());
        }
        if let Some(ext) = extended_snapshot.as_ref() {
            // Windows normalize
            if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) {
                if let Some(ws) = win.get("software").and_then(|v| v.as_array()) {
                    for item in ws {
                        let name = item
                            .get("DisplayName")
                            .or_else(|| item.get("name"))
                            .cloned()
                            .unwrap_or(json!(null));
                        let version = item
                            .get("DisplayVersion")
                            .or_else(|| item.get("version"))
                            .cloned()
                            .unwrap_or(json!(null));
                        let publisher = item.get("Publisher").cloned().unwrap_or(json!(null));
                        software
                            .push(json!({ "name": name, "version": version, "source": publisher }));
                    }
                }
            }
            // macOS normalize
            if let Some(macos) = ext.get("macos").and_then(|v| v.as_object()) {
                if let Some(pkgs) = macos.get("packages").and_then(|v| v.as_array()) {
                    for p in pkgs {
                        software.push(json!({ "name": p, "version": null, "source": "pkgutil" }));
                    }
                }
            }
        }
        if !software.is_empty() {
            obj.insert("software".into(), json!(software));
        }

        // Merge services
        let mut services: Vec<serde_json::Value> = Vec::new();
        if let Some(existing) = obj.get("services").and_then(|v| v.as_array()) {
            services.extend(existing.iter().cloned());
        }
        if let Some(ext) = extended_snapshot.as_ref() {
            if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) {
                if let Some(wsvc) = win.get("services").and_then(|v| v.as_array()) {
                    for s in wsvc {
                        let name = s.get("Name").cloned().unwrap_or(json!(null));
                        let status = s.get("Status").cloned().unwrap_or(json!(null));
                        let display = s.get("DisplayName").cloned().unwrap_or(json!(null));
                        services.push(
                            json!({ "name": name, "status": status, "displayName": display }),
                        );
                    }
                }
            }
        }
        if !services.is_empty() {
            obj.insert("services".into(), json!(services));
        }

        let mut gpus: Vec<serde_json::Value> = Vec::new();
        if let Some(ext) = extended_snapshot.as_ref() {
            if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) {
                if let Some(video) = win.get("videoControllers").and_then(|v| v.as_array()) {
                    for controller in video {
                        let name = controller.get("Name").and_then(|v| v.as_str());
                        let memory = controller.get("AdapterRAM").and_then(parse_u64);
                        let driver = controller.get("DriverVersion").and_then(|v| v.as_str());
                        push_gpu(&mut gpus, name, memory, None, driver);
                    }
                }
                if obj
                    .get("disks")
                    .and_then(|v| v.as_array())
                    .map(|arr| arr.is_empty())
                    .unwrap_or(true)
                {
                    if let Some(raw) = win.get("disks").and_then(|v| v.as_array()) {
                        let mapped = raw
                            .iter()
                            .map(|disk| {
                                let model = disk.get("Model").and_then(|v| v.as_str()).unwrap_or_default();
                                let serial = disk.get("SerialNumber").and_then(|v| v.as_str()).unwrap_or_default();
                                let size = parse_u64(disk.get("Size").unwrap_or(&serde_json::Value::Null)).unwrap_or(0);
                                let interface = disk.get("InterfaceType").and_then(|v| v.as_str()).unwrap_or("");
                                let media = disk.get("MediaType").and_then(|v| v.as_str()).unwrap_or("");
                                json!({
                                    "name": if !model.is_empty() { model } else { serial },
                                    "mountPoint": "",
                                    "fs": if !media.is_empty() { media } else { "—" },
                                    "interface": if !interface.is_empty() { serde_json::Value::String(interface.to_string()) } else { serde_json::Value::Null },
                                    "serial": if !serial.is_empty() { serde_json::Value::String(serial.to_string()) } else { serde_json::Value::Null },
                                    "totalBytes": size,
                                    "availableBytes": serde_json::Value::Null,
                                })
                            })
                            .collect::<Vec<_>>();
                        if !mapped.is_empty() {
                            obj.insert("disks".into(), json!(mapped));
                        }
                    }
                }
            }
            if let Some(linux) = ext.get("linux").and_then(|v| v.as_object()) {
                if let Some(pci) = linux.get("pciList").and_then(|v| v.as_array()) {
                    for entry in pci {
                        if let Some(text) = entry.get("text").and_then(|v| v.as_str()) {
                            let lower = text.to_lowercase();
                            if lower.contains(" vga ")
                                || lower.contains(" 3d controller")
                                || lower.contains("display controller")
                            {
                                push_gpu(&mut gpus, Some(text), None, None, None);
                            }
                        }
                    }
                }
            }
            if let Some(macos) = ext.get("macos").and_then(|v| v.as_object()) {
                if let Some(profiler) = macos.get("systemProfiler").and_then(|v| v.as_object()) {
                    if let Some(displays) = profiler
                        .get("SPDisplaysDataType")
                        .and_then(|v| v.as_array())
                    {
                        for display in displays {
                            if let Some(d) = display.as_object() {
                                let name = d.get("_name").and_then(|v| v.as_str());
                                let vram = d
                                    .get("spdisplays_vram")
                                    .and_then(|v| v.as_str())
                                    .and_then(|s| {
                                        let digits = s.split_whitespace().next().unwrap_or("");
                                        digits.parse::<u64>().ok().map(|n| {
                                            if s.to_lowercase().contains("gb") {
                                                n * 1024 * 1024 * 1024
                                            } else if s.to_lowercase().contains("mb") {
                                                n * 1024 * 1024
                                            } else {
                                                n
                                            }
                                        })
                                    });
                                push_gpu(&mut gpus, name, vram, None, None);
                            }
                        }
                    }
                }
            }
        }

        if !gpus.is_empty() {
            let entry = obj.entry("hardware").or_insert_with(|| json!({}));
            if let Some(hardware) = entry.as_object_mut() {
                hardware.insert("gpus".into(), json!(gpus.clone()));
                if let Some(primary) = gpus.first() {
                    hardware.insert("primaryGpu".into(), primary.clone());
                }
            }
        }
    }

    json!({ "inventory": inventory })
}

pub fn collect_inventory_plain() -> serde_json::Value {
    let system = collect_system();
    let meta = build_inventory_metadata(&system);
    match meta.get("inventory") {
        Some(value) => value.clone(),
        None => json!({}),
    }
}

#[cfg(target_os = "linux")]
fn collect_software_linux() -> serde_json::Value {
    use std::process::Command;
    // Tenta dpkg-query primeiro
    let dpkg = Command::new("sh")
        .arg("-lc")
        .arg("dpkg-query -W -f='${binary:Package}\t${Version}\n' 2>/dev/null || true")
        .output();
    if let Ok(out) = dpkg {
        if out.status.success() {
            let s = String::from_utf8_lossy(&out.stdout);
            let mut items = Vec::new();
            for line in s.lines() {
                let mut parts = line.split('\t');
                let name = parts.next().unwrap_or("").trim();
                let version = parts.next().unwrap_or("").trim();
                if !name.is_empty() {
                    items.push(json!({"name": name, "version": version, "source": "dpkg"}));
                }
            }
            return json!(items);
        }
    }

    // Fallback rpm
    let rpm = std::process::Command::new("sh")
        .arg("-lc")
        .arg("rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\n' 2>/dev/null || true")
        .output();
    if let Ok(out) = rpm {
        if out.status.success() {
            let s = String::from_utf8_lossy(&out.stdout);
            let mut items = Vec::new();
            for line in s.lines() {
                let mut parts = line.split('\t');
                let name = parts.next().unwrap_or("").trim();
                let version = parts.next().unwrap_or("").trim();
                if !name.is_empty() {
                    items.push(json!({"name": name, "version": version, "source": "rpm"}));
                }
            }
            return json!(items);
        }
    }
    json!([])
}

#[cfg(target_os = "linux")]
fn collect_services_linux() -> serde_json::Value {
    use std::process::Command;
    let out = Command::new("sh")
        .arg("-lc")
        .arg("systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null || true")
        .output();
    if let Ok(out) = out {
        if out.status.success() {
            let s = String::from_utf8_lossy(&out.stdout);
            let mut items = Vec::new();
            for line in s.lines() {
                // Typical format: UNIT LOAD ACTIVE SUB DESCRIPTION
                // We take UNIT and ACTIVE
                let cols: Vec<&str> = line.split_whitespace().collect();
                if cols.is_empty() {
                    continue;
                }
                let unit = cols.get(0).unwrap_or(&"");
                let active = cols.get(2).copied().unwrap_or("");
                if !unit.is_empty() {
                    items.push(json!({"name": unit, "status": active}));
                }
            }
            return json!(items);
        }
    }
    json!([])
}

#[cfg(target_os = "linux")]
fn collect_linux_extended() -> serde_json::Value {
    use std::process::Command;
    // lsblk em JSON (block devices)
    let block_json = Command::new("sh")
        .arg("-lc")
        .arg("lsblk -J -b 2>/dev/null || true")
        .output()
        .ok()
        .and_then(|out| {
            if out.status.success() {
                Some(out.stdout)
            } else {
                Some(out.stdout)
            }
        })
        .and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).ok())
        .unwrap_or_else(|| json!({}));

    // lspci e lsusb — texto livre (depende de pacotes pciutils/usbutils)
    let lspci = Command::new("sh")
        .arg("-lc")
        .arg("lspci 2>/dev/null || true")
        .output()
        .ok()
        .map(|out| String::from_utf8_lossy(&out.stdout).to_string())
        .unwrap_or_default();
    let lsusb = Command::new("sh")
        .arg("-lc")
        .arg("lsusb 2>/dev/null || true")
        .output()
        .ok()
        .map(|out| String::from_utf8_lossy(&out.stdout).to_string())
        .unwrap_or_default();
    // Parse básico de lspci/lsusb em listas
    fn parse_lines_to_list(input: &str) -> Vec<serde_json::Value> {
        input
            .lines()
            .map(|l| l.trim())
            .filter(|l| !l.is_empty())
            .map(|l| json!({ "text": l }))
            .collect::<Vec<_>>()
    }
    let pci_list = parse_lines_to_list(&lspci);
    let usb_list = parse_lines_to_list(&lsusb);

    // smartctl (se disponível) por disco
    let mut smart: Vec<serde_json::Value> = Vec::new();
    if let Ok(out) = std::process::Command::new("sh")
        .arg("-lc")
        .arg("command -v smartctl >/dev/null 2>&1 && echo yes || echo no")
        .output()
    {
        if out.status.success() && String::from_utf8_lossy(&out.stdout).contains("yes") {
            if let Some(devices) = block_json.get("blockdevices").and_then(|v| v.as_array()) {
                for dev in devices {
                    let t = dev.get("type").and_then(|v| v.as_str()).unwrap_or("");
                    let name = dev.get("name").and_then(|v| v.as_str()).unwrap_or("");
                    if t == "disk" && !name.is_empty() {
                        let path = format!("/dev/{}", name);
                        if let Ok(out) = Command::new("sh")
                            .arg("-lc")
                            .arg(format!("smartctl -H -j {} 2>/dev/null || true", path))
                            .output()
                        {
                            if out.status.success() || !out.stdout.is_empty() {
                                if let Ok(val) =
                                    serde_json::from_slice::<serde_json::Value>(&out.stdout)
                                {
                                    smart.push(val);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    json!({
        "linux": {
            "lsblk": block_json,
            "lspci": lspci,
            "lsusb": lsusb,
            "pciList": pci_list,
            "usbList": usb_list,
            "smart": smart,
        }
    })
}

#[cfg(target_os = "windows")]
fn collect_windows_extended() -> serde_json::Value {
    use std::os::windows::process::CommandExt;
    use std::process::Command;
    const CREATE_NO_WINDOW: u32 = 0x08000000;
    fn ps(cmd: &str) -> Option<serde_json::Value> {
        let ps_cmd = format!(
            "$ErrorActionPreference='SilentlyContinue'; {} | ConvertTo-Json -Depth 4 -Compress",
            cmd
        );
        let out = Command::new("powershell")
            .creation_flags(CREATE_NO_WINDOW)
            .arg("-NoProfile")
            .arg("-WindowStyle")
            .arg("Hidden")
            .arg("-NoLogo")
            .arg("-Command")
            .arg(ps_cmd)
            .output()
            .ok()?;
        if out.stdout.is_empty() {
            return None;
        }
        serde_json::from_slice::<serde_json::Value>(&out.stdout).ok()
    }

    let software = ps(r#"@(Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'; Get-ItemProperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion, Publisher"#)
        .unwrap_or_else(|| json!([]));
    let services =
        ps("@(Get-Service | Select-Object Name,Status,DisplayName)").unwrap_or_else(|| json!([]));
    let defender = ps("Get-MpComputerStatus | Select-Object AMRunningMode,AntivirusEnabled,RealTimeProtectionEnabled,AntispywareEnabled").unwrap_or_else(|| json!({}));
    let hotfix = ps("Get-HotFix | Select-Object HotFixID,InstalledOn").unwrap_or_else(|| json!([]));

    // Informações de build/edição e ativação
    let os_info = ps(r#"
      $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion';
      $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue;
      $lsItems = Get-CimInstance -Query "SELECT Name, LicenseStatus, PartialProductKey FROM SoftwareLicensingProduct WHERE PartialProductKey IS NOT NULL" | Where-Object { $_.Name -like 'Windows*' };
      $activatedItem = $lsItems | Where-Object { $_.LicenseStatus -eq 1 } | Select-Object -First 1;
      $primaryItem = if ($activatedItem) { $activatedItem } else { $lsItems | Select-Object -First 1 };
      $lsCode = if ($primaryItem -and $primaryItem.LicenseStatus -ne $null) { [int]$primaryItem.LicenseStatus } else { 0 };
      [PSCustomObject]@{
        ProductName            = $cv.ProductName
        CurrentBuild           = $cv.CurrentBuild
        CurrentBuildNumber     = $cv.CurrentBuildNumber
        DisplayVersion         = $cv.DisplayVersion
        ReleaseId              = $cv.ReleaseId
        EditionID              = $cv.EditionID
        UBR                    = $cv.UBR
        CompositionEditionID   = $cv.CompositionEditionID
        InstallationType       = $cv.InstallationType
        InstallDate            = $cv.InstallDate
        InstallationDate       = $os.InstallDate
        InstalledOn            = $os.InstallDate
        Version                = $os.Version
        BuildNumber            = $os.BuildNumber
        Caption                = $os.Caption
        FeatureExperiencePack  = $cv.FeatureExperiencePack
        LicenseStatus          = $lsCode
        IsActivated            = ($activatedItem -ne $null)
      }
    "#).unwrap_or_else(|| json!({}));

    // Hardware detalhado (CPU/Board/BIOS/Memória/Vídeo/Discos)
    let cpu = ps("Get-CimInstance Win32_Processor | Select-Object Name,Manufacturer,SocketDesignation,NumberOfCores,NumberOfLogicalProcessors,L2CacheSize,L3CacheSize,MaxClockSpeed").unwrap_or_else(|| json!({}));
    let baseboard = ps(
        "Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version",
    )
    .unwrap_or_else(|| json!({}));
    let bios = ps("Get-CimInstance Win32_BIOS | Select-Object Manufacturer,SMBIOSBIOSVersion,ReleaseDate,Version").unwrap_or_else(|| json!({}));
    let memory = ps("@(Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,Capacity,Manufacturer,PartNumber,SerialNumber,ConfiguredClockSpeed,Speed,ConfiguredVoltage)").unwrap_or_else(|| json!([]));
    let video = ps("@(Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID)").unwrap_or_else(|| json!([]));
    let disks = ps("@(Get-CimInstance Win32_DiskDrive | Select-Object Model,SerialNumber,Size,InterfaceType,MediaType)").unwrap_or_else(|| json!([]));

    json!({
        "windows": {
            "software": software,
            "services": services,
            "defender": defender,
            "hotfix": hotfix,
            "osInfo": os_info,
            "cpu": cpu,
            "baseboard": baseboard,
            "bios": bios,
            "memoryModules": memory,
            "videoControllers": video,
            "disks": disks,
        }
    })
}

#[cfg(target_os = "macos")]
fn collect_macos_extended() -> serde_json::Value {
    use std::process::Command;
    // system_profiler em JSON (pode ser pesado; limitar a alguns tipos)
    let profiler = Command::new("sh")
        .arg("-lc")
        .arg("system_profiler -json SPHardwareDataType SPSoftwareDataType SPNetworkDataType SPStorageDataType SPDisplaysDataType 2>/dev/null || true")
        .output()
        .ok()
        .and_then(|out| serde_json::from_slice::<serde_json::Value>(&out.stdout).ok())
        .unwrap_or_else(|| json!({}));
    let pkgs = Command::new("sh")
        .arg("-lc")
        .arg("pkgutil --pkgs 2>/dev/null || true")
        .output()
        .ok()
        .map(|out| {
            String::from_utf8_lossy(&out.stdout)
                .lines()
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();
    let services_text = Command::new("sh")
        .arg("-lc")
        .arg("launchctl list 2>/dev/null || true")
        .output()
        .ok()
        .map(|out| String::from_utf8_lossy(&out.stdout).to_string())
        .unwrap_or_default();

    json!({
        "macos": {
            "systemProfiler": profiler,
            "packages": pkgs,
            "launchctl": services_text,
        }
    })
}

fn collect_system() -> System {
    let mut system = System::new_all();
    system.refresh_all();
    system
}

fn collect_metrics(system: &System) -> MachineMetrics {
    let collected_at = Utc::now();
    let total_memory = system.total_memory();
    let used_memory = system.used_memory();
    // sysinfo 0.31: valores já em bytes
    let memory_total_bytes = total_memory;
    let memory_used_bytes = used_memory;
    let memory_used_percent = if total_memory > 0 {
        (used_memory as f32 / total_memory as f32) * 100.0
    } else {
        0.0
    };

    let load = System::load_average();
    let cpu_usage_percent = system.global_cpu_usage();
    let cpu_logical_cores = system.cpus().len();
    let cpu_physical_cores = system.physical_core_count();

    MachineMetrics {
        collected_at,
        cpu_logical_cores,
        cpu_physical_cores,
        cpu_usage_percent,
        load_average_one: Some(load.one),
        load_average_five: Some(load.five),
        load_average_fifteen: Some(load.fifteen),
        memory_total_bytes,
        memory_used_bytes,
        memory_used_percent,
        uptime_seconds: System::uptime(),
    }
}

pub fn collect_profile() -> Result<MachineProfile, AgentError> {
    let hostname = hostname::get()
        .map_err(|_| AgentError::Hostname)?
        .to_string_lossy()
        .trim()
        .to_string();

    let system = collect_system();

    let os_name = System::name()
        .or_else(|| System::long_os_version())
        .unwrap_or_else(|| "desconhecido".to_string());
    let os_version = System::os_version();
    let architecture = std::env::consts::ARCH.to_string();

    let mac_addresses = collect_mac_addresses();
    let serials: Vec<String> = collect_serials();

    if mac_addresses.is_empty() && serials.is_empty() {
        return Err(AgentError::MissingIdentifiers);
    }

    let metrics = collect_metrics(&system);
    let cpu_brand = system
        .cpus()
        .first()
        .map(|cpu| cpu.brand().to_string())
        .filter(|brand| !brand.trim().is_empty());

    let inventory = MachineInventory {
        cpu_brand,
        host_identifier: serials.first().cloned(),
    };

    Ok(MachineProfile {
        hostname,
        os: MachineOs {
            name: os_name,
            version: os_version,
            architecture: Some(architecture),
        },
        mac_addresses,
        serial_numbers: serials,
        inventory,
        metrics,
    })
}

static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
    reqwest::Client::builder()
        .user_agent("sistema-de-chamados-agent/1.0")
        .timeout(Duration::from_secs(20))
        .use_rustls_tls()
        .build()
        .expect("failed to build http client")
});

async fn post_heartbeat(
    base_url: &str,
    token: &str,
    status: Option<String>,
) -> Result<(), AgentError> {
    let system = collect_system();
    let metrics = collect_metrics(&system);
    let hostname = hostname::get()
        .map_err(|_| AgentError::Hostname)?
        .to_string_lossy()
        .into_owned();
    let os = MachineOs {
        name: System::name()
            .or_else(|| System::long_os_version())
            .unwrap_or_else(|| "desconhecido".to_string()),
        version: System::os_version(),
        architecture: Some(std::env::consts::ARCH.to_string()),
    };

    let payload = HeartbeatPayload {
        machine_token: token.to_string(),
        status,
        hostname: Some(hostname),
        os: Some(os),
        metrics: Some(metrics),
        metadata: Some(build_inventory_metadata(&system)),
    };

    let url = format!("{}/api/machines/heartbeat", base_url);
    HTTP_CLIENT.post(url).json(&payload).send().await?;
    Ok(())
}

struct HeartbeatHandle {
    token: String,
    base_url: String,
    stop_signal: Arc<Notify>,
    join_handle: JoinHandle<()>,
}

impl HeartbeatHandle {
    fn stop(self) {
        self.stop_signal.notify_waiters();
        self.join_handle.abort();
    }
}

#[derive(Default)]
pub struct AgentRuntime {
    inner: Mutex<Option<HeartbeatHandle>>,
}

fn sanitize_base_url(input: &str) -> Result<String, AgentError> {
    let trimmed = input.trim().trim_end_matches('/');
    if trimmed.is_empty() {
        return Err(AgentError::InvalidApiUrl);
    }
    Ok(trimmed.to_string())
}

impl AgentRuntime {
    pub fn new() -> Self {
        Self {
            inner: Mutex::new(None),
        }
    }

    pub fn start_heartbeat(
        &self,
        base_url: String,
        token: String,
        status: Option<String>,
        interval_seconds: Option<u64>,
    ) -> Result<(), AgentError> {
        let sanitized_base = sanitize_base_url(&base_url)?;
        let interval = interval_seconds.unwrap_or(300).max(60);

        {
            let mut guard = self.inner.lock();
            if let Some(handle) = guard.take() {
                if handle.token == token && handle.base_url == sanitized_base {
                    // Reuse existing heartbeat; keep running.
                    *guard = Some(handle);
                    return Ok(());
                }
                handle.stop();
            }
        }

        let stop_signal = Arc::new(Notify::new());
        let stop_signal_clone = stop_signal.clone();
        let token_clone = token.clone();
        let base_clone = sanitized_base.clone();
        let status_clone = status.clone();

        let join_handle = async_runtime::spawn(async move {
            if let Err(error) =
                post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await
            {
                eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}");
            }

            let mut ticker = tokio::time::interval(Duration::from_secs(interval));
            ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);

            loop {
                // Wait interval
                tokio::select! {
                    _ = stop_signal_clone.notified() => {
                        break;
                    }
                    _ = ticker.tick() => {}
                }

                if let Err(error) =
                    post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await
                {
                    eprintln!("[agent] Falha ao enviar heartbeat: {error}");
                }
            }
        });

        let handle = HeartbeatHandle {
            token,
            base_url: sanitized_base,
            stop_signal,
            join_handle,
        };

        let mut guard = self.inner.lock();
        *guard = Some(handle);

        Ok(())
    }

    pub fn stop(&self) {
        let mut guard = self.inner.lock();
        if let Some(handle) = guard.take() {
            handle.stop();
        }
    }
}
