//! Global smolvm configuration.
//!
//! This module handles persistent configuration storage for smolvm,
//! including default settings and VM registry.
//!
//! State is persisted to a SQLite database at `~/.local/share/smolvm/server/smolvm.db`.
//! For backward compatibility, `SmolvmConfig` maintains an in-memory cache of VMs
//! and provides the same API as the old confy-based implementation.

use crate::data::network;
use crate::data::resources::{DEFAULT_MICROVM_CPU_COUNT, DEFAULT_MICROVM_MEMORY_MIB};
use crate::db::SmolvmDb;
use crate::error::Result;
use crate::network::NetworkBackend;
use serde::{Deserialize, Serialize};
pub use smolvm_protocol::publish_socket::SocketDirection;
use std::collections::HashMap;

/// A user-published host↔guest Unix-socket bridge (`--expose-socket` /
/// `--mount-socket`), persisted on the VM record. The vsock port is assigned at
/// launch (not stored); only the paths and direction are durable.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PublishedSocketConfig {
    /// Bridge direction (`expose` = guest→host, `mount` = host→guest).
    pub direction: SocketDirection,
    /// Guest-side socket path: the existing app socket to expose, or the path a
    /// mounted host socket is created at inside the guest.
    pub guest_path: String,
    /// Host-side socket path. For `expose`, where the host-side socket is
    /// created (`None` → default to `<per-VM dir>/<basename of guest_path>`).
    /// For `mount`, the existing host socket to bridge in (required).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub host_path: Option<String>,
}

/// VM lifecycle state.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum RecordState {
    /// Container exists, VM not started.
    #[default]
    Created,
    /// VM process is running.
    Running,
    /// VM exited cleanly.
    Stopped,
    /// VM crashed or error.
    Failed,
    /// libkrun VMM process is alive but the guest agent is not
    /// responding to vsock pings. Typical cause: the agent crashed
    /// (OOM, panic, kernel issue) while the VMM stayed up — common
    /// aftermath of a workload that exhausted guest resources.
    /// `machine list` shows this so operators see the truth instead
    /// of a misleading "running"; `machine start` recovers by
    /// killing the zombie VMM and starting fresh.
    Unreachable,
    /// libkrun VMM process is alive but deliberately frozen as a fork
    /// base: `machine fork` snapshotted it and its clones' disk overlays
    /// are copy-on-write backed by its disks. Its guest agent is paused
    /// and never answers a vsock ping — so it is reported *without* a
    /// liveness probe (it would otherwise look identical to an
    /// `Unreachable` zombie) and is never reaped or auto-restarted: it
    /// must outlive its clones. Resolved on the fly when a record has
    /// dependent clones; not persisted.
    Frozen,
}

impl std::fmt::Display for RecordState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RecordState::Created => write!(f, "created"),
            RecordState::Running => write!(f, "running"),
            RecordState::Stopped => write!(f, "stopped"),
            RecordState::Failed => write!(f, "failed"),
            RecordState::Unreachable => write!(f, "unreachable"),
            RecordState::Frozen => write!(f, "frozen"),
        }
    }
}

/// Restart policy for a machine.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum RestartPolicy {
    /// Never restart the machine automatically.
    #[default]
    Never,
    /// Always restart the machine when it exits.
    Always,
    /// Restart only if the machine exited with a non-zero exit code.
    OnFailure,
    /// Restart unless the user explicitly stopped the machine.
    UnlessStopped,
}

impl std::fmt::Display for RestartPolicy {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RestartPolicy::Never => write!(f, "never"),
            RestartPolicy::Always => write!(f, "always"),
            RestartPolicy::OnFailure => write!(f, "on-failure"),
            RestartPolicy::UnlessStopped => write!(f, "unless-stopped"),
        }
    }
}

impl std::str::FromStr for RestartPolicy {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "never" => Ok(RestartPolicy::Never),
            "always" => Ok(RestartPolicy::Always),
            "on-failure" | "onfailure" => Ok(RestartPolicy::OnFailure),
            "unless-stopped" | "unlessstopped" => Ok(RestartPolicy::UnlessStopped),
            _ => Err(format!("invalid restart policy: {}", s)),
        }
    }
}

/// Restart configuration for a machine.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RestartConfig {
    /// The restart policy.
    #[serde(default)]
    pub policy: RestartPolicy,
    /// Maximum number of restart attempts (0 = unlimited).
    #[serde(default)]
    pub max_retries: u32,
    /// Maximum backoff duration in seconds (0 = use default 300s).
    #[serde(default)]
    pub max_backoff_secs: u64,
    /// Current restart count.
    #[serde(default)]
    pub restart_count: u32,
    /// Whether the user explicitly stopped this machine.
    #[serde(default)]
    pub user_stopped: bool,
}

impl RestartConfig {
    /// Determine whether the machine should be restarted based on the policy,
    /// exit code, and current restart count.
    pub fn should_restart(&self, exit_code: Option<i32>) -> bool {
        // An explicit user stop suppresses auto-restart for every policy until an
        // explicit start clears the flag — a machine the operator stopped must
        // stay stopped (matching `docker stop` on a restart-policy container),
        // not be resurrected by the supervisor. Only `unless-stopped` consulted
        // this flag before, so `always`/`on-failure` machines were un-stoppable.
        if self.user_stopped {
            return false;
        }
        // Check max retries limit (0 = unlimited)
        if self.max_retries > 0 && self.restart_count >= self.max_retries {
            return false;
        }
        match self.policy {
            RestartPolicy::Never => false,
            RestartPolicy::Always => true,
            RestartPolicy::OnFailure => exit_code != Some(0),
            RestartPolicy::UnlessStopped => true,
        }
    }

    /// Default maximum backoff duration in seconds (5 minutes).
    const DEFAULT_MAX_BACKOFF_SECS: u64 = 300;

    /// Maximum exponent for backoff calculation (2^8 = 256s).
    const MAX_BACKOFF_EXPONENT: u32 = 8;

    /// Calculate exponential backoff duration for the current restart count.
    ///
    /// Formula: 2^n seconds, capped at max_backoff_secs (default 300s).
    pub fn backoff_duration(&self) -> std::time::Duration {
        let max_secs = if self.max_backoff_secs > 0 {
            self.max_backoff_secs
        } else {
            Self::DEFAULT_MAX_BACKOFF_SECS
        };
        let exponent = self.restart_count.min(Self::MAX_BACKOFF_EXPONENT);
        std::time::Duration::from_secs(2u64.pow(exponent).min(max_secs))
    }
}

/// Global smolvm configuration with database-backed persistence.
///
/// This struct provides backward-compatible access to VM records while
/// using SQLite for ACID-compliant storage. The `vms` field is an in-memory
/// cache that is kept in sync with the database.
#[derive(Debug, Clone)]
pub struct SmolvmConfig {
    /// Database handle for persistence.
    db: SmolvmDb,
    /// Configuration format version.
    pub version: u8,
    /// Default number of vCPUs for new VMs.
    pub default_cpus: u8,
    /// Default memory in MiB for new VMs.
    pub default_mem: u32,
    /// Default DNS server for VMs with network egress.
    pub default_dns: String,
    /// Storage volume path (macOS only, for case-sensitive filesystem).
    #[cfg(target_os = "macos")]
    pub storage_volume: String,
    /// Registry of known VMs (by name) - in-memory cache.
    pub vms: HashMap<String, VmRecord>,
}

impl SmolvmConfig {
    /// Create a new configuration with default values.
    ///
    /// This is the fallible version of `Default::default()`. Use this when
    /// you need to handle database initialization errors.
    pub fn try_default() -> Result<Self> {
        Ok(Self {
            db: SmolvmDb::open()?,
            version: 1,
            default_cpus: DEFAULT_MICROVM_CPU_COUNT,
            default_mem: DEFAULT_MICROVM_MEMORY_MIB,
            default_dns: network::default_dns(),
            #[cfg(target_os = "macos")]
            storage_volume: String::new(),
            vms: HashMap::new(),
        })
    }
}

impl SmolvmConfig {
    /// Load configuration from the database.
    ///
    /// Opens the database and loads all config settings and VM records in a
    /// single database transaction (1 open/close cycle instead of 6).
    pub fn load() -> Result<Self> {
        let db = SmolvmDb::open()?;

        // Load all config + all VMs in one DB round-trip
        let (config_map, vms) = db.load_all()?;

        let version = config_map
            .get("version")
            .and_then(|s| s.parse().ok())
            .unwrap_or(1);
        let default_cpus = config_map
            .get("default_cpus")
            .and_then(|s| s.parse().ok())
            .unwrap_or(DEFAULT_MICROVM_CPU_COUNT);
        let default_mem = config_map
            .get("default_mem")
            .and_then(|s| s.parse().ok())
            .unwrap_or(DEFAULT_MICROVM_MEMORY_MIB);
        let default_dns = config_map
            .get("default_dns")
            .cloned()
            .unwrap_or_else(network::default_dns);

        #[cfg(target_os = "macos")]
        let storage_volume = config_map
            .get("storage_volume")
            .cloned()
            .unwrap_or_default();

        Ok(Self {
            db,
            version,
            default_cpus,
            default_mem,
            default_dns,
            #[cfg(target_os = "macos")]
            storage_volume,
            vms,
        })
    }

    /// Save global configuration to the database.
    ///
    /// Persists all global config settings in a single DB transaction
    /// (1 open/close cycle instead of 4). VM records are not saved here
    /// since writes are immediate via `update_vm()` and `insert_vm()`.
    pub fn save(&self) -> Result<()> {
        let version_str = self.version.to_string();
        let cpus_str = self.default_cpus.to_string();
        let mem_str = self.default_mem.to_string();

        #[cfg(not(target_os = "macos"))]
        let settings: Vec<(&str, &str)> = vec![
            ("version", version_str.as_str()),
            ("default_cpus", cpus_str.as_str()),
            ("default_mem", mem_str.as_str()),
            ("default_dns", self.default_dns.as_str()),
        ];

        #[cfg(target_os = "macos")]
        let settings: Vec<(&str, &str)> = {
            let mut s = vec![
                ("version", version_str.as_str()),
                ("default_cpus", cpus_str.as_str()),
                ("default_mem", mem_str.as_str()),
                ("default_dns", self.default_dns.as_str()),
            ];
            if !self.storage_volume.is_empty() {
                s.push(("storage_volume", self.storage_volume.as_str()));
            }
            s
        };

        self.db.save_config(&settings)
    }

    /// Insert a VM record (persists immediately to database).
    pub fn insert_vm(&mut self, name: String, record: VmRecord) -> Result<()> {
        self.db.insert_vm(&name, &record)?;
        self.vms.insert(name, record);
        Ok(())
    }

    /// Remove a VM from the registry.
    pub fn remove_vm(&mut self, id: &str) -> Option<VmRecord> {
        // Remove from database (ignore errors, just log)
        if let Err(e) = self.db.remove_vm(id) {
            tracing::warn!(error = %e, vm = %id, "failed to remove VM from database");
        }
        self.vms.remove(id)
    }

    /// Get a VM record by ID.
    pub fn get_vm(&self, id: &str) -> Option<&VmRecord> {
        self.vms.get(id)
    }

    /// List all VM records.
    pub fn list_vms(&self) -> impl Iterator<Item = (&String, &VmRecord)> {
        self.vms.iter()
    }

    /// Update a VM record in place (persists immediately to database).
    ///
    /// Returns `None` if the record doesn't exist, `Some(Err)` if the DB write
    /// fails, `Some(Ok)` on success. Callers that need fail-closed semantics
    /// should check both.
    pub fn update_vm<F>(&mut self, id: &str, f: F) -> Option<crate::Result<()>>
    where
        F: FnOnce(&mut VmRecord),
    {
        if let Some(record) = self.vms.get_mut(id) {
            f(record);
            // Persist to database
            Some(self.db.insert_vm(id, record))
        } else {
            None
        }
    }

    /// Get the underlying database handle.
    pub fn db(&self) -> &SmolvmDb {
        &self.db
    }
}

/// Record of a VM in the registry.
///
/// This stores machine configuration only. Container configuration
/// is managed separately via the container commands.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmRecord {
    /// VM name/ID.
    pub name: String,

    /// Creation timestamp (seconds since Unix epoch).
    #[serde(deserialize_with = "deserialize_timestamp", default)]
    pub created_at: u64,

    /// VM lifecycle state.
    #[serde(default)]
    pub state: RecordState,

    /// Process ID when running.
    #[serde(default)]
    pub pid: Option<i32>,

    /// Process start time (seconds since epoch) for PID verification.
    /// Used alongside PID to detect PID reuse by the OS.
    #[serde(default)]
    pub pid_start_time: Option<u64>,

    /// Number of vCPUs.
    #[serde(default = "default_cpus")]
    pub cpus: u8,

    /// Memory in MiB.
    #[serde(default = "default_mem")]
    pub mem: u32,

    /// Volume mounts (host_path, guest_path, read_only).
    #[serde(default)]
    pub mounts: Vec<(String, String, bool)>,

    /// Port mappings (host_port, guest_port).
    #[serde(default)]
    pub ports: Vec<(u16, u16)>,

    /// User-published Unix-socket bridges (`--expose-socket` / `--mount-socket`).
    #[serde(default)]
    pub published_sockets: Vec<PublishedSocketConfig>,

    /// Enable outbound network access (TSI).
    #[serde(default)]
    pub network: bool,

    /// Enable GPU acceleration (virtio-gpu with Venus/Vulkan).
    #[serde(default)]
    pub gpu: Option<bool>,

    /// GPU shared-memory region size in MiB. `None` → default
    /// (`DEFAULT_GPU_VRAM_MIB`). Ignored unless `gpu` is true.
    #[serde(default)]
    pub gpu_vram_mib: Option<u32>,

    /// Enable Rosetta 2 for x86_64 binary translation on Apple Silicon.
    #[serde(default)]
    pub rosetta: Option<bool>,

    /// Restart configuration.
    #[serde(default)]
    pub restart: RestartConfig,

    /// Last exit code from the VM process.
    #[serde(default)]
    pub last_exit_code: Option<i32>,

    /// Commands to run on first VM start (via `sh -c`).
    #[serde(default)]
    pub init: Vec<String>,

    /// Whether init commands have already completed successfully.
    /// Set to true after first successful run; reset when init commands change.
    #[serde(default)]
    pub init_completed: bool,

    /// Environment variables for init commands.
    #[serde(default)]
    pub env: Vec<(String, String)>,

    /// Secret references declared by a Smolfile `[secrets]` section, keyed
    /// by the guest-side env var name. Resolved to plaintext at each VM
    /// start (and for `machine exec`) and appended to the env vector — the
    /// plaintext values never touch this record or the DB.
    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
    pub secret_refs: std::collections::BTreeMap<String, crate::secrets::SecretRef>,

    /// Working directory for the container workload.
    #[serde(default)]
    pub workdir: Option<String>,

    /// Container user (UID or username). Resolved from image metadata at first
    /// launch and stored so restart can replay the same identity.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub user: Option<String>,

    /// Storage disk size in GiB (None = default 20 GiB).
    #[serde(default)]
    pub storage_gb: Option<u64>,

    /// Overlay disk size in GiB (None = default 10 GiB).
    #[serde(default)]
    pub overlay_gb: Option<u64>,

    /// Allowed egress CIDR ranges. None = unrestricted, Some([]) = deny all.
    #[serde(default)]
    pub allowed_cidrs: Option<Vec<String>>,

    /// Preferred network backend override for machine launch.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub network_backend: Option<NetworkBackend>,

    /// Custom DNS resolver for the guest. None = backend default.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dns: Option<std::net::Ipv4Addr>,

    /// OCI image for auto-container creation on start.
    #[serde(default)]
    pub image: Option<String>,

    /// Entrypoint for the container.
    #[serde(default)]
    pub entrypoint: Vec<String>,

    /// Default command for the container.
    #[serde(default)]
    pub cmd: Vec<String>,

    /// Health check command (run inside VM to verify workload is healthy).
    #[serde(default)]
    pub health_cmd: Option<Vec<String>>,

    /// Health check interval in seconds.
    #[serde(default)]
    pub health_interval_secs: Option<u64>,

    /// Health check timeout in seconds.
    #[serde(default)]
    pub health_timeout_secs: Option<u64>,

    /// Health check failure threshold before marking unhealthy.
    #[serde(default)]
    pub health_retries: Option<u32>,

    /// Grace period in seconds before health checks start after boot.
    #[serde(default)]
    pub health_startup_grace_secs: Option<u64>,

    /// Enable SSH agent forwarding into the VM.
    #[serde(default)]
    pub ssh_agent: bool,

    /// Enable CUDA-over-vsock: smolvm starts a host CUDA server and remotes the
    /// guest's CUDA Driver-API calls to the host NVIDIA GPU.
    #[serde(default)]
    pub cuda: bool,

    /// Planned number of runnable CUDA fork clones. Persisted so every clone
    /// receives the same pre-initialization VRAM policy as its golden.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cuda_fork_pool_size: Option<u32>,

    /// Explicit logical VRAM limit applied to the golden and every clone.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cuda_vram_limit_mib: Option<u64>,

    /// Expose the guest's Docker daemon socket to the host as a Unix socket in
    /// the VM data dir, so a host client can drive it with `DOCKER_HOST=unix://…`.
    #[serde(default)]
    pub docker_socket: bool,

    /// Hostnames for DNS filtering. When set, the guest DNS proxy filters
    /// queries against this allowlist.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dns_filter_hosts: Option<Vec<String>>,

    /// True for `machine run` VMs. Auto-deleted on exit or cleanup sweep.
    #[serde(default)]
    pub ephemeral: bool,

    /// Absolute path to the .smolmachine sidecar this machine was created from.
    /// When set, `machine start` extracts layers from the sidecar and mounts
    /// them via virtiofs instead of pulling the image from a registry.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source_smolmachine: Option<String>,

    /// Name of the golden VM this machine was forked from, if any. A clone's
    /// block disks are copy-on-write overlays backed by the golden's disks, so
    /// the golden must outlive its clones. The disk *format* is not recorded
    /// here — it is derived from the on-disk file (`.qcow2` vs `.raw`), which is
    /// the single source of truth (see `agent::resolve_disk_image`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub golden: Option<String>,

    /// Whether a fork clone is still parked at the inherited workload
    /// forkpoint. Held clones are clean, already-booted pool slots: a caller
    /// installs the job-specific fork parameters and releases each slot once.
    /// A released training clone is disposable and must never be marked held
    /// again because its optimizer, RNG, and dataset state may have changed.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub forkpoint_held: bool,

    /// Parameters delivered through `/etc/smolvm/fork-env` for this clone.
    /// Kept separately from the machine's ordinary environment so a held slot
    /// can merge assignment-time values without copying unrelated golden env
    /// entries into the workload-facing parameter file.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub fork_env: Vec<(String, String)>,

    /// Set for machines created by the Kubernetes containerd shim (pod
    /// sandboxes). Scopes node-reboot reconciliation (see
    /// `control::reconcile_runtime_machines`) so it reclaims only shim-managed
    /// VMs whose process died with a crash or reboot, never a user's persistent
    /// CLI/SDK machine.
    #[serde(default)]
    pub runtime_managed: bool,
}

/// Deserialize `created_at` from either a legacy JSON string `"1705312345"` or
/// the current integer `1705312345`. Old DB records stored it as a string.
fn deserialize_timestamp<'de, D>(deserializer: D) -> std::result::Result<u64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::Deserialize;
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum StrOrU64 {
        Str(String),
        U64(u64),
    }
    match StrOrU64::deserialize(deserializer)? {
        StrOrU64::U64(n) => Ok(n),
        StrOrU64::Str(s) => s.parse::<u64>().map_err(serde::de::Error::custom),
    }
}

fn default_cpus() -> u8 {
    1
}

fn default_mem() -> u32 {
    512
}

impl VmRecord {
    /// Create a new VM record.
    pub fn new(
        name: String,
        cpus: u8,
        mem: u32,
        mounts: Vec<(String, String, bool)>,
        ports: Vec<(u16, u16)>,
        network: bool,
    ) -> Self {
        Self {
            name,
            created_at: crate::util::current_timestamp(),
            state: RecordState::Created,
            pid: None,
            pid_start_time: None,
            cpus,
            mem,
            mounts,
            ports,
            published_sockets: Vec::new(),
            network,
            gpu: None,
            gpu_vram_mib: None,
            rosetta: None,
            restart: RestartConfig::default(),
            last_exit_code: None,
            init: Vec::new(),
            init_completed: false,
            env: Vec::new(),
            secret_refs: std::collections::BTreeMap::new(),
            workdir: None,
            user: None,
            storage_gb: None,
            overlay_gb: None,
            allowed_cidrs: None,
            network_backend: None,
            dns: None,
            image: None,
            entrypoint: Vec::new(),
            cmd: Vec::new(),
            health_cmd: None,
            health_interval_secs: None,
            health_timeout_secs: None,
            health_retries: None,
            health_startup_grace_secs: None,
            ssh_agent: false,
            cuda: false,
            cuda_fork_pool_size: None,
            cuda_vram_limit_mib: None,
            docker_socket: false,
            dns_filter_hosts: None,
            ephemeral: false,
            source_smolmachine: None,
            golden: None,
            forkpoint_held: false,
            fork_env: Vec::new(),
            runtime_managed: false,
        }
    }

    /// Create a new VM record with restart configuration.
    pub fn new_with_restart(
        name: String,
        cpus: u8,
        mem: u32,
        mounts: Vec<(String, String, bool)>,
        ports: Vec<(u16, u16)>,
        network: bool,
        restart: RestartConfig,
    ) -> Self {
        Self {
            name,
            created_at: crate::util::current_timestamp(),
            state: RecordState::Created,
            pid: None,
            pid_start_time: None,
            cpus,
            mem,
            mounts,
            ports,
            published_sockets: Vec::new(),
            network,
            gpu: None,
            gpu_vram_mib: None,
            rosetta: None,
            restart,
            last_exit_code: None,
            init: Vec::new(),
            init_completed: false,
            env: Vec::new(),
            secret_refs: std::collections::BTreeMap::new(),
            workdir: None,
            user: None,
            storage_gb: None,
            overlay_gb: None,
            allowed_cidrs: None,
            network_backend: None,
            dns: None,
            image: None,
            entrypoint: Vec::new(),
            cmd: Vec::new(),
            health_cmd: None,
            health_interval_secs: None,
            health_timeout_secs: None,
            health_retries: None,
            health_startup_grace_secs: None,
            ssh_agent: false,
            cuda: false,
            cuda_fork_pool_size: None,
            cuda_vram_limit_mib: None,
            docker_socket: false,
            dns_filter_hosts: None,
            ephemeral: false,
            source_smolmachine: None,
            golden: None,
            forkpoint_held: false,
            fork_env: Vec::new(),
            runtime_managed: false,
        }
    }

    /// Check if the VM process is still alive.
    ///
    /// Uses start time verification to detect PID reuse by the OS.
    /// Falls back to PID-only check for legacy records without start time.
    pub fn is_process_alive(&self) -> bool {
        if let Some(pid) = self.pid {
            crate::process::is_our_process(pid, self.pid_start_time)
        } else {
            false
        }
    }

    /// Get the actual state, checking if running process is still alive.
    pub fn actual_state(&self) -> RecordState {
        if self.state == RecordState::Running {
            if self.is_process_alive() {
                RecordState::Running
            } else {
                RecordState::Stopped // Process died
            }
        } else {
            self.state.clone()
        }
    }

    /// Convert stored mounts to HostMount format.
    pub fn host_mounts(&self) -> Vec<crate::data::storage::HostMount> {
        self.mounts
            .iter()
            .map(|(host, guest, ro)| crate::data::storage::HostMount {
                source: std::path::PathBuf::from(host),
                target: std::path::PathBuf::from(guest),
                read_only: *ro,
            })
            .collect()
    }

    /// Convert stored ports to PortMapping format.
    pub fn port_mappings(&self) -> Vec<crate::data::network::PortMapping> {
        self.ports
            .iter()
            .map(|(host, guest)| crate::data::network::PortMapping::new(*host, *guest))
            .collect()
    }

    /// Convert record fields to VmResources.
    pub fn vm_resources(&self) -> crate::agent::VmResources {
        crate::agent::VmResources {
            cpus: self.cpus,
            memory_mib: self.mem,
            network: self.network,
            network_backend: self.network_backend,
            gpu: self.gpu.unwrap_or(false),
            gpu_vram_mib: self.gpu_vram_mib,
            cuda: self.cuda,
            rosetta: self.rosetta.unwrap_or(false),
            storage_gib: self.storage_gb,
            overlay_gib: self.overlay_gb,
            allowed_cidrs: self.allowed_cidrs.clone(),
            dns: self.dns,
        }
    }
}

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

    #[test]
    fn test_vm_record_serialization() {
        let record = VmRecord::new(
            "test".to_string(),
            2,
            512,
            vec![("/host".to_string(), "/guest".to_string(), false)],
            vec![(8080, 80)],
            false,
        );

        let json = serde_json::to_string(&record).unwrap();
        let deserialized: VmRecord = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.name, record.name);
        assert_eq!(deserialized.mounts, record.mounts);
    }

    #[test]
    fn test_vm_record_secret_refs_roundtrip() {
        use crate::secrets::SecretRef;
        let mut record = VmRecord::new("r".into(), 1, 256, vec![], vec![], false);
        record.secret_refs.insert(
            "TLS_KEY".into(),
            SecretRef {
                from_env: None,
                from_file: Some("/run/secrets/tls.key".into()),
            },
        );
        record.secret_refs.insert(
            "DB_URL".into(),
            SecretRef {
                from_env: Some("PROD_DB".into()),
                from_file: None,
            },
        );

        let json = serde_json::to_string(&record).unwrap();
        // Ref metadata — not sensitive — roundtrips through serde_json.
        assert!(json.contains("TLS_KEY"));
        assert!(json.contains("PROD_DB"));

        let back: VmRecord = serde_json::from_str(&json).unwrap();
        assert_eq!(back.secret_refs.len(), 2);
        assert_eq!(
            back.secret_refs["TLS_KEY"]
                .from_file
                .as_ref()
                .map(|p| p.to_string_lossy().into_owned()),
            Some("/run/secrets/tls.key".to_string())
        );
        assert_eq!(
            back.secret_refs["DB_URL"].from_env.as_deref(),
            Some("PROD_DB")
        );
    }

    #[test]
    fn test_vm_record_with_restart() {
        let restart = RestartConfig {
            policy: RestartPolicy::Always,
            max_retries: 5,
            ..Default::default()
        };
        let record =
            VmRecord::new_with_restart("test".to_string(), 2, 512, vec![], vec![], false, restart);

        let json = serde_json::to_string(&record).unwrap();
        let deserialized: VmRecord = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.restart.policy, RestartPolicy::Always);
        assert_eq!(deserialized.restart.max_retries, 5);
    }

    #[test]
    fn test_record_state_display() {
        assert_eq!(RecordState::Created.to_string(), "created");
        assert_eq!(RecordState::Running.to_string(), "running");
        assert_eq!(RecordState::Stopped.to_string(), "stopped");
        assert_eq!(RecordState::Failed.to_string(), "failed");
    }

    #[test]
    fn test_restart_policy_display_and_parse() {
        assert_eq!(RestartPolicy::Never.to_string(), "never");
        assert_eq!(RestartPolicy::Always.to_string(), "always");
        assert_eq!(RestartPolicy::OnFailure.to_string(), "on-failure");
        assert_eq!(RestartPolicy::UnlessStopped.to_string(), "unless-stopped");

        assert_eq!(
            "never".parse::<RestartPolicy>().unwrap(),
            RestartPolicy::Never
        );
        assert_eq!(
            "always".parse::<RestartPolicy>().unwrap(),
            RestartPolicy::Always
        );
        assert_eq!(
            "on-failure".parse::<RestartPolicy>().unwrap(),
            RestartPolicy::OnFailure
        );
        assert_eq!(
            "unless-stopped".parse::<RestartPolicy>().unwrap(),
            RestartPolicy::UnlessStopped
        );
    }

    #[test]
    fn test_restart_policy_serialization() {
        let policy = RestartPolicy::OnFailure;
        let json = serde_json::to_string(&policy).unwrap();
        assert_eq!(json, "\"on-failure\"");

        let deserialized: RestartPolicy = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, RestartPolicy::OnFailure);
    }

    #[test]
    fn test_restart_config_default() {
        let config = RestartConfig::default();
        assert_eq!(config.policy, RestartPolicy::Never);
        assert_eq!(config.max_retries, 0);
        assert_eq!(config.restart_count, 0);
        assert!(!config.user_stopped);
    }

    #[test]
    fn test_should_restart() {
        // (policy, max_retries, restart_count, user_stopped, last_exit_code, expected, desc)
        let cases = [
            (
                RestartPolicy::Never,
                0,
                0,
                false,
                None,
                false,
                "never policy",
            ),
            (
                RestartPolicy::Always,
                0,
                5,
                false,
                None,
                true,
                "always policy",
            ),
            (
                RestartPolicy::Always,
                3,
                3,
                false,
                None,
                false,
                "max retries reached",
            ),
            (
                RestartPolicy::Always,
                3,
                2,
                false,
                None,
                true,
                "under max retries",
            ),
            (
                RestartPolicy::OnFailure,
                0,
                0,
                false,
                Some(1),
                true,
                "on-failure non-zero exit",
            ),
            (
                RestartPolicy::OnFailure,
                0,
                0,
                false,
                Some(0),
                false,
                "on-failure clean exit",
            ),
            (
                RestartPolicy::OnFailure,
                0,
                0,
                false,
                None,
                true,
                "on-failure unknown exit",
            ),
            (
                RestartPolicy::UnlessStopped,
                0,
                0,
                false,
                None,
                true,
                "unless-stopped running",
            ),
            (
                RestartPolicy::UnlessStopped,
                0,
                0,
                true,
                None,
                false,
                "unless-stopped user stopped",
            ),
            // An explicit user stop suppresses restart for every policy, not just
            // unless-stopped — otherwise always/on-failure machines can't be stopped.
            (
                RestartPolicy::Always,
                0,
                0,
                true,
                None,
                false,
                "always but user stopped",
            ),
            (
                RestartPolicy::OnFailure,
                0,
                0,
                true,
                Some(1),
                false,
                "on-failure non-zero but user stopped",
            ),
        ];

        for (policy, max_retries, restart_count, user_stopped, last_exit_code, expected, desc) in
            cases
        {
            let config = RestartConfig {
                policy,
                max_retries,
                restart_count,
                user_stopped,
                ..Default::default()
            };
            assert_eq!(config.should_restart(last_exit_code), expected, "{}", desc);
        }
    }

    #[test]
    fn test_backoff_duration() {
        use std::time::Duration;
        let make = |count| RestartConfig {
            restart_count: count,
            ..Default::default()
        };
        assert_eq!(make(0).backoff_duration(), Duration::from_secs(1));
        assert_eq!(make(1).backoff_duration(), Duration::from_secs(2));
        assert_eq!(make(2).backoff_duration(), Duration::from_secs(4));
        assert_eq!(make(3).backoff_duration(), Duration::from_secs(8));
        assert_eq!(make(8).backoff_duration(), Duration::from_secs(256));
        // Exponent capped at 8 → 256s for any count >= 8
        assert_eq!(make(9).backoff_duration(), Duration::from_secs(256));
        assert_eq!(make(100).backoff_duration(), Duration::from_secs(256));
    }

    #[test]
    fn test_backoff_duration_respects_max_backoff() {
        use std::time::Duration;
        let config = RestartConfig {
            restart_count: 8,
            max_backoff_secs: 30,
            ..Default::default()
        };
        // 2^8 = 256, but capped at 30s
        assert_eq!(config.backoff_duration(), Duration::from_secs(30));
    }

    // ========================================================================
    // Resize-related tests
    // ========================================================================

    #[test]
    fn test_vm_record_storage_overlay_fields() {
        // Test that storage_gb and overlay_gb fields work correctly
        let mut record = VmRecord::new("test-vm".to_string(), 1, 512, vec![], vec![], false);

        // Initially None (uses defaults)
        assert!(record.storage_gb.is_none());
        assert!(record.overlay_gb.is_none());

        // Set storage_gb
        record.storage_gb = Some(50);
        assert_eq!(record.storage_gb, Some(50));

        // Set overlay_gb
        record.overlay_gb = Some(20);
        assert_eq!(record.overlay_gb, Some(20));
    }

    #[test]
    fn test_vm_record_partial_update() {
        // Test that we can update only some fields (partial update pattern)
        let mut record = VmRecord::new("test-vm".to_string(), 1, 512, vec![], vec![], false);
        record.storage_gb = Some(20);
        record.overlay_gb = Some(10);

        // Simulate partial update - only storage changes
        let new_storage_gb: Option<u64> = Some(50);
        let new_overlay_gb: Option<u64> = None;

        if let Some(s) = new_storage_gb {
            record.storage_gb = Some(s);
        }
        if let Some(o) = new_overlay_gb {
            record.overlay_gb = Some(o);
        }

        assert_eq!(record.storage_gb, Some(50));
        assert_eq!(record.overlay_gb, Some(10)); // Unchanged
    }

    #[test]
    fn test_vm_record_vm_resources_includes_storage() {
        // Test that vm_resources() includes storage_gb and overlay_gb
        let mut record = VmRecord::new("test-vm".to_string(), 2, 1024, vec![], vec![], false);
        record.storage_gb = Some(50);
        record.overlay_gb = Some(20);

        let resources = record.vm_resources();
        assert_eq!(resources.cpus, 2);
        assert_eq!(resources.memory_mib, 1024);
        assert_eq!(resources.storage_gib, Some(50));
        assert_eq!(resources.overlay_gib, Some(20));
    }

    #[test]
    fn test_vm_record_serialization_with_storage_overlay() {
        // Test that storage_gb and overlay_gb serialize/deserialize correctly
        let mut record = VmRecord::new("test-vm".to_string(), 1, 512, vec![], vec![], false);
        record.storage_gb = Some(50);
        record.overlay_gb = Some(20);

        let json = serde_json::to_string(&record).unwrap();

        // Verify fields are in JSON
        assert!(json.contains("storage_gb"));
        assert!(json.contains("overlay_gb"));
        assert!(json.contains("50"));
        assert!(json.contains("20"));

        let deserialized: VmRecord = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.storage_gb, Some(50));
        assert_eq!(deserialized.overlay_gb, Some(20));
    }

    #[test]
    fn test_vm_record_gpu_field() {
        // GPU defaults to None (not set)
        let record = VmRecord::new("test".to_string(), 2, 1024, vec![], vec![], false);
        assert_eq!(record.gpu, None);
        assert!(!record.vm_resources().gpu);

        // GPU set to true
        let mut record = VmRecord::new("test".to_string(), 2, 1024, vec![], vec![], false);
        record.gpu = Some(true);
        assert!(record.vm_resources().gpu);

        // GPU serializes/deserializes
        let json = serde_json::to_string(&record).unwrap();
        let deserialized: VmRecord = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.gpu, Some(true));

        // New records default to gpu = None → vm_resources().gpu = false
        let default_record = VmRecord::new("default".to_string(), 1, 512, vec![], vec![], false);
        assert_eq!(default_record.gpu, None);
        assert!(!default_record.vm_resources().gpu);
    }

    #[test]
    fn cuda_fork_capacity_policy_roundtrips_and_defaults_absent() {
        let mut record = VmRecord::new("cuda-pool".to_string(), 4, 4096, vec![], vec![], false);
        record.cuda = true;
        record.cuda_fork_pool_size = Some(4);
        record.cuda_vram_limit_mib = Some(10240);

        let encoded = serde_json::to_vec(&record).unwrap();
        let decoded: VmRecord = serde_json::from_slice(&encoded).unwrap();
        assert_eq!(decoded.cuda_fork_pool_size, Some(4));
        assert_eq!(decoded.cuda_vram_limit_mib, Some(10240));

        let mut legacy_value = serde_json::to_value(VmRecord::new(
            "legacy".to_string(),
            1,
            512,
            vec![],
            vec![],
            false,
        ))
        .unwrap();
        let legacy_object = legacy_value.as_object_mut().unwrap();
        legacy_object.remove("cuda_fork_pool_size");
        legacy_object.remove("cuda_vram_limit_mib");
        let legacy: VmRecord = serde_json::from_value(legacy_value).unwrap();
        assert_eq!(legacy.cuda_fork_pool_size, None);
        assert_eq!(legacy.cuda_vram_limit_mib, None);
    }

    #[test]
    fn held_fork_state_roundtrips_and_legacy_records_default_released() {
        let mut record = VmRecord::new("slot-0".to_string(), 2, 1024, vec![], vec![], false);
        record.golden = Some("golden".to_string());
        record.forkpoint_held = true;
        record.fork_env = vec![("SMOLVM_FORK_INDEX".to_string(), "0".to_string())];

        let encoded = serde_json::to_value(&record).unwrap();
        let decoded: VmRecord = serde_json::from_value(encoded.clone()).unwrap();
        assert!(decoded.forkpoint_held);
        assert_eq!(decoded.fork_env, record.fork_env);

        let mut legacy_value = encoded;
        let legacy_object = legacy_value.as_object_mut().unwrap();
        legacy_object.remove("forkpoint_held");
        legacy_object.remove("fork_env");
        let legacy: VmRecord = serde_json::from_value(legacy_value).unwrap();
        assert!(!legacy.forkpoint_held);
        assert!(legacy.fork_env.is_empty());
    }

    #[test]
    fn vm_record_gpu_vram_mib_flows_through_full_persistence_cycle() {
        // End-to-end plumbing test:
        //   CreateVmParams-like assignment → VmRecord → serde_json (DB)
        //   → deserialized VmRecord → vm_resources() → effective
        //
        // This is the chain that runs every time a user creates a
        // machine with `--gpu-vram N`, stops it, and starts it again.
        // A silent break anywhere in the chain (e.g., someone drops
        // the assignment, adds a new field and forgets to copy it,
        // changes the Option<u32> shape) fires this test.

        use crate::agent::VmResources;

        // 1. Start with a record, set the field the way `create_vm` does.
        let mut record = VmRecord::new("vramtest".into(), 2, 1024, vec![], vec![], false);
        record.gpu = Some(true);
        record.gpu_vram_mib = Some(1024);

        // 2. Roundtrip through JSON (the redb value format).
        let json = serde_json::to_vec(&record).unwrap();
        let back: VmRecord = serde_json::from_slice(&json).unwrap();
        assert_eq!(
            back.gpu_vram_mib,
            Some(1024),
            "gpu_vram_mib must survive DB roundtrip"
        );

        // 3. Convert to VmResources the way `start_vm_named` does.
        let res: VmResources = back.vm_resources();
        assert_eq!(res.gpu_vram_mib, Some(1024));
        assert_eq!(
            res.effective_gpu_vram_mib(),
            1024,
            "launcher will pass 1024 MiB to krun_set_gpu_options2"
        );

        // 4. And the unset path: default in, default out.
        let mut record = VmRecord::new("vramdefault".into(), 1, 512, vec![], vec![], false);
        record.gpu = Some(true);
        // gpu_vram_mib left as None
        let json = serde_json::to_vec(&record).unwrap();
        let back: VmRecord = serde_json::from_slice(&json).unwrap();
        assert_eq!(back.gpu_vram_mib, None);
        assert_eq!(
            back.vm_resources().effective_gpu_vram_mib(),
            crate::data::resources::DEFAULT_GPU_VRAM_MIB,
        );
    }
}
