//! VM lifecycle operations for Cloud Hypervisor.

use std::fs;
use std::path::Path;
use std::thread;
use std::time::Duration;

use super::error::CloudHvError;
use super::paths::{hero_vm_root, vm_dir, vm_json_path, vm_log_path, vm_pid_path};
use super::types::{VmRecord, VmRuntime, VmSpec};
use super::utils::{ensure_deps, proc_exists, read_json, shell_escape, shell_join, write_json};
use crate::qcow2;

/// Creates and persists a VM specification.
///
/// Validates the specification and creates the necessary directory structure.
/// Returns the VM ID on success.
pub fn vm_create(spec: &VmSpec) -> Result<String, CloudHvError> {
    // Validate inputs
    if spec.id.trim().is_empty() {
        return Err(CloudHvError::InvalidSpec(
            "spec.id must not be empty".into(),
        ));
    }

    // Validate boot method: either firmware_path or kernel_path must exist
    let has_fw = spec
        .firmware_path
        .as_ref()
        .is_some_and(|p| Path::new(p).exists());
    let has_kernel = spec
        .kernel_path
        .as_ref()
        .is_some_and(|p| Path::new(p).exists());

    if !(has_fw || has_kernel) {
        return Err(CloudHvError::InvalidSpec(
            "either firmware_path or kernel_path must be set to an existing file".into(),
        ));
    }

    if !Path::new(&spec.disk_path).exists() {
        return Err(CloudHvError::InvalidSpec(format!(
            "disk_path not found: {}",
            &spec.disk_path
        )));
    }
    if spec.vcpus == 0 {
        return Err(CloudHvError::InvalidSpec("vcpus must be >= 1".into()));
    }
    if spec.memory_mb == 0 {
        return Err(CloudHvError::InvalidSpec("memory_mb must be >= 128".into()));
    }

    // Prepare directory layout
    let dir = vm_dir(&spec.id);
    crate::os::mkdir(
        dir.to_str()
            .unwrap_or_else(|| "/tmp/hero/virt/vms/__invalid__"),
    )
    .map_err(|e| CloudHvError::IoError(e.to_string()))?;
    let log_dir = dir.join("logs");
    crate::os::mkdir(log_dir.to_str().unwrap())
        .map_err(|e| CloudHvError::IoError(e.to_string()))?;

    // Persist initial record
    let rec = VmRecord {
        spec: spec.clone(),
        runtime: VmRuntime {
            pid: None,
            status: "stopped".into(),
            log_file: vm_log_path(&spec.id).to_string_lossy().into_owned(),
        },
    };
    let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
    write_json(&vm_json_path(&spec.id), &value)?;

    Ok(spec.id.clone())
}

/// Starts a VM using cloud-hypervisor.
///
/// Loads the VM specification, prepares the disk (converting qcow2 to raw if needed),
/// and spawns the cloud-hypervisor process.
pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
    ensure_deps()?;

    // Load record
    let p = vm_json_path(id);
    if !p.exists() {
        return Err(CloudHvError::NotFound(format!("VM '{}' not found", id)));
    }
    let value = read_json(&p)?;
    let mut rec: VmRecord =
        serde_json::from_value(value).map_err(|e| CloudHvError::JsonError(e.to_string()))?;

    // Prepare API socket path
    let api_socket = if rec.spec.api_socket.trim().is_empty() {
        vm_dir(id).join("api.sock").to_string_lossy().into_owned()
    } else {
        rec.spec.api_socket.clone()
    };
    let log_file = vm_log_path(id).to_string_lossy().into_owned();

    // Ensure API socket directory exists and remove any stale socket file
    let api_path = Path::new(&api_socket);
    if let Some(parent) = api_path.parent() {
        fs::create_dir_all(parent).map_err(|e| CloudHvError::IoError(e.to_string()))?;
    }
    let _ = fs::remove_file(api_path);

    // Convert qcow2 to raw if needed
    let mut disk_to_use = rec.spec.disk_path.clone();
    if let Ok(info) = qcow2::info(&disk_to_use) {
        if info.get("format").and_then(|v| v.as_str()) == Some("qcow2") {
            let dest = vm_dir(id).join("disk.raw").to_string_lossy().into_owned();
            let cmd = format!(
                "qemu-img convert -O raw {} {}",
                shell_escape(&disk_to_use),
                shell_escape(&dest)
            );
            match crate::process::run(&cmd).silent(true).execute() {
                Ok(res) if res.success => {
                    disk_to_use = dest;
                }
                Ok(res) => {
                    return Err(CloudHvError::CommandFailed(format!(
                        "Failed converting qcow2 to raw: {}",
                        res.stderr
                    )));
                }
                Err(e) => {
                    return Err(CloudHvError::CommandFailed(format!(
                        "Failed converting qcow2 to raw: {}",
                        e
                    )));
                }
            }
        }
    }

    // Build command
    let mut parts: Vec<String> = vec![
        "cloud-hypervisor-static".into(),
        "--api-socket".into(),
        api_socket.clone(),
    ];

    if let Some(fw) = rec.spec.firmware_path.clone() {
        parts.push("--firmware".into());
        parts.push(fw);
    } else if let Some(kpath) = rec.spec.kernel_path.clone() {
        let cmdline = rec
            .spec
            .cmdline
            .clone()
            .unwrap_or_else(|| "console=ttyS0 reboot=k panic=1".to_string());
        parts.push("--kernel".into());
        parts.push(kpath);
        parts.push("--cmdline".into());
        parts.push(cmdline);
    } else {
        return Err(CloudHvError::InvalidSpec(
            "neither firmware_path nor kernel_path set at start time".into(),
        ));
    }

    parts.push("--disk".into());
    parts.push(format!("path={}", disk_to_use));
    parts.push("--cpus".into());
    parts.push(format!("boot={}", rec.spec.vcpus));
    parts.push("--memory".into());
    parts.push(format!("size={}M", rec.spec.memory_mb));
    parts.push("--serial".into());
    parts.push("tty".into());
    parts.push("--console".into());
    parts.push("off".into());

    if let Some(extra) = rec.spec.extra_args.clone() {
        for e in extra {
            parts.push(e);
        }
    }

    let args_str = shell_join(&parts);
    let script = format!(
        "#!/bin/bash -e
nohup {} > '{}' 2>&1 &
echo $! > '{}'
",
        args_str,
        log_file,
        vm_pid_path(id).to_string_lossy()
    );

    // Execute script
    let result = crate::process::run(&script).execute();
    match result {
        Ok(res) => {
            if !res.success {
                return Err(CloudHvError::CommandFailed(format!(
                    "Failed to start VM '{}': {}",
                    id, res.stderr
                )));
            }
        }
        Err(e) => {
            return Err(CloudHvError::CommandFailed(format!(
                "Failed to start VM '{}': {}",
                id, e
            )))
        }
    }

    // Read PID back
    let pid = match fs::read_to_string(vm_pid_path(id)) {
        Ok(s) => s.trim().parse::<i64>().ok(),
        Err(_) => None,
    };

    // Update state
    rec.runtime.pid = pid;
    rec.runtime.status = if pid.is_some() {
        "running".into()
    } else {
        "stopped".into()
    };
    rec.runtime.log_file = log_file;
    rec.spec.api_socket = api_socket.clone();

    let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
    write_json(&vm_json_path(id), &value)?;

    Ok(())
}

/// Returns VM record info (spec + runtime) by ID.
pub fn vm_info(id: &str) -> Result<VmRecord, CloudHvError> {
    let p = vm_json_path(id);
    if !p.exists() {
        return Err(CloudHvError::NotFound(format!("VM '{}' not found", id)));
    }
    let value = read_json(&p)?;
    let rec: VmRecord =
        serde_json::from_value(value).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
    Ok(rec)
}

/// Stops a VM via ch-remote (graceful), optionally force kills.
pub fn vm_stop(id: &str, force: bool) -> Result<(), CloudHvError> {
    ensure_deps().ok();

    let p = vm_json_path(id);
    if !p.exists() {
        return Err(CloudHvError::NotFound(format!("VM '{}' not found", id)));
    }
    let value = read_json(&p)?;
    let mut rec: VmRecord =
        serde_json::from_value(value).map_err(|e| CloudHvError::JsonError(e.to_string()))?;

    // Attempt graceful shutdown if API socket is known
    if !rec.spec.api_socket.trim().is_empty() {
        let cmd = format!(
            "ch-remote-static --api-socket {} shutdown",
            rec.spec.api_socket
        );
        let _ = crate::process::run(&cmd).die(false).silent(true).execute();
    }

    // Wait for process to exit (up to ~10s)
    if let Some(pid) = rec.runtime.pid {
        for _ in 0..50 {
            if !proc_exists(pid) {
                break;
            }
            thread::sleep(Duration::from_millis(200));
        }
        // If still alive and force, kill -9 and wait again
        if proc_exists(pid) && force {
            let _ = crate::process::run(&format!("kill -9 {}", pid))
                .die(false)
                .silent(true)
                .execute();
            for _ in 0..50 {
                if !proc_exists(pid) {
                    break;
                }
                thread::sleep(Duration::from_millis(200));
            }
        }
    }

    // Update state
    rec.runtime.status = "stopped".into();
    rec.runtime.pid = None;
    let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
    write_json(&vm_json_path(id), &value)?;

    // Remove pid file
    let _ = fs::remove_file(vm_pid_path(id));

    Ok(())
}

/// Deletes a VM definition; optionally deletes disks.
pub fn vm_delete(id: &str, delete_disks: bool) -> Result<(), CloudHvError> {
    let p = vm_json_path(id);
    if !p.exists() {
        return Err(CloudHvError::NotFound(format!("VM '{}' not found", id)));
    }
    let rec: VmRecord = serde_json::from_value(read_json(&p)?)
        .map_err(|e| CloudHvError::JsonError(e.to_string()))?;

    // If appears to be running, attempt a force stop first
    if let Some(pid) = rec.runtime.pid {
        if proc_exists(pid) {
            let _ = vm_stop(id, true);
            for _ in 0..25 {
                if !proc_exists(pid) {
                    break;
                }
                thread::sleep(Duration::from_millis(200));
            }
            if proc_exists(pid) {
                return Err(CloudHvError::CommandFailed(
                    "VM appears to be running; stop it first".into(),
                ));
            }
        }
    }

    if delete_disks {
        let _ = fs::remove_file(&rec.spec.disk_path);
    }

    let d = vm_dir(id);
    fs::remove_dir_all(&d).map_err(|e| CloudHvError::IoError(e.to_string()))?;
    Ok(())
}

/// Lists all VMs.
pub fn vm_list() -> Result<Vec<VmRecord>, CloudHvError> {
    let root = hero_vm_root();
    if !root.exists() {
        return Ok(vec![]);
    }
    let mut out = vec![];
    for entry in fs::read_dir(&root).map_err(|e| CloudHvError::IoError(e.to_string()))? {
        let entry = entry.map_err(|e| CloudHvError::IoError(e.to_string()))?;
        let p = entry.path();
        if !p.is_dir() {
            continue;
        }
        let vm_json = p.join("vm.json");
        if !vm_json.exists() {
            continue;
        }
        let rec: VmRecord = serde_json::from_value(read_json(&vm_json)?)
            .map_err(|e| CloudHvError::JsonError(e.to_string()))?;

        out.push(rec);
    }
    Ok(out)
}
