use nix::sys::signal::{kill as nix_kill, Signal};
use nix::unistd::Pid;
use plasmavmc_types::{Error, NetworkSpec, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::net::Ipv4Addr;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use tokio::fs;
use tokio::process::Command;
use tokio::time::{sleep, Duration, Instant};

pub const NETWORK_STATE_KEY: &str = "network_state";
const DNSMASQ_START_TIMEOUT: Duration = Duration::from_secs(5);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkRuntimeState {
    pub nic_id: String,
    pub subnet_id: String,
    pub port_id: String,
    pub bridge_name: String,
    pub tap_name: String,
    pub mac_address: String,
    pub ip_address: String,
    pub cidr_block: String,
    pub gateway_ip: String,
    pub dhcp_enabled: bool,
    pub network_dir: String,
    pub dnsmasq_conf: String,
    pub hosts_file: String,
    pub lease_file: String,
    pub pid_file: String,
    pub host_alias: String,
}

#[derive(Debug, Clone)]
struct NicDataplaneConfig {
    state: NetworkRuntimeState,
    gateway_prefix: String,
    dhcp_range_start: String,
    dhcp_range_end: String,
}

pub fn tap_name_for_nic(nic: &NetworkSpec) -> String {
    let seed = nic
        .port_id
        .as_deref()
        .or(nic.subnet_id.as_deref())
        .unwrap_or(&nic.id);
    interface_name("pct", seed)
}

pub fn encode_network_states(states: &[NetworkRuntimeState]) -> Result<String> {
    serde_json::to_string(states)
        .map_err(|error| Error::HypervisorError(format!("failed to encode network state: {error}")))
}

pub fn decode_network_states(serialized: Option<&String>) -> Result<Vec<NetworkRuntimeState>> {
    match serialized {
        Some(value) if !value.trim().is_empty() => serde_json::from_str(value).map_err(|error| {
            Error::HypervisorError(format!("failed to decode network state: {error}"))
        }),
        _ => Ok(Vec::new()),
    }
}

pub async fn ensure_vm_networks(
    runtime_root: &Path,
    nics: &[NetworkSpec],
) -> Result<Vec<NetworkRuntimeState>> {
    let mut states = Vec::with_capacity(nics.len());
    for nic in nics {
        let config = dataplane_config(runtime_root, nic)?;
        states.push(config.state.clone());
        if let Err(error) = ensure_bridge(&config).await {
            let _ = cleanup_vm_networks(&states).await;
            return Err(error);
        }
        if let Err(error) = ensure_dnsmasq(&config).await {
            let _ = cleanup_vm_networks(&states).await;
            return Err(error);
        }
        if let Err(error) = ensure_tap(runtime_root, &config).await {
            let _ = cleanup_vm_networks(&states).await;
            return Err(error);
        }
    }
    Ok(states)
}

pub async fn cleanup_vm_networks(states: &[NetworkRuntimeState]) -> Result<()> {
    let mut errors = Vec::new();
    let mut seen_bridges = HashSet::new();

    for state in states.iter().rev() {
        if let Err(error) = delete_interface_if_present(&state.tap_name).await {
            errors.push(error.to_string());
        }

        if let Err(error) = remove_host_entry(state).await {
            errors.push(error.to_string());
        }

        if !seen_bridges.insert(state.bridge_name.clone()) {
            continue;
        }

        match bridge_has_hosts(state).await {
            Ok(true) => {
                if let Err(error) = reload_dnsmasq(state).await {
                    errors.push(error.to_string());
                }
            }
            Ok(false) => {
                if let Err(error) = stop_dnsmasq(state).await {
                    errors.push(error.to_string());
                }
                if let Err(error) = delete_interface_if_present(&state.bridge_name).await {
                    errors.push(error.to_string());
                }
                let _ = fs::remove_dir_all(&state.network_dir).await;
            }
            Err(error) => errors.push(error.to_string()),
        }
    }

    if errors.is_empty() {
        Ok(())
    } else {
        Err(Error::HypervisorError(format!(
            "network cleanup failed: {}",
            errors.join("; ")
        )))
    }
}

fn dataplane_config(runtime_root: &Path, nic: &NetworkSpec) -> Result<NicDataplaneConfig> {
    let subnet_id = nic
        .subnet_id
        .clone()
        .ok_or_else(|| Error::UnsupportedFeature("KVM NIC requires subnet_id".into()))?;
    let port_id = nic.port_id.clone().unwrap_or_else(|| nic.id.clone());
    let mac_address = nic
        .mac_address
        .clone()
        .ok_or_else(|| Error::UnsupportedFeature("KVM NIC requires mac_address".into()))?;
    let ip_address = nic
        .ip_address
        .clone()
        .ok_or_else(|| Error::UnsupportedFeature("KVM NIC requires ip_address".into()))?;
    let cidr_block = nic
        .cidr_block
        .clone()
        .ok_or_else(|| Error::UnsupportedFeature("KVM NIC requires cidr_block".into()))?;
    let gateway_ip = nic
        .gateway_ip
        .clone()
        .ok_or_else(|| Error::UnsupportedFeature("KVM NIC requires gateway_ip".into()))?;

    let (cidr_ip, prefix) = parse_ipv4_cidr(&cidr_block)?;
    let gateway = parse_ipv4(&gateway_ip, "gateway_ip")?;
    if !cidr_contains_ip(cidr_ip, prefix, gateway) {
        return Err(Error::HypervisorError(format!(
            "gateway {gateway_ip} is outside subnet {cidr_block}"
        )));
    }
    let (dhcp_range_start, dhcp_range_end) = dhcp_range(cidr_ip, prefix, gateway)?;
    let bridge_name = interface_name("pcbr", &subnet_id);
    let tap_name = tap_name_for_nic(nic);
    let network_dir = runtime_root.join("networks").join(&subnet_id);
    let host_alias = format!("port-{}", compact_id(&port_id, 12));
    let state = NetworkRuntimeState {
        nic_id: nic.id.clone(),
        subnet_id,
        port_id,
        bridge_name,
        tap_name,
        mac_address,
        ip_address,
        cidr_block,
        gateway_ip: gateway_ip.clone(),
        dhcp_enabled: nic.dhcp_enabled,
        network_dir: network_dir.display().to_string(),
        dnsmasq_conf: network_dir.join("dnsmasq.conf").display().to_string(),
        hosts_file: network_dir.join("hosts").display().to_string(),
        lease_file: network_dir.join("leases").display().to_string(),
        pid_file: network_dir.join("dnsmasq.pid").display().to_string(),
        host_alias,
    };

    Ok(NicDataplaneConfig {
        gateway_prefix: format!("{gateway_ip}/{prefix}"),
        dhcp_range_start,
        dhcp_range_end,
        state,
    })
}

async fn ensure_bridge(config: &NicDataplaneConfig) -> Result<()> {
    if !link_exists(&config.state.bridge_name).await? {
        run_command(
            "ip",
            [
                "link",
                "add",
                "name",
                config.state.bridge_name.as_str(),
                "type",
                "bridge",
            ],
        )
        .await?;
    }

    run_command(
        "ip",
        [
            "addr",
            "replace",
            config.gateway_prefix.as_str(),
            "dev",
            config.state.bridge_name.as_str(),
        ],
    )
    .await?;
    run_command(
        "ip",
        [
            "link",
            "set",
            "dev",
            config.state.bridge_name.as_str(),
            "up",
        ],
    )
    .await
}

async fn ensure_dnsmasq(config: &NicDataplaneConfig) -> Result<()> {
    fs::create_dir_all(&config.state.network_dir)
        .await
        .map_err(|error| {
            Error::HypervisorError(format!(
                "failed to create network runtime directory {}: {error}",
                config.state.network_dir
            ))
        })?;
    write_hosts_file(&config.state).await?;

    let dnsmasq_conf = format!(
        "interface={bridge}\n\
bind-interfaces\n\
except-interface=lo\n\
port=0\n\
dhcp-authoritative\n\
dhcp-option=option:router,{gateway}\n\
dhcp-range={range_start},{range_end},{mask},1h\n\
dhcp-hostsfile={hosts_file}\n\
dhcp-leasefile={lease_file}\n\
pid-file={pid_file}\n",
        bridge = config.state.bridge_name,
        gateway = config.state.gateway_ip,
        range_start = config.dhcp_range_start,
        range_end = config.dhcp_range_end,
        mask = cidr_mask(&config.state.cidr_block)?,
        hosts_file = config.state.hosts_file,
        lease_file = config.state.lease_file,
        pid_file = config.state.pid_file,
    );
    fs::write(&config.state.dnsmasq_conf, dnsmasq_conf)
        .await
        .map_err(|error| {
            Error::HypervisorError(format!(
                "failed to write dnsmasq config {}: {error}",
                config.state.dnsmasq_conf
            ))
        })?;

    if dnsmasq_running(&config.state).await? {
        reload_dnsmasq(&config.state).await?;
        return Ok(());
    }

    let mut command = Command::new("dnsmasq");
    command.arg(format!("--conf-file={}", config.state.dnsmasq_conf));
    let output = command
        .output()
        .await
        .map_err(|error| Error::HypervisorError(format!("failed to spawn dnsmasq: {error}")))?;
    if !output.status.success() {
        return Err(command_failed("dnsmasq", &[], &output));
    }

    let deadline = Instant::now() + DNSMASQ_START_TIMEOUT;
    while Instant::now() < deadline {
        if dnsmasq_running(&config.state).await? {
            return Ok(());
        }
        sleep(Duration::from_millis(100)).await;
    }

    Err(Error::HypervisorError(format!(
        "dnsmasq did not start for bridge {}",
        config.state.bridge_name
    )))
}

async fn ensure_tap(runtime_root: &Path, config: &NicDataplaneConfig) -> Result<()> {
    let _ = delete_interface_if_present(&config.state.tap_name).await;
    let metadata = fs::metadata(runtime_root).await.map_err(|error| {
        Error::HypervisorError(format!(
            "failed to inspect runtime root {}: {error}",
            runtime_root.display()
        ))
    })?;
    let uid = metadata.uid().to_string();
    let gid = metadata.gid().to_string();

    run_command(
        "ip",
        [
            "tuntap",
            "add",
            "dev",
            config.state.tap_name.as_str(),
            "mode",
            "tap",
            "user",
            uid.as_str(),
            "group",
            gid.as_str(),
        ],
    )
    .await?;
    run_command(
        "ip",
        [
            "link",
            "set",
            "dev",
            config.state.tap_name.as_str(),
            "master",
            config.state.bridge_name.as_str(),
        ],
    )
    .await?;
    run_command(
        "ip",
        ["link", "set", "dev", config.state.tap_name.as_str(), "up"],
    )
    .await
}

async fn write_hosts_file(state: &NetworkRuntimeState) -> Result<()> {
    let hosts_path = PathBuf::from(&state.hosts_file);
    let existing = match fs::read_to_string(&hosts_path).await {
        Ok(contents) => contents,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
        Err(error) => {
            return Err(Error::HypervisorError(format!(
                "failed to read dnsmasq hosts file {}: {error}",
                hosts_path.display()
            )))
        }
    };

    let mut lines: Vec<String> = existing
        .lines()
        .filter(|line| !line.trim().is_empty() && !line.contains(&state.host_alias))
        .map(ToOwned::to_owned)
        .collect();
    lines.push(format!(
        "{mac},{ip},{alias}",
        mac = state.mac_address,
        ip = state.ip_address,
        alias = state.host_alias
    ));
    let mut rendered = lines.join("\n");
    if !rendered.is_empty() {
        rendered.push('\n');
    }
    fs::write(&hosts_path, rendered).await.map_err(|error| {
        Error::HypervisorError(format!(
            "failed to write dnsmasq hosts file {}: {error}",
            hosts_path.display()
        ))
    })
}

async fn remove_host_entry(state: &NetworkRuntimeState) -> Result<()> {
    let hosts_path = PathBuf::from(&state.hosts_file);
    let existing = match fs::read_to_string(&hosts_path).await {
        Ok(contents) => contents,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
        Err(error) => {
            return Err(Error::HypervisorError(format!(
                "failed to read dnsmasq hosts file {}: {error}",
                hosts_path.display()
            )))
        }
    };
    let filtered: Vec<&str> = existing
        .lines()
        .filter(|line| !line.contains(&state.host_alias))
        .collect();
    let mut rendered = filtered.join("\n");
    if !rendered.is_empty() {
        rendered.push('\n');
    }
    fs::write(&hosts_path, rendered).await.map_err(|error| {
        Error::HypervisorError(format!(
            "failed to update dnsmasq hosts file {}: {error}",
            hosts_path.display()
        ))
    })
}

async fn bridge_has_hosts(state: &NetworkRuntimeState) -> Result<bool> {
    match fs::read_to_string(&state.hosts_file).await {
        Ok(contents) => Ok(contents.lines().any(|line| !line.trim().is_empty())),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(error) => Err(Error::HypervisorError(format!(
            "failed to inspect dnsmasq hosts file {}: {error}",
            state.hosts_file
        ))),
    }
}

async fn dnsmasq_running(state: &NetworkRuntimeState) -> Result<bool> {
    let pid = read_pid_file(&state.pid_file).await?;
    if let Some(pid) = pid {
        return Ok(pid_running(pid));
    }
    Ok(false)
}

async fn reload_dnsmasq(state: &NetworkRuntimeState) -> Result<()> {
    if let Some(pid) = read_pid_file(&state.pid_file).await? {
        signal_pid(pid, Signal::SIGHUP)?;
    }
    Ok(())
}

async fn stop_dnsmasq(state: &NetworkRuntimeState) -> Result<()> {
    if let Some(pid) = read_pid_file(&state.pid_file).await? {
        signal_pid(pid, Signal::SIGTERM)?;
        let deadline = Instant::now() + Duration::from_secs(2);
        while pid_running(pid) && Instant::now() < deadline {
            sleep(Duration::from_millis(50)).await;
        }
        if pid_running(pid) {
            signal_pid(pid, Signal::SIGKILL)?;
        }
    }
    let _ = fs::remove_file(&state.pid_file).await;
    Ok(())
}

async fn read_pid_file(path: &str) -> Result<Option<u32>> {
    match fs::read_to_string(path).await {
        Ok(contents) => Ok(contents.trim().parse::<u32>().ok()),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(error) => Err(Error::HypervisorError(format!(
            "failed to read pid file {path}: {error}"
        ))),
    }
}

fn signal_pid(pid: u32, signal: Signal) -> Result<()> {
    nix_kill(Pid::from_raw(pid as i32), signal).map_err(|error| {
        Error::HypervisorError(format!(
            "failed to signal pid {pid} with {signal:?}: {error}"
        ))
    })
}

fn pid_running(pid: u32) -> bool {
    match nix_kill(Pid::from_raw(pid as i32), None::<Signal>) {
        Ok(()) => true,
        Err(nix::errno::Errno::EPERM) => true,
        Err(nix::errno::Errno::ESRCH) => false,
        Err(_) => false,
    }
}

async fn link_exists(name: &str) -> Result<bool> {
    let output = Command::new("ip")
        .args(["link", "show", "dev", name])
        .output()
        .await
        .map_err(|error| Error::HypervisorError(format!("failed to query link {name}: {error}")))?;
    Ok(output.status.success())
}

async fn delete_interface_if_present(name: &str) -> Result<()> {
    if !link_exists(name).await? {
        return Ok(());
    }
    run_command("ip", ["link", "set", "dev", name, "down"]).await?;
    run_command("ip", ["link", "delete", "dev", name]).await
}

async fn run_command<const N: usize>(program: &str, args: [&str; N]) -> Result<()> {
    let output = Command::new(program)
        .args(args)
        .output()
        .await
        .map_err(|error| Error::HypervisorError(format!("failed to spawn {program}: {error}")))?;
    if output.status.success() {
        Ok(())
    } else {
        Err(command_failed(program, &args, &output))
    }
}

fn command_failed(program: &str, args: &[&str], output: &std::process::Output) -> Error {
    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    let detail = if !stderr.is_empty() {
        stderr
    } else if !stdout.is_empty() {
        stdout
    } else {
        format!("exit code {:?}", output.status.code())
    };
    Error::HypervisorError(format!("{program} {} failed: {detail}", args.join(" ")))
}

fn parse_ipv4(value: &str, field: &str) -> Result<Ipv4Addr> {
    value
        .parse()
        .map_err(|error| Error::HypervisorError(format!("invalid {field} {value}: {error}")))
}

fn parse_ipv4_cidr(cidr: &str) -> Result<(Ipv4Addr, u8)> {
    let (ip, prefix) = cidr.split_once('/').ok_or_else(|| {
        Error::HypervisorError(format!("invalid cidr_block {cidr}: missing prefix"))
    })?;
    let ip = parse_ipv4(ip, "cidr_block ip")?;
    let prefix = prefix
        .parse::<u8>()
        .map_err(|error| Error::HypervisorError(format!("invalid cidr_block {cidr}: {error}")))?;
    if prefix > 32 {
        return Err(Error::HypervisorError(format!(
            "invalid cidr_block {cidr}: prefix must be <= 32"
        )));
    }
    Ok((ip, prefix))
}

fn cidr_contains_ip(cidr_ip: Ipv4Addr, prefix: u8, ip: Ipv4Addr) -> bool {
    let mask = if prefix == 0 {
        0
    } else {
        u32::MAX << (32 - prefix)
    };
    (u32::from(cidr_ip) & mask) == (u32::from(ip) & mask)
}

fn cidr_mask(cidr: &str) -> Result<String> {
    let (_, prefix) = parse_ipv4_cidr(cidr)?;
    let mask = if prefix == 0 {
        0
    } else {
        u32::MAX << (32 - prefix)
    };
    Ok(Ipv4Addr::from(mask).to_string())
}

fn dhcp_range(cidr_ip: Ipv4Addr, prefix: u8, gateway: Ipv4Addr) -> Result<(String, String)> {
    if prefix >= 31 {
        return Err(Error::UnsupportedFeature(
            "KVM local bridge dataplane requires an IPv4 subnet larger than /31".into(),
        ));
    }
    let mask = if prefix == 0 {
        0
    } else {
        u32::MAX << (32 - prefix)
    };
    let network = u32::from(cidr_ip) & mask;
    let broadcast = network | !mask;
    let gateway = u32::from(gateway);
    let mut start = network + 1;
    let mut end = broadcast - 1;
    if start == gateway {
        start += 1;
    }
    if end == gateway {
        end = end.saturating_sub(1);
    }
    if start > end {
        return Err(Error::HypervisorError(
            "subnet does not have enough usable DHCP addresses".into(),
        ));
    }
    Ok((
        Ipv4Addr::from(start).to_string(),
        Ipv4Addr::from(end).to_string(),
    ))
}

fn compact_id(value: &str, limit: usize) -> String {
    let compact: String = value
        .chars()
        .filter(|ch| ch.is_ascii_alphanumeric())
        .map(|ch| ch.to_ascii_lowercase())
        .collect();
    if compact.is_empty() {
        "0".repeat(limit.max(1))
    } else {
        compact.chars().take(limit).collect()
    }
}

fn interface_name(prefix: &str, seed: &str) -> String {
    let available = 15usize.saturating_sub(prefix.len());
    format!("{prefix}{}", compact_id(seed, available.max(1)))
}

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

    #[test]
    fn tap_name_prefers_port_id() {
        let nic = NetworkSpec {
            id: "tenant0".into(),
            port_id: Some("12345678-1234-1234-1234-1234567890ab".into()),
            ..NetworkSpec::default()
        };
        assert_eq!(tap_name_for_nic(&nic), "pct123456781234");
    }

    #[test]
    fn interface_names_fit_kernel_limit() {
        let name = interface_name("pcbr", "12345678-1234-1234-1234-1234567890ab");
        assert!(name.len() <= 15);
        assert_eq!(name, "pcbr12345678123");
    }

    #[test]
    fn dhcp_range_skips_gateway() {
        let (start, end) = dhcp_range(
            "10.62.10.0".parse().unwrap(),
            24,
            "10.62.10.1".parse().unwrap(),
        )
        .unwrap();
        assert_eq!(start, "10.62.10.2");
        assert_eq!(end, "10.62.10.254");
    }

    #[test]
    fn encode_and_decode_network_state_round_trip() {
        let state = NetworkRuntimeState {
            nic_id: "tenant0".into(),
            subnet_id: "subnet".into(),
            port_id: "port".into(),
            bridge_name: "pcbrsubnet".into(),
            tap_name: "pctport".into(),
            mac_address: "02:00:00:00:62:10".into(),
            ip_address: "10.62.10.10".into(),
            cidr_block: "10.62.10.0/24".into(),
            gateway_ip: "10.62.10.1".into(),
            dhcp_enabled: true,
            network_dir: "/run/libvirt/plasmavmc/networks/subnet".into(),
            dnsmasq_conf: "/run/libvirt/plasmavmc/networks/subnet/dnsmasq.conf".into(),
            hosts_file: "/run/libvirt/plasmavmc/networks/subnet/hosts".into(),
            lease_file: "/run/libvirt/plasmavmc/networks/subnet/leases".into(),
            pid_file: "/run/libvirt/plasmavmc/networks/subnet/dnsmasq.pid".into(),
            host_alias: "port-port".into(),
        };
        let encoded = encode_network_states(&[state.clone()]).unwrap();
        let decoded = decode_network_states(Some(&encoded)).unwrap();
        assert_eq!(decoded, vec![state]);
    }
}
