use serde_json::Value;
use std::error::Error;
use std::fmt;
use std::fs;
use std::path::Path;

use herolib_os::process::RunError;

/// Error type for qcow2 operations
#[derive(Debug)]
pub enum Qcow2Error {
    /// Failed to execute a system command
    CommandExecutionFailed(String),
    /// Command executed but returned non-zero or failed semantics
    CommandFailed(String),
    /// JSON parsing error
    JsonParseError(String),
    /// IO error (filesystem)
    IoError(String),
    /// Dependency missing or invalid input
    Other(String),
}

impl fmt::Display for Qcow2Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Qcow2Error::CommandExecutionFailed(e) => write!(f, "Command execution failed: {}", e),
            Qcow2Error::CommandFailed(e) => write!(f, "{}", e),
            Qcow2Error::JsonParseError(e) => write!(f, "JSON parse error: {}", e),
            Qcow2Error::IoError(e) => write!(f, "IO error: {}", e),
            Qcow2Error::Other(e) => write!(f, "{}", e),
        }
    }
}

impl Error for Qcow2Error {}

fn from_run_error(e: RunError) -> Qcow2Error {
    Qcow2Error::CommandExecutionFailed(e.to_string())
}

fn ensure_parent_dir(path: &str) -> Result<(), Qcow2Error> {
    if let Some(parent) = Path::new(path).parent() {
        fs::create_dir_all(parent).map_err(|e| Qcow2Error::IoError(e.to_string()))?;
    }
    Ok(())
}

fn ensure_qemu_img() -> Result<(), Qcow2Error> {
    if herolib_os::process::which("qemu-img").is_none() {
        return Err(Qcow2Error::Other(
            "qemu-img not found on PATH. Please install qemu-utils (Debian/Ubuntu) or the QEMU tools for your distro.".to_string(),
        ));
    }
    Ok(())
}

fn run_quiet(cmd: &str) -> Result<herolib_os::process::CommandResult, Qcow2Error> {
    herolib_os::process::run(cmd)
        .silent(true)
        .execute()
        .map_err(from_run_error)
        .and_then(|res| {
            if res.success {
                Ok(res)
            } else {
                Err(Qcow2Error::CommandFailed(format!(
                    "Command failed (code {}): {}\n{}",
                    res.code, cmd, res.stderr
                )))
            }
        })
}

/// Create a qcow2 image at path with a given virtual size (in GiB)
pub fn create(path: &str, size_gb: i64) -> Result<String, Qcow2Error> {
    ensure_qemu_img()?;
    if size_gb <= 0 {
        return Err(Qcow2Error::Other(
            "size_gb must be > 0 for qcow2.create".to_string(),
        ));
    }
    ensure_parent_dir(path)?;
    let cmd = format!("qemu-img create -f qcow2 {} {}G", path, size_gb);
    run_quiet(&cmd)?;
    Ok(path.to_string())
}

/// Return qemu-img info as a JSON value
pub fn info(path: &str) -> Result<Value, Qcow2Error> {
    ensure_qemu_img()?;
    if !Path::new(path).exists() {
        return Err(Qcow2Error::IoError(format!("Image not found: {}", path)));
    }
    let cmd = format!("qemu-img info --output=json {}", path);
    let res = run_quiet(&cmd)?;
    serde_json::from_str::<Value>(&res.stdout)
        .map_err(|e| Qcow2Error::JsonParseError(e.to_string()))
}

/// Create an offline snapshot on a qcow2 image
pub fn snapshot_create(path: &str, name: &str) -> Result<(), Qcow2Error> {
    ensure_qemu_img()?;
    if name.trim().is_empty() {
        return Err(Qcow2Error::Other(
            "snapshot name cannot be empty".to_string(),
        ));
    }
    let cmd = format!("qemu-img snapshot -c {} {}", name, path);
    run_quiet(&cmd).map(|_| ())
}

/// Delete a snapshot on a qcow2 image
pub fn snapshot_delete(path: &str, name: &str) -> Result<(), Qcow2Error> {
    ensure_qemu_img()?;
    if name.trim().is_empty() {
        return Err(Qcow2Error::Other(
            "snapshot name cannot be empty".to_string(),
        ));
    }
    let cmd = format!("qemu-img snapshot -d {} {}", name, path);
    run_quiet(&cmd).map(|_| ())
}

/// Snapshot representation (subset of qemu-img info snapshots)
#[derive(Debug, Clone)]
pub struct Qcow2Snapshot {
    pub id: Option<String>,
    pub name: Option<String>,
    pub vm_state_size: Option<i64>,
    pub date_sec: Option<i64>,
    pub date_nsec: Option<i64>,
    pub vm_clock_nsec: Option<i64>,
}

/// List snapshots on a qcow2 image (offline)
pub fn snapshot_list(path: &str) -> Result<Vec<Qcow2Snapshot>, Qcow2Error> {
    let v = info(path)?;
    let mut out = Vec::new();
    if let Some(snaps) = v.get("snapshots").and_then(|s| s.as_array()) {
        for s in snaps {
            let snap = Qcow2Snapshot {
                id: s.get("id").and_then(|x| x.as_str()).map(|s| s.to_string()),
                name: s
                    .get("name")
                    .and_then(|x| x.as_str())
                    .map(|s| s.to_string()),
                vm_state_size: s.get("vm-state-size").and_then(|x| x.as_i64()),
                date_sec: s.get("date-sec").and_then(|x| x.as_i64()),
                date_nsec: s.get("date-nsec").and_then(|x| x.as_i64()),
                vm_clock_nsec: s.get("vm-clock-nsec").and_then(|x| x.as_i64()),
            };
            out.push(snap);
        }
    }
    Ok(out)
}

/// Result for building the base image
#[derive(Debug, Clone)]
pub struct BuildBaseResult {
    pub base_image_path: String,
    pub snapshot: String,
    pub url: String,
    pub resized_to_gb: Option<i64>,
}

/// Build/download Ubuntu 24.04 base image (Noble cloud image), optionally resize, and create a base snapshot
pub fn build_ubuntu_24_04_base(
    dest_dir: &str,
    size_gb: Option<i64>,
) -> Result<BuildBaseResult, Qcow2Error> {
    ensure_qemu_img()?;

    // Ensure destination directory exists
    herolib_os::os::mkdir(dest_dir).map_err(|e| Qcow2Error::IoError(e.to_string()))?;

    // Canonical Ubuntu Noble cloud image (amd64)
    let url = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img";

    // Build destination path
    let dest_dir_sanitized = dest_dir.trim_end_matches('/');
    let dest_path = format!("{}/noble-server-cloudimg-amd64.img", dest_dir_sanitized);

    // Download if not present
    let path_obj = Path::new(&dest_path);
    if !path_obj.exists() {
        // 50MB minimum for sanity; the actual image is much larger
        herolib_os::os::download_file(url, &dest_path, 50_000)
            .map_err(|e| Qcow2Error::IoError(e.to_string()))?;
    }

    // Resize if requested
    if let Some(sz) = size_gb {
        if sz > 0 {
            let cmd = format!("qemu-img resize {} {}G", dest_path, sz);
            run_quiet(&cmd)?;
        }
    }

    // Create "base" snapshot
    snapshot_create(&dest_path, "base")?;

    Ok(BuildBaseResult {
        base_image_path: dest_path,
        snapshot: "base".to_string(),
        url: url.to_string(),
        resized_to_gb: size_gb.filter(|v| *v > 0),
    })
}
