//! QCOW2 Disk Image Management
//!
//! This module provides functionality for creating, managing, and configuring QCOW2 disk images.
//! QCOW2 (QEMU Copy-On-Write version 2) is a disk image format that supports snapshots, compression,
//! and sparse allocation, making it ideal for virtual machines.
//!
//! # Features
//!
//! - **Create QCOW2 Images**: Create new disk images with specified virtual sizes
//! - **Snapshot Management**: Create, delete, and list snapshots for offline images
//! - **Image Information**: Query detailed information about QCOW2 images in JSON format
//! - **Base Image Builder**: Download and configure Ubuntu 24.04 cloud images as base images
//! - **Cross-Platform**: Works on Linux, macOS, and Windows (where QEMU is installed)
//!
//! # Requirements
//!
//! This module requires `qemu-img` to be installed and available on your system PATH.
//! - **Debian/Ubuntu**: `sudo apt-get install qemu-utils`
//! - **macOS**: `brew install qemu`
//! - **RedHat/CentOS**: `sudo yum install qemu-img`
//!
//! # Example
//!
//! ```rust,no_run
//! use herolib_virt::qcow2;
//!
//! // Create a simple disk image
//! let path = qcow2::create("/tmp/disk.qcow2", 20)?;
//! println!("Created image at: {}", path);
//!
//! // Get image information
//! let info = qcow2::info(&path)?;
//! println!("Image info: {:?}", info);
//!
//! // Create snapshots for backup points
//! qcow2::snapshot_create(&path, "before-install")?;
//! qcow2::snapshot_create(&path, "after-install")?;
//!
//! // List all snapshots
//! let snapshots = qcow2::snapshot_list(&path)?;
//! for snap in snapshots {
//!     println!("Snapshot: {:?}", snap.name);
//! }
//!
//! // Delete a snapshot
//! qcow2::snapshot_delete(&path, "before-install")?;
//!
//! # Ok::<(), qcow2::Qcow2Error>(())
//! ```
//!
//! # Rhai Scripting
//!
//! QCOW2 operations are also accessible from Rhai scripts:
//!
//! ```rhai
//! // Create a disk image
//! let disk_path = qcow2_create("/tmp/vm-disk.qcow2", 50);
//! print("Created disk: " + disk_path);
//!
//! // Create snapshots
//! qcow2_snapshot_create(disk_path, "initial");
//! qcow2_snapshot_create(disk_path, "with-packages");
//!
//! // List snapshots
//! let snaps = qcow2_snapshot_list(disk_path);
//! for snap in snaps {
//!     print("Snapshot: " + snap.name);
//! }
//! ```
//!
//! # Common Patterns
//!
//! ## Creating a VM Disk
//!
//! ```rust,no_run
//! # use herolib_virt::qcow2;
//! // Create a disk for a virtual machine
//! let vm_disk = qcow2::create("/var/vms/my-vm.qcow2", 50)?; // 50 GiB
//! # Ok::<(), qcow2::Qcow2Error>(())
//! ```
//!
//! ## Using Ubuntu Cloud Images as Base
//!
//! ```rust,no_run
//! # use herolib_virt::qcow2;
//! // Download and prepare an Ubuntu 24.04 cloud image
//! let base = qcow2::build_ubuntu_24_04_base("/var/vms/images", Some(100))?;
//! println!("Base image: {}", base.base_image_path);
//! println!("Snapshot: {}", base.snapshot);
//! println!("Downloaded from: {}", base.url);
//! # Ok::<(), qcow2::Qcow2Error>(())
//! ```
//!
//! ## Snapshot Workflow
//!
//! ```rust,no_run
//! # use herolib_virt::qcow2;
//! let disk = "/tmp/system.qcow2";
//! qcow2::create(disk, 30)?;
//!
//! // Create snapshot before changes
//! qcow2::snapshot_create(disk, "before-changes")?;
//!
//! // ... make changes to disk ...
//!
//! // If changes are good, create another snapshot
//! qcow2::snapshot_create(disk, "after-changes")?;
//!
//! // Query all snapshots
//! let snapshots = qcow2::snapshot_list(disk)?;
//! for snap in snapshots {
//!     if let Some(name) = snap.name {
//!         if let Some(date) = snap.date_sec {
//!             println!("Snapshot '{}' created at Unix time {}", name, date);
//!         }
//!     }
//! }
//!
//! // Clean up old snapshot
//! qcow2::snapshot_delete(disk, "before-changes")?;
//! # Ok::<(), qcow2::Qcow2Error>(())
//! ```

pub mod rhai;

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
///
/// Represents various errors that can occur during QCOW2 operations,
/// including command execution failures, IO errors, and validation errors.
#[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 new QCOW2 disk image at the specified path.
///
/// Creates a new QCOW2 disk image file with the specified virtual size.
/// The actual file size will be smaller due to QCOW2's sparse allocation.
/// The parent directory will be created if it doesn't exist.
///
/// # Arguments
///
/// * `path` - Full path where the QCOW2 image should be created
/// * `size_gb` - Virtual size of the disk in GiB (must be > 0)
///
/// # Returns
///
/// Returns the path to the created image on success
///
/// # Errors
///
/// Returns `Qcow2Error` if:
/// - `qemu-img` is not found on PATH
/// - `size_gb` is <= 0
/// - Parent directory cannot be created
/// - `qemu-img create` command fails
///
/// # Example
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// let path = qcow2::create("/tmp/my-disk.qcow2", 20)?;
/// println!("Created disk at: {}", path);
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
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())
}

/// Retrieve detailed information about a QCOW2 image.
///
/// Queries the QCOW2 image using `qemu-img info` and returns the result as JSON.
/// The returned object contains properties like virtual size, actual size, snapshots, etc.
///
/// # Arguments
///
/// * `path` - Path to the QCOW2 image file
///
/// # Returns
///
/// A JSON value containing image information including:
/// - `virtual-size`: Virtual size in bytes
/// - `disk-size`: Actual disk usage in bytes
/// - `format`: Image format (should be "qcow2")
/// - `snapshots`: Array of snapshots (if any)
///
/// # Errors
///
/// Returns `Qcow2Error` if:
/// - `qemu-img` is not found
/// - Image file doesn't exist
/// - Image is corrupted or not a valid QCOW2 file
/// - JSON parsing fails
///
/// # Example
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// let info = qcow2::info("/tmp/disk.qcow2")?;
/// if let Some(virtual_size) = info.get("virtual-size") {
///     println!("Virtual size: {} bytes", virtual_size);
/// }
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
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 a snapshot of a QCOW2 image (offline).
///
/// Creates a named snapshot of the QCOW2 image. Snapshots are useful for:
/// - Creating restore points before making changes
/// - Testing different configurations
/// - Backing up the system state
///
/// **Note**: The image must not be in use by a running VM when creating snapshots.
///
/// # Arguments
///
/// * `path` - Path to the QCOW2 image file
/// * `name` - Name for the snapshot (cannot be empty)
///
/// # Errors
///
/// Returns `Qcow2Error` if:
/// - `qemu-img` is not found
/// - Snapshot name is empty
/// - Image file doesn't exist or is corrupted
/// - `qemu-img snapshot` command fails
///
/// # Example
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// let disk = "/tmp/system.qcow2";
/// qcow2::create(disk, 30)?;
///
/// // Create a snapshot before system changes
/// qcow2::snapshot_create(disk, "clean-install")?;
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
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 from a QCOW2 image (offline).
///
/// Removes a named snapshot from the QCOW2 image. This operation only works
/// when the image is not in use by a running VM.
///
/// # Arguments
///
/// * `path` - Path to the QCOW2 image file
/// * `name` - Name of the snapshot to delete
///
/// # Errors
///
/// Returns `Qcow2Error` if:
/// - `qemu-img` is not found
/// - Snapshot name is empty
/// - Image file doesn't exist
/// - Snapshot doesn't exist
/// - `qemu-img snapshot` command fails
///
/// # Example
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// let disk = "/tmp/system.qcow2";
/// qcow2::create(disk, 30)?;
/// qcow2::snapshot_create(disk, "checkpoint")?;
///
/// // ... do some work ...
///
/// // Delete the snapshot when no longer needed
/// qcow2::snapshot_delete(disk, "checkpoint")?;
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
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(|_| ())
}

/// Represents a snapshot of a QCOW2 image.
///
/// This structure contains metadata about a snapshot created on a QCOW2 disk image,
/// including its name, creation date, and size information.
///
/// # Fields
///
/// * `id` - Snapshot ID (internal QEMU identifier)
/// * `name` - Human-readable snapshot name
/// * `vm_state_size` - Size of VM state data in bytes (if VM was running)
/// * `date_sec` - Unix timestamp (seconds) when snapshot was created
/// * `date_nsec` - Nanoseconds component of the timestamp
/// * `vm_clock_nsec` - VM clock value when snapshot was created
///
/// # Example
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// let snapshots = qcow2::snapshot_list("/tmp/disk.qcow2")?;
/// for snap in snapshots {
///     println!("Snapshot: {:?}", snap.name);
///     if let Some(date) = snap.date_sec {
///         println!("  Created at Unix time: {}", date);
///     }
///     if let Some(size) = snap.vm_state_size {
///         println!("  VM state size: {} bytes", size);
///     }
/// }
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
#[derive(Debug, Clone)]
pub struct Qcow2Snapshot {
    /// Snapshot ID (internal QEMU identifier)
    pub id: Option<String>,
    /// Human-readable name of the snapshot
    pub name: Option<String>,
    /// Size of saved VM state in bytes
    pub vm_state_size: Option<i64>,
    /// Unix timestamp (seconds) of creation
    pub date_sec: Option<i64>,
    /// Nanoseconds component of timestamp
    pub date_nsec: Option<i64>,
    /// VM clock value (nanoseconds)
    pub vm_clock_nsec: Option<i64>,
}

/// List all snapshots in a QCOW2 image (offline).
///
/// Retrieves all snapshots associated with the QCOW2 image.
/// Returns an empty vector if the image has no snapshots.
///
/// # Arguments
///
/// * `path` - Path to the QCOW2 image file
///
/// # Returns
///
/// A vector of `Qcow2Snapshot` structures, one for each snapshot in the image
///
/// # Errors
///
/// Returns `Qcow2Error` if:
/// - `qemu-img` is not found
/// - Image file doesn't exist
/// - Image is corrupted or not a valid QCOW2 file
/// - JSON parsing fails
///
/// # Example
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// let disk = "/tmp/system.qcow2";
/// qcow2::create(disk, 30)?;
/// qcow2::snapshot_create(disk, "snapshot1")?;
/// qcow2::snapshot_create(disk, "snapshot2")?;
///
/// let snapshots = qcow2::snapshot_list(disk)?;
/// println!("Found {} snapshots", snapshots.len());
/// for snap in snapshots {
///     if let Some(name) = snap.name {
///         println!("  - {}", name);
///     }
/// }
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
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 of building a base QCOW2 image from a cloud image.
///
/// Returned by `build_ubuntu_24_04_base()` to provide information about
/// the downloaded and configured base image.
///
/// # Fields
///
/// * `base_image_path` - Full path to the downloaded/resized image
/// * `snapshot` - Name of the base snapshot created (usually "base")
/// * `url` - Source URL of the cloud image
/// * `resized_to_gb` - Virtual size if resized, None if not resized
///
/// # Example
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// let result = qcow2::build_ubuntu_24_04_base("/var/vms/images", Some(100))?;
/// println!("Base image path: {}", result.base_image_path);
/// println!("Snapshot: {}", result.snapshot);
/// println!("Source URL: {}", result.url);
/// if let Some(size) = result.resized_to_gb {
///     println!("Resized to: {} GiB", size);
/// }
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
#[derive(Debug, Clone)]
pub struct BuildBaseResult {
    /// Full path to the base image file
    pub base_image_path: String,
    /// Name of the base snapshot (typically "base")
    pub snapshot: String,
    /// Source URL where the image was downloaded from
    pub url: String,
    /// Virtual size in GiB if resized, None otherwise
    pub resized_to_gb: Option<i64>,
}

/// Download and prepare an Ubuntu 24.04 cloud image as a QCOW2 base image.
///
/// This convenience function downloads the Canonical Ubuntu 24.04 (Noble) cloud image,
/// optionally resizes it to the specified size, and creates a "base" snapshot.
/// Cloud images are smaller than full VM images and come pre-configured.
///
/// If the image already exists at the destination, it will be reused (not re-downloaded).
///
/// # Arguments
///
/// * `dest_dir` - Directory where the image should be stored
/// * `size_gb` - Optional new virtual size in GiB. If None, uses the cloud image's default size.
///              If Some, resizes the image to the specified size (must be >= cloud image size).
///
/// # Returns
///
/// A `BuildBaseResult` containing:
/// - Path to the downloaded/resized image
/// - Name of the "base" snapshot that was created
/// - Original download URL
/// - Resized size if applicable
///
/// # Errors
///
/// Returns `Qcow2Error` if:
/// - `qemu-img` is not found
/// - Network download fails or image is corrupted
/// - Directory cannot be created
/// - Resize fails (e.g., size is too small)
/// - Snapshot creation fails
///
/// # Example: Create a base image and clone it for VMs
///
/// ```rust,no_run
/// use herolib_virt::qcow2;
///
/// // Download and resize Ubuntu 24.04 image to 100 GiB
/// let base = qcow2::build_ubuntu_24_04_base("/var/vms/images", Some(100))?;
/// println!("Base image ready at: {}", base.base_image_path);
///
/// // Now you can use this as a base image for cloning or snapshot-based VMs
/// # Ok::<(), qcow2::Qcow2Error>(())
/// ```
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),
    })
}
