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 dispositivo")]
    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 base64::engine::general_purpose::STANDARD;
    use base64::Engine as _;
    use std::os::windows::process::CommandExt;
    use std::process::Command;
    const CREATE_NO_WINDOW: u32 = 0x08000000;

    fn decode_powershell_text(bytes: &[u8]) -> Option<String> {
        if bytes.is_empty() {
            return None;
        }
        if bytes.starts_with(&[0xFF, 0xFE]) {
            return decode_utf16_le_to_string(&bytes[2..]);
        }
        if bytes.len() >= 2 && bytes[1] == 0 {
            if let Some(s) = decode_utf16_le_to_string(bytes) {
                return Some(s);
            }
        }
        if bytes.contains(&0) {
            if let Some(s) = decode_utf16_le_to_string(bytes) {
                return Some(s);
            }
        }
        let text = std::str::from_utf8(bytes).ok()?.trim().to_string();
        if text.is_empty() {
            None
        } else {
            Some(text)
        }
    }

    fn decode_utf16_le_to_string(bytes: &[u8]) -> Option<String> {
        if !bytes.len().is_multiple_of(2) {
            return None;
        }
        let utf16: Vec<u16> = bytes
            .chunks_exact(2)
            .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
            .collect();
        let text = String::from_utf16(&utf16).ok()?;
        let trimmed = text.trim();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed.to_string())
        }
    }

    fn preview_base64(bytes: &[u8], max_len: usize) -> String {
        if bytes.is_empty() {
            return "<empty>".to_string();
        }
        let prefix = if bytes.len() > max_len {
            &bytes[..max_len]
        } else {
            bytes
        };
        format!("base64:{}...", STANDARD.encode(prefix))
    }

    fn encode_ps_script(script: &str) -> String {
        let mut bytes = Vec::with_capacity(script.len() * 2);
        for unit in script.encode_utf16() {
            bytes.extend_from_slice(&unit.to_le_bytes());
        }
        STANDARD.encode(bytes)
    }

    fn ps(cmd: &str) -> Option<serde_json::Value> {
        let script = format!(
            "$ErrorActionPreference='SilentlyContinue';$ProgressPreference='SilentlyContinue';$result = & {{\n{}\n}};if ($null -eq $result) {{ return }};$json = $result | ConvertTo-Json -Depth 4 -Compress;if ([string]::IsNullOrWhiteSpace($json)) {{ return }};[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$json;",
            cmd
        );
        let encoded = encode_ps_script(&script);
        let out = Command::new("powershell")
            .creation_flags(CREATE_NO_WINDOW)
            .arg("-NoProfile")
            .arg("-NoLogo")
            .arg("-NonInteractive")
            .arg("-ExecutionPolicy")
            .arg("Bypass")
            .arg("-EncodedCommand")
            .arg(encoded)
            .output()
            .ok()?;
        let stdout_text = decode_powershell_text(&out.stdout);
        if cfg!(test) {
            if let Some(ref txt) = stdout_text {
                let preview = txt.chars().take(512).collect::<String>();
                eprintln!("[collect_windows_extended] stdout `{cmd}` => {preview}");
            } else {
                let preview = preview_base64(&out.stdout, 512);
                eprintln!(
                    "[collect_windows_extended] stdout `{cmd}` => <não decodificado {preview}>"
                );
            }
            if !out.stderr.is_empty() {
                if let Some(err) = decode_powershell_text(&out.stderr) {
                    eprintln!("[collect_windows_extended] stderr `{cmd}` => {err}");
                } else {
                    let preview = preview_base64(&out.stderr, 512);
                    eprintln!(
                        "[collect_windows_extended] stderr `{cmd}` => <não decodificado {preview}>"
                    );
                }
            }
        }
        stdout_text.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).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!([]));
    let bitlocker = ps(
        "@(if (Get-Command -Name Get-BitLockerVolume -ErrorAction SilentlyContinue) { Get-BitLockerVolume | Select-Object MountPoint,VolumeStatus,ProtectionStatus,LockStatus,EncryptionMethod,EncryptionPercentage,CapacityGB,KeyProtector } else { @() })",
    )
    .unwrap_or_else(|| json!([]));
    let tpm = ps(
        "if (Get-Command -Name Get-Tpm -ErrorAction SilentlyContinue) { Get-Tpm | Select-Object TpmPresent,TpmReady,TpmEnabled,TpmActivated,ManagedAuthLevel,OwnerAuth,ManufacturerId,ManufacturerIdTxt,ManufacturerVersion,ManufacturerVersionFull20,SpecVersion } else { $null }",
    )
    .unwrap_or_else(|| json!({}));
    let secure_boot = ps(
        r#"
          if (-not (Get-Command -Name Confirm-SecureBootUEFI -ErrorAction SilentlyContinue)) {
            [PSCustomObject]@{ Supported = $false; Enabled = $null; Error = 'Cmdlet Confirm-SecureBootUEFI indisponível' }
          } else {
            try {
              $enabled = Confirm-SecureBootUEFI
              [PSCustomObject]@{ Supported = $true; Enabled = [bool]$enabled; Error = $null }
            } catch {
              [PSCustomObject]@{ Supported = $true; Enabled = $null; Error = $_.Exception.Message }
            }
          }
        "#,
    )
    .unwrap_or_else(|| json!({}));
    let device_guard = ps(
        "@(Get-CimInstance -ClassName Win32_DeviceGuard | Select-Object SecurityServicesConfigured,SecurityServicesRunning,RequiredSecurityProperties,AvailableSecurityProperties,VirtualizationBasedSecurityStatus)",
    )
    .unwrap_or_else(|| json!([]));
    let firewall_profiles = ps(
        "@(Get-NetFirewallProfile | Select-Object Name,Enabled,DefaultInboundAction,DefaultOutboundAction,NotifyOnListen)",
    )
    .unwrap_or_else(|| json!([]));
    let windows_update = ps(
        r#"
          $reg = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -ErrorAction SilentlyContinue
          if ($null -eq $reg) { return $null }
          $last = $null
          if ($reg.PSObject.Properties.Name -contains 'LastSuccessTime') {
            $raw = $reg.LastSuccessTime
            if ($raw) {
              try {
                if ($raw -is [DateTime]) {
                  $last = ($raw.ToUniversalTime()).ToString('o')
                } elseif ($raw -is [string]) {
                  $last = $raw
                } else {
                  $last = [DateTime]::FromFileTimeUtc([long]$raw).ToString('o')
                }
              } catch {
                $last = $raw
              }
            }
          }
          [PSCustomObject]@{
            AUOptions = $reg.AUOptions
            NoAutoUpdate = $reg.NoAutoUpdate
            ScheduledInstallDay = $reg.ScheduledInstallDay
            ScheduledInstallTime = $reg.ScheduledInstallTime
            DetectionFrequency = $reg.DetectionFrequencyEnabled
            LastSuccessTime = $last
          }
        "#,
    )
    .unwrap_or_else(|| json!({}));
    let computer_system = ps(
        "Get-CimInstance Win32_ComputerSystem | Select-Object Manufacturer,Model,Domain,DomainRole,PartOfDomain,Workgroup,TotalPhysicalMemory,HypervisorPresent,PCSystemType,PCSystemTypeEx",
    )
    .unwrap_or_else(|| json!({}));
    let device_join = ps(
        r#"
          $output = & dsregcmd.exe /status 2>$null
          if (-not $output) { return $null }
          $map = [ordered]@{}
          $current = $null
          foreach ($line in $output) {
            if ([string]::IsNullOrWhiteSpace($line)) { continue }
            if ($line -match '^\[(.+)\]$') {
              $current = $matches[1].Trim()
              if (-not $map.Contains($current)) {
                $map[$current] = [ordered]@{}
              }
              continue
            }
            if (-not $current) { continue }
            $parts = $line.Split(':', 2)
            if ($parts.Length -ne 2) { continue }
            $key = $parts[0].Trim()
            $value = $parts[1].Trim()
            if ($key) {
              ($map[$current])[$key] = $value
            }
          }
          if ($map.Count -eq 0) { return $null }
          $obj = [ordered]@{}
          foreach ($entry in $map.GetEnumerator()) {
            $obj[$entry.Key] = [PSCustomObject]$entry.Value
          }
          [PSCustomObject]$obj
        "#,
    )
    .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!([]));
    // Coleta de GPU com VRAM correta (nvidia-smi para NVIDIA, registro como fallback para >4GB)
    let video = ps(r#"
        $gpus = @()
        $wmiGpus = Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID
        foreach ($gpu in $wmiGpus) {
            $vram = $gpu.AdapterRAM
            # Tenta nvidia-smi para GPUs NVIDIA (retorna valor correto para >4GB)
            if ($gpu.Name -match 'NVIDIA') {
                try {
                    $nvidiaSmi = & 'nvidia-smi' '--query-gpu=memory.total' '--format=csv,noheader,nounits' 2>$null
                    if ($nvidiaSmi) {
                        $vramMB = [int64]($nvidiaSmi.Trim())
                        $vram = $vramMB * 1024 * 1024
                    }
                } catch {}
            }
            # Fallback: tenta registro do Windows (qwMemorySize é uint64)
            if ($vram -le 4294967296 -and $vram -gt 0) {
                try {
                    $regPath = 'HKLM:\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0*'
                    $regGpus = Get-ItemProperty $regPath -ErrorAction SilentlyContinue
                    foreach ($reg in $regGpus) {
                        if ($reg.DriverDesc -eq $gpu.Name -and $reg.'HardwareInformation.qwMemorySize') {
                            $vram = [int64]$reg.'HardwareInformation.qwMemorySize'
                            break
                        }
                    }
                } catch {}
            }
            $gpus += [PSCustomObject]@{
                Name = $gpu.Name
                AdapterRAM = $vram
                DriverVersion = $gpu.DriverVersion
                PNPDeviceID = $gpu.PNPDeviceID
            }
        }
        @($gpus)
    "#).unwrap_or_else(|| json!([]));
    let disks = ps("@(Get-CimInstance Win32_DiskDrive | Select-Object Model,SerialNumber,Size,InterfaceType,MediaType)").unwrap_or_else(|| json!([]));

    // Bateria (notebooks/laptops)
    let battery = ps(r#"
        $batteries = @(Get-CimInstance Win32_Battery | Select-Object Name,DeviceID,Status,BatteryStatus,EstimatedChargeRemaining,EstimatedRunTime,DesignCapacity,FullChargeCapacity,DesignVoltage,Chemistry,BatteryRechargeTime)
        if ($batteries.Count -eq 0) {
            [PSCustomObject]@{ Present = $false; Batteries = @() }
        } else {
            # Mapeia status numérico para texto
            $statusMap = @{
                1 = 'Discharging'
                2 = 'AC Power'
                3 = 'Fully Charged'
                4 = 'Low'
                5 = 'Critical'
                6 = 'Charging'
                7 = 'Charging High'
                8 = 'Charging Low'
                9 = 'Charging Critical'
                10 = 'Undefined'
                11 = 'Partially Charged'
            }
            foreach ($b in $batteries) {
                if ($b.BatteryStatus) {
                    $b | Add-Member -NotePropertyName 'BatteryStatusText' -NotePropertyValue ($statusMap[[int]$b.BatteryStatus] ?? 'Unknown') -Force
                }
            }
            [PSCustomObject]@{ Present = $true; Batteries = $batteries }
        }
    "#).unwrap_or_else(|| json!({ "Present": false, "Batteries": [] }));

    // Sensores térmicos (temperatura CPU/GPU quando disponível)
    let thermal = ps(r#"
        $temps = @()
        # Tenta WMI thermal zone (requer admin em alguns sistemas)
        try {
            $zones = Get-CimInstance -Namespace 'root/WMI' -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue
            foreach ($z in $zones) {
                if ($z.CurrentTemperature) {
                    $celsius = [math]::Round(($z.CurrentTemperature - 2732) / 10, 1)
                    $temps += [PSCustomObject]@{
                        Source = 'ThermalZone'
                        Name = $z.InstanceName
                        TemperatureCelsius = $celsius
                        CriticalTripPoint = if ($z.CriticalTripPoint) { [math]::Round(($z.CriticalTripPoint - 2732) / 10, 1) } else { $null }
                    }
                }
            }
        } catch {}
        # CPU temp via Open Hardware Monitor WMI (se instalado)
        try {
            $ohm = Get-CimInstance -Namespace 'root/OpenHardwareMonitor' -ClassName Sensor -ErrorAction SilentlyContinue | Where-Object { $_.SensorType -eq 'Temperature' }
            foreach ($s in $ohm) {
                $temps += [PSCustomObject]@{
                    Source = 'OpenHardwareMonitor'
                    Name = $s.Name
                    TemperatureCelsius = $s.Value
                    Parent = $s.Parent
                }
            }
        } catch {}
        @($temps)
    "#).unwrap_or_else(|| json!([]));

    // Adaptadores de rede (físicos e virtuais)
    let network_adapters = ps(r#"
        @(Get-CimInstance Win32_NetworkAdapter | Where-Object { $_.PhysicalAdapter -eq $true -or $_.NetConnectionStatus -ne $null } | Select-Object Name,Description,MACAddress,Speed,NetConnectionStatus,AdapterType,Manufacturer,NetConnectionID,PNPDeviceID | ForEach-Object {
            $statusMap = @{
                0 = 'Disconnected'
                1 = 'Connecting'
                2 = 'Connected'
                3 = 'Disconnecting'
                4 = 'Hardware not present'
                5 = 'Hardware disabled'
                6 = 'Hardware malfunction'
                7 = 'Media disconnected'
                8 = 'Authenticating'
                9 = 'Authentication succeeded'
                10 = 'Authentication failed'
                11 = 'Invalid address'
                12 = 'Credentials required'
            }
            $_ | Add-Member -NotePropertyName 'StatusText' -NotePropertyValue ($statusMap[[int]$_.NetConnectionStatus] ?? 'Unknown') -Force
            $_
        })
    "#).unwrap_or_else(|| json!([]));

    // Monitores conectados
    let monitors = ps(r#"
        @(Get-CimInstance WmiMonitorID -Namespace root/wmi -ErrorAction SilentlyContinue | ForEach-Object {
            $decode = { param($arr) if ($arr) { -join ($arr | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ }) } else { $null } }
            [PSCustomObject]@{
                ManufacturerName = & $decode $_.ManufacturerName
                ProductCodeID = & $decode $_.ProductCodeID
                SerialNumberID = & $decode $_.SerialNumberID
                UserFriendlyName = & $decode $_.UserFriendlyName
                YearOfManufacture = $_.YearOfManufacture
                WeekOfManufacture = $_.WeekOfManufacture
            }
        })
    "#).unwrap_or_else(|| json!([]));

    // Fonte de alimentação / chassis
    let power_supply = ps(r#"
        $chassis = Get-CimInstance Win32_SystemEnclosure | Select-Object ChassisTypes,Manufacturer,SerialNumber,SMBIOSAssetTag
        $chassisTypeMap = @{
            1 = 'Other'; 2 = 'Unknown'; 3 = 'Desktop'; 4 = 'Low Profile Desktop'
            5 = 'Pizza Box'; 6 = 'Mini Tower'; 7 = 'Tower'; 8 = 'Portable'
            9 = 'Laptop'; 10 = 'Notebook'; 11 = 'Hand Held'; 12 = 'Docking Station'
            13 = 'All in One'; 14 = 'Sub Notebook'; 15 = 'Space-Saving'; 16 = 'Lunch Box'
            17 = 'Main Server Chassis'; 18 = 'Expansion Chassis'; 19 = 'SubChassis'
            20 = 'Bus Expansion Chassis'; 21 = 'Peripheral Chassis'; 22 = 'RAID Chassis'
            23 = 'Rack Mount Chassis'; 24 = 'Sealed-case PC'; 25 = 'Multi-system chassis'
            30 = 'Tablet'; 31 = 'Convertible'; 32 = 'Detachable'
        }
        $types = @()
        if ($chassis.ChassisTypes) {
            foreach ($t in $chassis.ChassisTypes) {
                $types += $chassisTypeMap[[int]$t] ?? "Type$t"
            }
        }
        [PSCustomObject]@{
            ChassisTypes = $chassis.ChassisTypes
            ChassisTypesText = $types
            Manufacturer = $chassis.Manufacturer
            SerialNumber = $chassis.SerialNumber
            SMBIOSAssetTag = $chassis.SMBIOSAssetTag
        }
    "#).unwrap_or_else(|| json!({}));

    // Último reinício e contagem de boots
    let boot_info = ps(r#"
        $os = Get-CimInstance Win32_OperatingSystem | Select-Object LastBootUpTime
        $lastBoot = $os.LastBootUpTime

        # Calcula uptime
        $uptime = if ($lastBoot) { (New-TimeSpan -Start $lastBoot -End (Get-Date)).TotalSeconds } else { 0 }

        # Conta eventos de boot (ID 6005) - últimos 30 dias para performance
        $startDate = (Get-Date).AddDays(-30)
        $bootEvents = @()
        $bootCount = 0
        try {
            $events = Get-WinEvent -FilterHashtable @{
                LogName = 'System'
                ID = 6005
                StartTime = $startDate
            } -MaxEvents 50 -ErrorAction SilentlyContinue
            $bootCount = @($events).Count
            $bootEvents = @($events | Select-Object -First 10 | ForEach-Object {
                @{
                    TimeCreated = $_.TimeCreated.ToString('o')
                    Computer = $_.MachineName
                }
            })
        } catch {}

        [PSCustomObject]@{
            LastBootTime = if ($lastBoot) { $lastBoot.ToString('o') } else { $null }
            UptimeSeconds = [math]::Round($uptime)
            BootCountLast30Days = $bootCount
            RecentBoots = $bootEvents
        }
    "#).unwrap_or_else(|| json!({ "LastBootTime": null, "UptimeSeconds": 0, "BootCountLast30Days": 0, "RecentBoots": [] }));

    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,
            "bitLocker": bitlocker,
            "tpm": tpm,
            "secureBoot": secure_boot,
            "deviceGuard": device_guard,
            "firewallProfiles": firewall_profiles,
            "windowsUpdate": windows_update,
            "computerSystem": computer_system,
            "azureAdStatus": device_join,
            "battery": battery,
            "thermal": thermal,
            "networkAdapters": network_adapters,
            "monitors": monitors,
            "chassis": power_supply,
            "bootInfo": boot_info,
        }
    })
}

#[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(())
}

#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct UsbPolicyResponse {
    pending: bool,
    policy: Option<String>,
    #[allow(dead_code)]
    applied_at: Option<i64>,
}

#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct UsbPolicyStatusReport {
    machine_token: String,
    status: String,
    error: Option<String>,
    current_policy: Option<String>,
}

async fn check_and_apply_usb_policy(base_url: &str, token: &str) {
    crate::log_info!("Verificando politica USB pendente...");

    let url = format!("{}/api/machines/usb-policy?machineToken={}", base_url, token);

    let response = match HTTP_CLIENT.get(&url).send().await {
        Ok(resp) => {
            crate::log_info!("Resposta da verificacao de politica USB: status={}", resp.status());
            resp
        }
        Err(e) => {
            crate::log_error!("Falha ao verificar politica USB: {e}");
            return;
        }
    };

    let policy_response: UsbPolicyResponse = match response.json().await {
        Ok(data) => data,
        Err(e) => {
            crate::log_error!("Falha ao parsear resposta de politica USB: {e}");
            return;
        }
    };

    if !policy_response.pending {
        crate::log_info!("Nenhuma politica USB pendente");
        return;
    }

    let policy_str = match policy_response.policy {
        Some(p) => p,
        None => {
            crate::log_warn!("Politica USB pendente mas sem valor de policy");
            return;
        }
    };

    crate::log_info!("Politica USB pendente encontrada: {}", policy_str);

    #[cfg(target_os = "windows")]
    {
        use crate::usb_control::{get_current_policy, UsbPolicy};
        use crate::service_client;

        let policy = match UsbPolicy::from_str(&policy_str) {
            Some(p) => p,
            None => {
                crate::log_error!("Politica USB invalida: {}", policy_str);
                report_usb_policy_status(base_url, token, "FAILED", Some(format!("Politica invalida: {}", policy_str)), None).await;
                return;
            }
        };

        // Verifica se a politica ja esta aplicada localmente
        match get_current_policy() {
            Ok(current) if current == policy => {
                crate::log_info!("Politica USB ja esta aplicada localmente: {}", policy_str);
                let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await;
                if !reported {
                    crate::log_error!("Falha ao reportar politica ja aplicada");
                }
                return;
            }
            Ok(current) => {
                crate::log_info!("Politica atual: {:?}, esperada: {:?}", current, policy);
            }
            Err(e) => {
                crate::log_warn!("Nao foi possivel ler politica atual: {e}");
            }
        }

        crate::log_info!("Aplicando politica USB: {}", policy_str);

        // Reporta APPLYING para progress bar real no frontend
        let _ = report_usb_policy_status(base_url, token, "APPLYING", None, None).await;

        // Tenta primeiro via RavenService (privilegiado)
        crate::log_info!("Tentando aplicar politica via RavenService...");
        match service_client::apply_usb_policy(&policy_str) {
            Ok(result) => {
                if result.success {
                    crate::log_info!("Politica USB aplicada com sucesso via RavenService: {:?}", result);
                    let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await;
                    if !reported {
                        crate::log_error!("CRITICO: Politica aplicada mas falha ao reportar ao servidor!");
                        let base_url = base_url.to_string();
                        let token = token.to_string();
                        tokio::spawn(async move {
                            tokio::time::sleep(Duration::from_secs(60)).await;
                            crate::log_info!("Retry agendado: reportando politica USB...");
                            let _ = report_usb_policy_status(&base_url, &token, "APPLIED", None, Some(policy_str)).await;
                        });
                    }
                    return;
                } else {
                    let err_msg = result.error.unwrap_or_else(|| "Erro desconhecido".to_string());
                    crate::log_error!("RavenService retornou erro: {}", err_msg);
                    report_usb_policy_status(base_url, token, "FAILED", Some(err_msg), None).await;
                }
            }
            Err(service_client::ServiceClientError::ServiceUnavailable(msg)) => {
                crate::log_warn!("RavenService nao disponivel: {}", msg);
                // Tenta fallback direto (vai falhar se nao tiver privilegio)
                crate::log_info!("Tentando aplicar politica diretamente...");
                match crate::usb_control::apply_usb_policy(policy) {
                    Ok(result) => {
                        crate::log_info!("Politica USB aplicada com sucesso (direto): {:?}", result);
                        let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await;
                        if !reported {
                            crate::log_error!("CRITICO: Politica aplicada mas falha ao reportar ao servidor!");
                            let base_url = base_url.to_string();
                            let token = token.to_string();
                            tokio::spawn(async move {
                                tokio::time::sleep(Duration::from_secs(60)).await;
                                crate::log_info!("Retry agendado: reportando politica USB...");
                                let _ = report_usb_policy_status(&base_url, &token, "APPLIED", None, Some(policy_str)).await;
                            });
                        }
                    }
                    Err(e) => {
                        let err_msg = format!("RavenService indisponivel e aplicacao direta falhou: {}. Instale ou inicie o RavenService.", e);
                        crate::log_error!("{}", err_msg);
                        report_usb_policy_status(base_url, token, "FAILED", Some(err_msg), None).await;
                    }
                }
            }
            Err(e) => {
                crate::log_error!("Falha ao comunicar com RavenService: {e}");
                report_usb_policy_status(base_url, token, "FAILED", Some(e.to_string()), None).await;
            }
        }
    }

    #[cfg(not(target_os = "windows"))]
    {
        crate::log_warn!("Controle de USB nao suportado neste sistema operacional");
        report_usb_policy_status(base_url, token, "FAILED", Some("Sistema operacional nao suportado".to_string()), None).await;
    }
}

async fn report_usb_policy_status(
    base_url: &str,
    token: &str,
    status: &str,
    error: Option<String>,
    current_policy: Option<String>,
) -> bool {
    let url = format!("{}/api/machines/usb-policy", base_url);

    let report = UsbPolicyStatusReport {
        machine_token: token.to_string(),
        status: status.to_string(),
        error,
        current_policy,
    };

    crate::log_info!("Reportando status de politica USB: status={}", status);

    // Retry simples: 1 tentativa imediata + 1 retry após 2s
    let delays = [2];
    let mut last_error = None;

    for (attempt, delay_secs) in delays.iter().enumerate() {
        match HTTP_CLIENT.post(&url).json(&report).send().await {
            Ok(response) => {
                let status_code = response.status();
                if status_code.is_success() {
                    crate::log_info!(
                        "Report de politica USB enviado com sucesso na tentativa {}",
                        attempt + 1
                    );
                    return true;
                } else {
                    let body = response.text().await.unwrap_or_default();
                    last_error = Some(format!("HTTP {} - {}", status_code, body));
                    crate::log_warn!(
                        "Report de politica USB falhou (tentativa {}): HTTP {}",
                        attempt + 1,
                        status_code
                    );
                }
            }
            Err(e) => {
                last_error = Some(e.to_string());
                crate::log_warn!(
                    "Report de politica USB falhou (tentativa {}): {}",
                    attempt + 1,
                    e
                );
            }
        }

        if attempt < delays.len() - 1 {
            crate::log_info!("Retentando report de politica USB em {}s...", delay_secs);
            tokio::time::sleep(Duration::from_secs(*delay_secs)).await;
        }
    }

    if let Some(err) = last_error {
        crate::log_error!(
            "Falha ao reportar status de politica USB apos {} tentativas: {err}",
            delays.len()
        );
    }

    false
}

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, Clone)]
pub struct AgentRuntime {
    inner: Arc<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: Arc::new(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 {
            crate::log_info!("Loop de agente iniciado");

            if let Err(error) =
                post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await
            {
                crate::log_error!("Falha inicial ao enviar heartbeat: {error}");
            } else {
                crate::log_info!("Heartbeat inicial enviado com sucesso");
            }

            // Verifica politica USB apos heartbeat inicial
            check_and_apply_usb_policy(&base_clone, &token_clone).await;

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

            loop {
                // Wait interval
                tokio::select! {
                    _ = stop_signal_clone.notified() => {
                        crate::log_info!("Loop de agente encerrado por sinal de parada");
                        break;
                    }
                    _ = heartbeat_ticker.tick() => {}
                    _ = usb_ticker.tick() => {
                        check_and_apply_usb_policy(&base_clone, &token_clone).await;
                        continue;
                    }
                }

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

                // Verifica politica USB apos cada heartbeat
                check_and_apply_usb_policy(&base_clone, &token_clone).await;
            }
        });

        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();
        }
    }
}

#[cfg(all(test, target_os = "windows"))]
mod windows_tests {
    use super::collect_windows_extended;
    use serde_json::Value;

    fn expect_object<'a>(value: &'a Value, context: &str) -> &'a serde_json::Map<String, Value> {
        value
            .as_object()
            .unwrap_or_else(|| panic!("{context} não é um objeto JSON: {value:?}"))
    }

    #[test]
    fn collects_activation_and_defender_status() {
        let extended = collect_windows_extended();
        let windows = extended.get("windows").unwrap_or_else(|| {
            panic!("payload windows ausente: {extended:?}");
        });
        let windows_obj = expect_object(windows, "windows");

        let os_info = windows_obj
            .get("osInfo")
            .unwrap_or_else(|| panic!("windows.osInfo ausente: {windows_obj:?}"));
        let os_info_obj = expect_object(os_info, "windows.osInfo");

        let is_activated = os_info_obj.get("IsActivated").unwrap_or_else(|| {
            panic!("campo IsActivated ausente em windows.osInfo: {os_info_obj:?}")
        });
        assert!(
            is_activated.as_bool().is_some(),
            "esperava booleano em windows.osInfo.IsActivated, valor recebido: {is_activated:?}"
        );

        let license_status = os_info_obj.get("LicenseStatus").unwrap_or_else(|| {
            panic!("campo LicenseStatus ausente em windows.osInfo: {os_info_obj:?}")
        });
        assert!(
            license_status.as_i64().is_some(),
            "esperava número em windows.osInfo.LicenseStatus, valor recebido: {license_status:?}"
        );

        let defender = windows_obj.get("defender").unwrap_or_else(|| {
            panic!("windows.defender ausente: {windows_obj:?}");
        });
        let defender_obj = expect_object(defender, "windows.defender");

        let realtime = defender_obj
            .get("RealTimeProtectionEnabled")
            .unwrap_or_else(|| {
                panic!(
                    "campo RealTimeProtectionEnabled ausente em windows.defender: {defender_obj:?}"
                )
            });
        assert!(
            realtime.as_bool().is_some(),
            "esperava booleano em windows.defender.RealTimeProtectionEnabled, valor recebido: {realtime:?}"
        );
    }
}
