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 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> {
    let mut entries = Vec::new();
    if let Ok(ifaces) = get_if_addrs::get_if_addrs() {
        for iface in ifaces {
            let name = iface.name;
            let mac = iface.mac.map(|m| m.to_string());
            let addr = iface.ip();
            let ip = addr.to_string();
            entries.push(json!({
                "name": name,
                "mac": mac,
                "ip": ip,
            }));
        }
    }
    entries
}

fn collect_disks(system: &System) -> Vec<serde_json::Value> {
    let mut out = Vec::new();
    for d in system.disks() {
        let name = d.name().to_string_lossy().to_string();
        let mount = d.mount_point().to_string_lossy().to_string();
        let fs = String::from_utf8_lossy(d.file_system()).to_string();
        let total = d.total_space();
        let avail = d.available_space();
        out.push(json!({
            "name": name,
            "mountPoint": mount,
            "fs": fs,
            "totalBytes": total,
            "availableBytes": avail,
        }));
    }
    out
}

fn build_inventory_metadata(system: &System) -> serde_json::Value {
    let cpu_brand = system
        .cpus()
        .first()
        .map(|cpu| cpu.brand().to_string())
        .unwrap_or_default();
    let mem_total_bytes = system.total_memory().saturating_mul(1024);
    let network = collect_network_addrs();
    let disks = collect_disks(system);
    let mut inventory = json!({
        "cpu": { "brand": cpu_brand },
        "memory": { "totalBytes": mem_total_bytes },
        "network": network,
        "disks": disks,
    });

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

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

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

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();
    let memory_total_bytes = total_memory.saturating_mul(1024);
    let memory_used_bytes = used_memory.saturating_mul(1024);
    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,
    status: Option<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,
            status,
            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();
        }
    }
}
