//! VM Service implementation

use crate::artifact_store::ArtifactStore;
use crate::config::{DefaultHypervisor, ServerConfig};
use crate::prismnet_client::PrismNETClient;
use crate::storage::{FileStore, FlareDBStore, ImageUploadSession, StorageBackend, VmStore};
use crate::volume_manager::VolumeManager;
use crate::watcher::StateSink;
use creditservice_client::{Client as CreditServiceClient, ResourceType as CreditResourceType};
use dashmap::DashMap;
use iam_client::client::IamClientConfig;
use iam_client::IamClient;
use iam_service_auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
    TenantContext,
};
use iam_types::{PolicyBinding, PrincipalKind, PrincipalRef, Scope};
use plasmavmc_api::proto::{
    disk_source::Source as ProtoDiskSourceKind, image_service_server::ImageService,
    node_service_server::NodeService, vm_service_client::VmServiceClient,
    vm_service_server::VmService, volume_service_server::VolumeService, AbortImageUploadRequest,
    Architecture as ProtoArchitecture, AttachDiskRequest, AttachNicRequest,
    BeginImageUploadRequest, BeginImageUploadResponse, CephRbdBacking, CompleteImageUploadRequest,
    CordonNodeRequest, CreateImageRequest, CreateVmRequest, CreateVolumeRequest,
    DeleteImageRequest, DeleteVmRequest, DeleteVolumeRequest, DetachDiskRequest, DetachNicRequest,
    DiskBus as ProtoDiskBus, DiskCache as ProtoDiskCache, DiskSource as ProtoDiskSource,
    DrainNodeRequest, Empty, GetImageRequest, GetNodeRequest, GetVmRequest, GetVolumeRequest,
    HeartbeatNodeRequest, HypervisorType as ProtoHypervisorType, Image as ProtoImage,
    ImageFormat as ProtoImageFormat, ImageStatus as ProtoImageStatus, ListImagesRequest,
    ListImagesResponse, ListNodesRequest, ListNodesResponse, ListVmsRequest, ListVmsResponse,
    ListVolumesRequest, ListVolumesResponse, ManagedVolumeBacking, MigrateVmRequest,
    NicModel as ProtoNicModel, Node as ProtoNode, NodeCapacity as ProtoNodeCapacity,
    NodeState as ProtoNodeState, OsType as ProtoOsType, PrepareVmMigrationRequest, RebootVmRequest,
    RecoverVmRequest, RegisterExternalVolumeRequest, ResetVmRequest, ResizeVolumeRequest,
    StartVmRequest, StopVmRequest, UncordonNodeRequest, UpdateImageRequest, UpdateVmRequest,
    UploadImagePartRequest, UploadImagePartResponse, VirtualMachine, Visibility as ProtoVisibility,
    VmEvent, VmEventType as ProtoVmEventType, VmSpec as ProtoVmSpec, VmState as ProtoVmState,
    VmStatus as ProtoVmStatus, Volume as ProtoVolume, VolumeBacking as ProtoVolumeBacking,
    VolumeDriverKind as ProtoVolumeDriverKind, VolumeFormat as ProtoVolumeFormat,
    VolumeStatus as ProtoVolumeStatus, WatchVmRequest,
};
use plasmavmc_hypervisor::HypervisorRegistry;
use plasmavmc_types::{
    Architecture, DiskBus, DiskCache, DiskSource, HypervisorType, Image, ImageFormat, ImageStatus,
    NetworkSpec, NicModel, Node, NodeCapacity, NodeId, NodeState, OsType, Visibility, VmId,
    VmState, Volume, VolumeBacking, VolumeDriverKind, VolumeFormat, VolumeStatus,
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio_stream::wrappers::ReceiverStream;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::{Request, Response, Status};
use uuid::Uuid;

const ACTION_VM_CREATE: &str = "compute:instances:create";
const ACTION_VM_READ: &str = "compute:instances:read";
const ACTION_VM_LIST: &str = "compute:instances:list";
const ACTION_VM_UPDATE: &str = "compute:instances:update";
const ACTION_VM_DELETE: &str = "compute:instances:delete";
const ACTION_IMAGE_CREATE: &str = "compute:images:create";
const ACTION_IMAGE_READ: &str = "compute:images:read";
const ACTION_IMAGE_LIST: &str = "compute:images:list";
const ACTION_IMAGE_UPDATE: &str = "compute:images:update";
const ACTION_IMAGE_DELETE: &str = "compute:images:delete";
const ACTION_VOLUME_CREATE: &str = "compute:volumes:create";
const ACTION_VOLUME_READ: &str = "compute:volumes:read";
const ACTION_VOLUME_LIST: &str = "compute:volumes:list";
const ACTION_VOLUME_UPDATE: &str = "compute:volumes:update";
const ACTION_VOLUME_DELETE: &str = "compute:volumes:delete";
const NODE_ENDPOINT_LABEL: &str = "plasmavmc_endpoint";
const FAILOVER_META_KEY: &str = "failover_at";
const FAILOVER_TARGET_KEY: &str = "failover_target";
const PRISMNET_VM_DEVICE_TYPE: i32 = prismnet_api::proto::DeviceType::Vm as i32;
const PRISMNET_AUTO_PORTS_METADATA_KEY: &str = "plasmavmc.prismnet.auto_ports";
const STORE_OP_TIMEOUT: Duration = Duration::from_secs(5);

/// VM Service implementation
#[derive(Clone)]
pub struct VmServiceImpl {
    /// Hypervisor registry
    hypervisor_registry: Arc<HypervisorRegistry>,
    vms: DashMap<TenantKey, plasmavmc_types::VirtualMachine>,
    images: DashMap<TenantKey, Image>,
    handles: DashMap<TenantKey, plasmavmc_types::VmHandle>,
    nodes: DashMap<String, Node>,
    /// Storage backend
    store: Arc<dyn VmStore>,
    /// PrismNET endpoint (optional)
    prismnet_endpoint: Option<String>,
    /// CreditService client (optional, for admission control)
    credit_service: Option<Arc<RwLock<CreditServiceClient>>>,
    /// Local node identifier (optional)
    local_node_id: Option<String>,
    shared_live_migration: bool,
    default_hypervisor: HypervisorType,
    watch_poll_interval: Duration,
    auto_restart: bool,
    failover_enabled: bool,
    failover_min_interval_secs: u64,
    artifact_store: Option<Arc<ArtifactStore>>,
    volume_manager: Arc<VolumeManager>,
    iam_client: Arc<IamClient>,
    auth: Arc<AuthService>,
}

#[derive(Clone, Eq)]
struct TenantKey {
    org_id: String,
    project_id: String,
    vm_id: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct AutoPrismnetPort {
    nic_id: String,
    subnet_id: String,
    port_id: String,
}

impl PartialEq for TenantKey {
    fn eq(&self, other: &Self) -> bool {
        self.org_id == other.org_id
            && self.project_id == other.project_id
            && self.vm_id == other.vm_id
    }
}

impl Hash for TenantKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.org_id.hash(state);
        self.project_id.hash(state);
        self.vm_id.hash(state);
    }
}

impl TenantKey {
    fn new(
        org_id: impl Into<String>,
        project_id: impl Into<String>,
        vm_id: impl Into<String>,
    ) -> Self {
        Self {
            org_id: org_id.into(),
            project_id: project_id.into(),
            vm_id: vm_id.into(),
        }
    }
}

fn handle_qmp_socket_exists(handle: &plasmavmc_types::VmHandle) -> bool {
    let qmp_socket = handle
        .backend_state
        .get("qmp_socket")
        .cloned()
        .unwrap_or_else(|| format!("{}/qmp.sock", handle.runtime_dir));
    std::path::Path::new(&qmp_socket).exists()
}

impl VmServiceImpl {
    /// Create a new VM service
    pub async fn new(
        hypervisor_registry: Arc<HypervisorRegistry>,
        auth: Arc<AuthService>,
        iam_endpoint: impl Into<String>,
        config: &ServerConfig,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let backend = StorageBackend::from_config(&config.storage);
        let store: Arc<dyn VmStore> = match backend {
            StorageBackend::FlareDB => match FlareDBStore::new_with_config(&config.storage).await {
                Ok(flaredb_store) => Arc::new(flaredb_store),
                Err(e) => {
                    tracing::warn!(
                        "Failed to connect to FlareDB, falling back to file storage: {}",
                        e
                    );
                    Arc::new(FileStore::new(config.storage.state_path.clone()))
                }
            },
            StorageBackend::File => {
                let file_store = FileStore::new(config.storage.state_path.clone());
                Arc::new(file_store)
            }
        };

        let prismnet_endpoint = config.integrations.prismnet_endpoint.clone();
        if let Some(ref endpoint) = prismnet_endpoint {
            tracing::info!("PrismNET integration enabled: {}", endpoint);
        }

        // Initialize CreditService client if endpoint is configured
        let credit_service = match config.integrations.creditservice_endpoint.as_deref() {
            Some(endpoint) => match CreditServiceClient::connect(endpoint).await {
                Ok(client) => {
                    tracing::info!("CreditService admission control enabled: {}", endpoint);
                    Some(Arc::new(RwLock::new(client)))
                }
                Err(e) => {
                    tracing::warn!(
                        "Failed to connect to CreditService (admission control disabled): {}",
                        e
                    );
                    None
                }
            },
            None => {
                tracing::info!("CreditService endpoint not configured, admission control disabled");
                None
            }
        };

        let local_node_id = config
            .agent
            .node_id
            .clone()
            .filter(|value| !value.trim().is_empty());
        if let Some(ref node_id) = local_node_id {
            tracing::info!("Local node ID: {}", node_id);
        }

        let normalized_iam_endpoint = Self::normalize_iam_endpoint(&iam_endpoint.into());
        let mut iam_config =
            IamClientConfig::new(normalized_iam_endpoint.clone()).with_timeout(5000);
        if normalized_iam_endpoint.starts_with("http://") {
            iam_config = iam_config.without_tls();
        }
        let iam_client = Arc::new(IamClient::connect(iam_config).await?);
        let artifact_store =
            ArtifactStore::from_config(&config.artifacts, &normalized_iam_endpoint)
                .await?
                .map(Arc::new);
        if artifact_store.is_some() {
            tracing::info!("LightningStor artifact backing enabled for VM disks");
        }
        let volume_manager = Arc::new(VolumeManager::new_with_config(
            store.clone(),
            artifact_store.clone(),
            &config.volumes,
            local_node_id.clone(),
        )?);

        let default_hypervisor = match config.default_hypervisor {
            DefaultHypervisor::Kvm => HypervisorType::Kvm,
            DefaultHypervisor::Firecracker => HypervisorType::Firecracker,
            DefaultHypervisor::Mvisor => HypervisorType::Mvisor,
        };

        let svc = Self {
            hypervisor_registry,
            vms: DashMap::new(),
            images: DashMap::new(),
            handles: DashMap::new(),
            nodes: DashMap::new(),
            store: store.clone(),
            prismnet_endpoint,
            credit_service,
            local_node_id,
            shared_live_migration: config.agent.shared_live_migration,
            default_hypervisor,
            watch_poll_interval: Duration::from_millis(
                config.watcher.vm_watch_poll_interval_ms.max(100),
            ),
            auto_restart: config.health.auto_restart,
            failover_enabled: config.health.failover_enabled,
            failover_min_interval_secs: config.health.failover_min_interval_secs.max(1),
            artifact_store,
            volume_manager,
            iam_client,
            auth,
        };
        svc.load_state().await;
        Ok(svc)
    }

    pub fn supported_volume_drivers(&self) -> Vec<VolumeDriverKind> {
        self.volume_manager.supported_volume_drivers()
    }

    pub fn supported_storage_classes(&self) -> Vec<String> {
        self.volume_manager.supported_storage_classes()
    }

    pub fn shared_live_migration(&self) -> bool {
        self.shared_live_migration
    }

    pub fn store(&self) -> Arc<dyn VmStore> {
        Arc::clone(&self.store)
    }

    fn to_status_code(err: plasmavmc_types::Error) -> Status {
        Status::internal(err.to_string())
    }

    fn map_hv(&self, typ: ProtoHypervisorType) -> HypervisorType {
        match typ {
            ProtoHypervisorType::Kvm => HypervisorType::Kvm,
            ProtoHypervisorType::Firecracker => HypervisorType::Firecracker,
            ProtoHypervisorType::Mvisor => HypervisorType::Mvisor,
            ProtoHypervisorType::Unspecified => self.default_hypervisor,
        }
    }

    fn map_hv_proto(typ: HypervisorType) -> ProtoHypervisorType {
        match typ {
            HypervisorType::Kvm => ProtoHypervisorType::Kvm,
            HypervisorType::Firecracker => ProtoHypervisorType::Firecracker,
            HypervisorType::Mvisor => ProtoHypervisorType::Mvisor,
        }
    }

    fn now_epoch() -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()
    }

    fn require_uuid(value: &str, field_name: &str) -> Result<(), Status> {
        Uuid::parse_str(value)
            .map(|_| ())
            .map_err(|_| Status::invalid_argument(format!("{field_name} must be a UUID")))
    }

    fn validate_disk_reference(disk: &plasmavmc_types::DiskSpec) -> Result<(), Status> {
        match &disk.source {
            DiskSource::Image { image_id } => Self::require_uuid(image_id, "image_id"),
            DiskSource::Volume { volume_id } => Self::require_uuid(volume_id, "volume_id"),
            DiskSource::Blank => Ok(()),
        }
    }

    fn validate_vm_disk_references(spec: &plasmavmc_types::VmSpec) -> Result<(), Status> {
        for disk in &spec.disks {
            Self::validate_disk_reference(disk)?;
        }
        Ok(())
    }

    fn validate_network_reference(network: &plasmavmc_types::NetworkSpec) -> Result<(), Status> {
        if network.port_id.is_some() && network.subnet_id.is_none() {
            return Err(Status::invalid_argument(
                "subnet_id is required when port_id is specified",
            ));
        }
        if !network.security_groups.is_empty() && network.subnet_id.is_none() {
            return Err(Status::invalid_argument(
                "subnet_id is required when security_groups are specified",
            ));
        }
        if let Some(subnet_id) = network.subnet_id.as_deref() {
            Self::require_uuid(subnet_id, "subnet_id")?;
        }
        if let Some(port_id) = network.port_id.as_deref() {
            Self::require_uuid(port_id, "port_id")?;
        }
        for security_group in &network.security_groups {
            Self::require_uuid(security_group, "security_group_id")?;
        }
        Ok(())
    }

    fn validate_vm_network_references(spec: &plasmavmc_types::VmSpec) -> Result<(), Status> {
        for network in &spec.network {
            Self::validate_network_reference(network)?;
        }
        Ok(())
    }

    fn map_prismnet_error(
        error: &(dyn std::error::Error + Send + Sync + 'static),
        action: &str,
    ) -> Status {
        if let Some(status) = error.downcast_ref::<tonic::Status>() {
            return Status::new(
                status.code(),
                format!("failed to {action}: {}", status.message()),
            );
        }

        Status::failed_precondition(format!("failed to {action}: {error}"))
    }

    fn ensure_internal_rpc(tenant: &TenantContext) -> Result<(), Status> {
        if tenant.principal_kind != PrincipalKind::ServiceAccount
            || !tenant.principal_id.starts_with("plasmavmc-")
        {
            return Err(Status::permission_denied(
                "this RPC is restricted to internal PlasmaVMC service accounts",
            ));
        }
        Ok(())
    }

    fn expected_migration_listener_uri(&self, vm_id: &str) -> Result<String, Status> {
        let node_id = self.local_node_id.as_deref().ok_or_else(|| {
            Status::failed_precondition("local node id is required for migration preparation")
        })?;
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        node_id.hash(&mut hasher);
        vm_id.hash(&mut hasher);
        let port = 4400 + (hasher.finish() % 1000) as u16;
        Ok(format!("tcp:0.0.0.0:{port}"))
    }

    fn apply_imported_image_metadata(
        image: &mut Image,
        imported: &crate::artifact_store::ImportedImage,
        source_format: ImageFormat,
    ) {
        image.status = ImageStatus::Available;
        image.format = imported.format;
        image.size_bytes = imported.size_bytes;
        image.checksum = imported.checksum.clone();
        image.updated_at = Self::now_epoch();
        image
            .metadata
            .insert("source_type".to_string(), imported.source_type.clone());
        if let Some(host) = &imported.source_host {
            image
                .metadata
                .insert("source_host".to_string(), host.clone());
        }
        image.metadata.insert(
            "artifact_key".to_string(),
            format!("{}/{}/{}.qcow2", image.org_id, image.project_id, image.id),
        );
        if source_format != image.format {
            image.metadata.insert(
                "source_format".to_string(),
                format!("{source_format:?}").to_lowercase(),
            );
        }
    }

    fn watch_poll_interval(&self) -> Duration {
        self.watch_poll_interval
    }

    fn vm_values_differ(
        previous: &plasmavmc_types::VirtualMachine,
        current: &plasmavmc_types::VirtualMachine,
    ) -> Result<bool, Status> {
        let previous = serde_json::to_value(previous).map_err(|error| {
            Status::internal(format!("failed to serialize VM snapshot: {error}"))
        })?;
        let current = serde_json::to_value(current).map_err(|error| {
            Status::internal(format!("failed to serialize VM snapshot: {error}"))
        })?;
        Ok(previous != current)
    }

    fn build_vm_event(
        previous: Option<&plasmavmc_types::VirtualMachine>,
        current: Option<&plasmavmc_types::VirtualMachine>,
    ) -> Result<Option<VmEvent>, Status> {
        let event_type = match (previous, current) {
            (None, Some(_)) => ProtoVmEventType::Created,
            (Some(_), None) => ProtoVmEventType::Deleted,
            (Some(previous), Some(current)) => {
                if previous.state != current.state
                    || previous.status.actual_state != current.status.actual_state
                {
                    ProtoVmEventType::StateChanged
                } else if Self::vm_values_differ(previous, current)? {
                    ProtoVmEventType::Updated
                } else {
                    return Ok(None);
                }
            }
            (None, None) => return Ok(None),
        };

        let vm = current
            .or(previous)
            .ok_or_else(|| Status::internal("watch event builder received an empty VM snapshot"))?;

        Ok(Some(VmEvent {
            vm_id: vm.id.to_string(),
            event_type: event_type as i32,
            vm: Some(Self::to_proto_vm(vm, vm.status.clone())),
            timestamp: Self::now_epoch() as i64,
        }))
    }

    fn endpoint_host(endpoint: &str) -> Result<String, Status> {
        let authority = endpoint
            .split("://")
            .nth(1)
            .unwrap_or(endpoint)
            .split('/')
            .next()
            .unwrap_or(endpoint);
        let host_port = authority.rsplit('@').next().unwrap_or(authority);
        let host = if let Some(rest) = host_port.strip_prefix('[') {
            rest.split(']').next().unwrap_or_default().to_string()
        } else {
            host_port.split(':').next().unwrap_or_default().to_string()
        };
        if host.is_empty() {
            return Err(Status::failed_precondition(format!(
                "Unable to derive destination host from endpoint {endpoint}",
            )));
        }
        Ok(host)
    }

    fn derive_migration_uris(
        endpoint: &str,
        destination_node_id: &str,
        vm_id: &str,
    ) -> Result<(String, String), Status> {
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        destination_node_id.hash(&mut hasher);
        vm_id.hash(&mut hasher);
        let port = 4400 + (hasher.finish() % 1000) as u16;
        let host = Self::endpoint_host(endpoint)?;
        Ok((format!("tcp:{host}:{port}"), format!("tcp:0.0.0.0:{port}")))
    }

    async fn connect_vm_service(endpoint: &str) -> Result<VmServiceClient<Channel>, Status> {
        let endpoint = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
            endpoint.to_string()
        } else {
            format!("http://{}", endpoint)
        };
        let channel = Channel::from_shared(endpoint.clone())
            .map_err(|e| Status::failed_precondition(format!("Invalid endpoint {endpoint}: {e}")))?
            .connect()
            .await
            .map_err(|e| Status::unavailable(format!("Failed to connect to {endpoint}: {e}")))?;
        Ok(VmServiceClient::new(channel))
    }

    fn normalize_iam_endpoint(endpoint: &str) -> String {
        if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
            endpoint.to_string()
        } else {
            format!("http://{endpoint}")
        }
    }

    async fn attach_internal_auth<T>(
        &self,
        req: &mut Request<T>,
        org_id: &str,
        project_id: &str,
    ) -> Result<(), Status> {
        let token = self.issue_internal_token(org_id, project_id).await?;
        let token_value = MetadataValue::try_from(token)
            .map_err(|_| Status::failed_precondition("Invalid internal auth token"))?;
        req.metadata_mut()
            .insert("x-photon-auth-token", token_value);
        Ok(())
    }

    async fn issue_internal_token(&self, org_id: &str, project_id: &str) -> Result<String, Status> {
        let principal_id = format!(
            "plasmavmc-{}-{}",
            Self::sanitize_identifier(org_id),
            Self::sanitize_identifier(project_id)
        );
        let principal_ref = PrincipalRef::service_account(&principal_id);
        let principal = match self
            .iam_client
            .get_principal(&principal_ref)
            .await
            .map_err(|e| Status::unavailable(format!("IAM principal lookup failed: {e}")))?
        {
            Some(principal) => principal,
            None => self
                .iam_client
                .create_service_account(&principal_id, &principal_id, org_id, project_id)
                .await
                .map_err(|e| {
                    Status::unavailable(format!("IAM service account create failed: {e}"))
                })?,
        };

        let scope = Scope::project(project_id, org_id);
        let bindings = self
            .iam_client
            .list_bindings_for_principal(&principal_ref)
            .await
            .map_err(|e| Status::unavailable(format!("IAM binding lookup failed: {e}")))?;
        let has_project_admin = bindings
            .iter()
            .any(|binding| binding.role_ref == "roles/ProjectAdmin" && binding.scope == scope);
        if !has_project_admin {
            let binding = PolicyBinding::new(
                format!("binding-{principal_id}-{project_id}"),
                principal_ref,
                "roles/ProjectAdmin",
                scope.clone(),
            );
            self.iam_client
                .create_binding(&binding)
                .await
                .map_err(|e| Status::unavailable(format!("IAM binding create failed: {e}")))?;
        }

        self.iam_client
            .issue_token(&principal, vec![], scope, 3600)
            .await
            .map_err(|e| Status::unavailable(format!("IAM token issue failed: {e}")))
    }

    fn sanitize_identifier(value: &str) -> String {
        value
            .chars()
            .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' })
            .collect()
    }

    fn load_auto_prismnet_ports(vm: &plasmavmc_types::VirtualMachine) -> Vec<AutoPrismnetPort> {
        vm.metadata
            .get(PRISMNET_AUTO_PORTS_METADATA_KEY)
            .and_then(|value| serde_json::from_str(value).ok())
            .unwrap_or_default()
    }

    fn record_auto_prismnet_port(
        vm: &mut plasmavmc_types::VirtualMachine,
        auto_port: AutoPrismnetPort,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let mut auto_ports = Self::load_auto_prismnet_ports(vm);
        if auto_ports.iter().all(|entry| entry != &auto_port) {
            auto_ports.push(auto_port);
            vm.metadata.insert(
                PRISMNET_AUTO_PORTS_METADATA_KEY.to_string(),
                serde_json::to_string(&auto_ports)?,
            );
        }
        Ok(())
    }

    async fn ensure_nodes_loaded(&self) {
        if !self.nodes.is_empty() {
            return;
        }
        if let Ok(nodes) = self.store.list_nodes().await {
            for node in nodes {
                self.nodes.insert(node.id.to_string(), node);
            }
        }
    }

    fn is_control_plane_scheduler(&self) -> bool {
        self.local_node_id.is_none()
    }

    async fn required_storage_for_spec(
        &self,
        org_id: &str,
        project_id: &str,
        spec: &plasmavmc_types::VmSpec,
    ) -> Result<(HashSet<VolumeDriverKind>, HashSet<String>), Status> {
        let mut drivers = HashSet::new();
        let mut storage_classes = HashSet::new();
        for disk in &spec.disks {
            match &disk.source {
                DiskSource::Image { .. } | DiskSource::Blank => {
                    drivers.insert(VolumeDriverKind::Managed);
                    storage_classes.insert("managed-default".to_string());
                }
                DiskSource::Volume { volume_id } => {
                    let volume = self
                        .volume_manager
                        .get_volume(org_id, project_id, volume_id)
                        .await?
                        .ok_or_else(|| {
                            Status::not_found(format!("volume {volume_id} not found"))
                        })?;
                    drivers.insert(volume.driver);
                    storage_classes.insert(volume.storage_class.clone());
                }
            }
        }
        Ok((drivers, storage_classes))
    }

    fn node_supports_storage(
        node: &Node,
        required_drivers: &HashSet<VolumeDriverKind>,
        required_storage_classes: &HashSet<String>,
    ) -> bool {
        required_drivers
            .iter()
            .all(|driver| node.supported_volume_drivers.contains(driver))
            && required_storage_classes.iter().all(|class| {
                node.supported_storage_classes
                    .iter()
                    .any(|item| item == class)
            })
    }

    async fn select_target_node(
        &self,
        hypervisor: HypervisorType,
        org_id: &str,
        project_id: &str,
        spec: &plasmavmc_types::VmSpec,
    ) -> Option<Node> {
        self.ensure_nodes_loaded().await;
        let Ok((required_drivers, required_storage_classes)) = self
            .required_storage_for_spec(org_id, project_id, spec)
            .await
        else {
            return None;
        };
        let mut nodes: Vec<Node> = self
            .nodes
            .iter()
            .map(|entry| entry.value().clone())
            .collect();
        nodes.sort_by(|lhs, rhs| lhs.id.as_str().cmp(rhs.id.as_str()));
        nodes.into_iter().find(|node| {
            node.state == NodeState::Ready
                && node.labels.contains_key(NODE_ENDPOINT_LABEL)
                && (node.hypervisors.is_empty() || node.hypervisors.contains(&hypervisor))
                && Self::node_supports_storage(node, &required_drivers, &required_storage_classes)
        })
    }

    async fn forward_create_to_node(
        &self,
        endpoint: &str,
        org_id: &str,
        project_id: &str,
        req: &CreateVmRequest,
    ) -> Result<VirtualMachine, Status> {
        let mut client = Self::connect_vm_service(endpoint).await?;
        let mut remote_req = Request::new(req.clone());
        self.attach_internal_auth(&mut remote_req, org_id, project_id)
            .await?;
        client
            .create_vm(remote_req)
            .await
            .map(|response| response.into_inner())
            .map_err(|status| Status::from_error(Box::new(status)))
    }

    fn ensure_local_owner(&self, vm: &plasmavmc_types::VirtualMachine) -> Result<(), Status> {
        let Some(local) = self.local_node_id.as_deref() else {
            return Ok(());
        };
        let Some(node_id) = vm.node_id.as_ref() else {
            return Ok(());
        };
        if node_id.as_str() != local {
            return Err(Status::failed_precondition(format!(
                "VM is owned by another node (owner={}, local={})",
                node_id.as_str(),
                local
            )));
        }
        Ok(())
    }

    fn map_state(state: VmState) -> ProtoVmState {
        match state {
            VmState::Pending => ProtoVmState::Pending,
            VmState::Creating => ProtoVmState::Creating,
            VmState::Stopped => ProtoVmState::Stopped,
            VmState::Starting => ProtoVmState::Starting,
            VmState::Running => ProtoVmState::Running,
            VmState::Stopping => ProtoVmState::Stopping,
            VmState::Migrating => ProtoVmState::Migrating,
            VmState::Error => ProtoVmState::Error,
            VmState::Failed => ProtoVmState::Failed,
            VmState::Deleted => ProtoVmState::Deleted,
        }
    }

    fn map_disk_bus(bus: i32) -> DiskBus {
        match ProtoDiskBus::try_from(bus).unwrap_or(ProtoDiskBus::Unspecified) {
            ProtoDiskBus::Virtio => DiskBus::Virtio,
            ProtoDiskBus::Scsi => DiskBus::Scsi,
            ProtoDiskBus::Ide => DiskBus::Ide,
            ProtoDiskBus::Sata => DiskBus::Sata,
            ProtoDiskBus::Unspecified => DiskBus::Virtio,
        }
    }

    fn map_disk_cache(cache: i32) -> DiskCache {
        match ProtoDiskCache::try_from(cache).unwrap_or(ProtoDiskCache::Unspecified) {
            ProtoDiskCache::None => DiskCache::None,
            ProtoDiskCache::Writeback => DiskCache::Writeback,
            ProtoDiskCache::Writethrough => DiskCache::Writethrough,
            // Writeback keeps QEMU/NBD flush semantics while avoiding the
            // worst direct-I/O path for shared CoronaFS volumes.
            ProtoDiskCache::Unspecified => DiskCache::Writeback,
        }
    }

    fn map_nic_model(model: i32) -> NicModel {
        match ProtoNicModel::try_from(model).unwrap_or(ProtoNicModel::Unspecified) {
            ProtoNicModel::VirtioNet => NicModel::VirtioNet,
            ProtoNicModel::E1000 => NicModel::E1000,
            ProtoNicModel::Unspecified => NicModel::VirtioNet,
        }
    }

    fn map_node_state(state: ProtoNodeState) -> NodeState {
        match state {
            ProtoNodeState::Ready => NodeState::Ready,
            ProtoNodeState::NotReady => NodeState::NotReady,
            ProtoNodeState::Cordoned => NodeState::Cordoned,
            ProtoNodeState::Draining => NodeState::Draining,
            ProtoNodeState::Maintenance => NodeState::Maintenance,
            ProtoNodeState::Unspecified => NodeState::NotReady,
        }
    }

    fn map_node_state_proto(state: NodeState) -> ProtoNodeState {
        match state {
            NodeState::Ready => ProtoNodeState::Ready,
            NodeState::NotReady => ProtoNodeState::NotReady,
            NodeState::Cordoned => ProtoNodeState::Cordoned,
            NodeState::Draining => ProtoNodeState::Draining,
            NodeState::Maintenance => ProtoNodeState::Maintenance,
        }
    }

    fn proto_capacity_to_types(cap: Option<ProtoNodeCapacity>) -> NodeCapacity {
        cap.map(|c| NodeCapacity {
            vcpus: c.vcpus,
            memory_mib: c.memory_mib,
            storage_gib: c.storage_gib,
        })
        .unwrap_or_default()
    }

    fn types_capacity_to_proto(cap: &NodeCapacity) -> ProtoNodeCapacity {
        ProtoNodeCapacity {
            vcpus: cap.vcpus,
            memory_mib: cap.memory_mib,
            storage_gib: cap.storage_gib,
        }
    }

    fn proto_disk_to_types(d: plasmavmc_api::proto::DiskSpec) -> plasmavmc_types::DiskSpec {
        use plasmavmc_api::proto::disk_source::Source as ProtoDiskSourceKind;
        let source = match d.source.and_then(|s| s.source) {
            Some(ProtoDiskSourceKind::ImageId(id)) => DiskSource::Image { image_id: id },
            Some(ProtoDiskSourceKind::VolumeId(id)) => DiskSource::Volume { volume_id: id },
            Some(ProtoDiskSourceKind::Blank(_)) | None => DiskSource::Blank,
        };
        plasmavmc_types::DiskSpec {
            id: d.id,
            source,
            size_gib: d.size_gib,
            bus: Self::map_disk_bus(d.bus),
            cache: Self::map_disk_cache(d.cache),
            boot_index: if d.boot_index == 0 {
                None
            } else {
                Some(d.boot_index)
            },
        }
    }

    fn proto_nic_to_types(n: plasmavmc_api::proto::NetworkSpec) -> plasmavmc_types::NetworkSpec {
        NetworkSpec {
            id: n.id,
            network_id: n.network_id,
            subnet_id: if n.subnet_id.is_empty() {
                None
            } else {
                Some(n.subnet_id)
            },
            port_id: if n.port_id.is_empty() {
                None
            } else {
                Some(n.port_id)
            },
            mac_address: if n.mac_address.is_empty() {
                None
            } else {
                Some(n.mac_address)
            },
            ip_address: if n.ip_address.is_empty() {
                None
            } else {
                Some(n.ip_address)
            },
            cidr_block: if n.cidr_block.is_empty() {
                None
            } else {
                Some(n.cidr_block)
            },
            gateway_ip: if n.gateway_ip.is_empty() {
                None
            } else {
                Some(n.gateway_ip)
            },
            dhcp_enabled: n.dhcp_enabled,
            model: Self::map_nic_model(n.model),
            security_groups: n.security_groups,
        }
    }

    fn proto_spec_to_types(spec: Option<ProtoVmSpec>) -> plasmavmc_types::VmSpec {
        let spec = spec.unwrap_or_default();
        let cpu = spec
            .cpu
            .map(|c| plasmavmc_types::CpuSpec {
                vcpus: c.vcpus,
                cores_per_socket: c.cores_per_socket,
                sockets: c.sockets,
                cpu_model: if c.cpu_model.is_empty() {
                    None
                } else {
                    Some(c.cpu_model)
                },
            })
            .unwrap_or_default();
        let memory = spec
            .memory
            .map(|m| plasmavmc_types::MemorySpec {
                size_mib: m.size_mib,
                hugepages: m.hugepages,
            })
            .unwrap_or_default();
        let disks = spec
            .disks
            .into_iter()
            .map(|d| {
                let source = match d.source.and_then(|s| s.source) {
                    Some(ProtoDiskSourceKind::ImageId(id)) => DiskSource::Image { image_id: id },
                    Some(ProtoDiskSourceKind::VolumeId(id)) => DiskSource::Volume { volume_id: id },
                    Some(ProtoDiskSourceKind::Blank(_)) | None => DiskSource::Blank,
                };
                plasmavmc_types::DiskSpec {
                    id: d.id,
                    source,
                    size_gib: d.size_gib,
                    bus: Self::map_disk_bus(d.bus),
                    cache: Self::map_disk_cache(d.cache),
                    boot_index: if d.boot_index == 0 {
                        None
                    } else {
                        Some(d.boot_index)
                    },
                }
            })
            .collect();
        let network = spec
            .network
            .into_iter()
            .map(|n| NetworkSpec {
                id: n.id,
                network_id: n.network_id,
                subnet_id: if n.subnet_id.is_empty() {
                    None
                } else {
                    Some(n.subnet_id)
                },
                port_id: if n.port_id.is_empty() {
                    None
                } else {
                    Some(n.port_id)
                },
                mac_address: if n.mac_address.is_empty() {
                    None
                } else {
                    Some(n.mac_address)
                },
                ip_address: if n.ip_address.is_empty() {
                    None
                } else {
                    Some(n.ip_address)
                },
                cidr_block: if n.cidr_block.is_empty() {
                    None
                } else {
                    Some(n.cidr_block)
                },
                gateway_ip: if n.gateway_ip.is_empty() {
                    None
                } else {
                    Some(n.gateway_ip)
                },
                dhcp_enabled: n.dhcp_enabled,
                model: Self::map_nic_model(n.model),
                security_groups: n.security_groups,
            })
            .collect();
        let boot = spec
            .boot
            .map(|b| plasmavmc_types::BootSpec {
                kernel: if b.kernel.is_empty() {
                    None
                } else {
                    Some(b.kernel)
                },
                initrd: if b.initrd.is_empty() {
                    None
                } else {
                    Some(b.initrd)
                },
                cmdline: if b.cmdline.is_empty() {
                    None
                } else {
                    Some(b.cmdline)
                },
            })
            .unwrap_or_default();
        let security = spec
            .security
            .map(|s| plasmavmc_types::SecuritySpec {
                secure_boot: s.secure_boot,
                tpm: s.tpm,
            })
            .unwrap_or_default();

        plasmavmc_types::VmSpec {
            cpu,
            memory,
            disks,
            network,
            boot,
            security,
        }
    }

    fn to_proto_vm(
        vm: &plasmavmc_types::VirtualMachine,
        status: plasmavmc_types::VmStatus,
    ) -> VirtualMachine {
        let state = Self::map_state(status.actual_state);
        VirtualMachine {
            id: vm.id.to_string(),
            name: vm.name.clone(),
            org_id: vm.org_id.clone(),
            project_id: vm.project_id.clone(),
            state: state as i32,
            spec: Some(Self::types_spec_to_proto(&vm.spec)),
            status: Some(Self::types_status_to_proto(status)),
            node_id: vm
                .node_id
                .as_ref()
                .map(|n| n.to_string())
                .unwrap_or_default(),
            hypervisor: match vm.hypervisor {
                HypervisorType::Kvm => ProtoHypervisorType::Kvm as i32,
                HypervisorType::Firecracker => ProtoHypervisorType::Firecracker as i32,
                HypervisorType::Mvisor => ProtoHypervisorType::Mvisor as i32,
            },
            created_at: vm.created_at as i64,
            updated_at: vm.updated_at as i64,
            created_by: vm.created_by.clone(),
            metadata: vm.metadata.clone(),
            labels: vm.labels.clone(),
        }
    }

    fn types_spec_to_proto(spec: &plasmavmc_types::VmSpec) -> ProtoVmSpec {
        let cpu = Some(plasmavmc_api::proto::CpuSpec {
            vcpus: spec.cpu.vcpus,
            cores_per_socket: spec.cpu.cores_per_socket,
            sockets: spec.cpu.sockets,
            cpu_model: spec.cpu.cpu_model.clone().unwrap_or_default(),
        });
        let memory = Some(plasmavmc_api::proto::MemorySpec {
            size_mib: spec.memory.size_mib,
            hugepages: spec.memory.hugepages,
        });
        let disks = spec
            .disks
            .iter()
            .map(|d| plasmavmc_api::proto::DiskSpec {
                id: d.id.clone(),
                source: Some(ProtoDiskSource {
                    source: match &d.source {
                        DiskSource::Image { image_id } => {
                            Some(ProtoDiskSourceKind::ImageId(image_id.clone()))
                        }
                        DiskSource::Volume { volume_id } => {
                            Some(ProtoDiskSourceKind::VolumeId(volume_id.clone()))
                        }
                        DiskSource::Blank => Some(ProtoDiskSourceKind::Blank(true)),
                    },
                }),
                size_gib: d.size_gib,
                bus: match d.bus {
                    DiskBus::Virtio => ProtoDiskBus::Virtio as i32,
                    DiskBus::Scsi => ProtoDiskBus::Scsi as i32,
                    DiskBus::Ide => ProtoDiskBus::Ide as i32,
                    DiskBus::Sata => ProtoDiskBus::Sata as i32,
                },
                cache: match d.cache {
                    DiskCache::None => ProtoDiskCache::None as i32,
                    DiskCache::Writeback => ProtoDiskCache::Writeback as i32,
                    DiskCache::Writethrough => ProtoDiskCache::Writethrough as i32,
                },
                boot_index: d.boot_index.unwrap_or_default(),
            })
            .collect();
        let network = spec
            .network
            .iter()
            .map(|n| plasmavmc_api::proto::NetworkSpec {
                id: n.id.clone(),
                network_id: n.network_id.clone(),
                subnet_id: n.subnet_id.clone().unwrap_or_default(),
                port_id: n.port_id.clone().unwrap_or_default(),
                mac_address: n.mac_address.clone().unwrap_or_default(),
                ip_address: n.ip_address.clone().unwrap_or_default(),
                cidr_block: n.cidr_block.clone().unwrap_or_default(),
                gateway_ip: n.gateway_ip.clone().unwrap_or_default(),
                dhcp_enabled: n.dhcp_enabled,
                model: match n.model {
                    NicModel::VirtioNet => ProtoNicModel::VirtioNet as i32,
                    NicModel::E1000 => ProtoNicModel::E1000 as i32,
                },
                security_groups: n.security_groups.clone(),
            })
            .collect();
        let boot = Some(plasmavmc_api::proto::BootSpec {
            kernel: spec.boot.kernel.clone().unwrap_or_default(),
            initrd: spec.boot.initrd.clone().unwrap_or_default(),
            cmdline: spec.boot.cmdline.clone().unwrap_or_default(),
        });
        let security = Some(plasmavmc_api::proto::SecuritySpec {
            secure_boot: spec.security.secure_boot,
            tpm: spec.security.tpm,
        });

        ProtoVmSpec {
            cpu,
            memory,
            disks,
            network,
            boot,
            security,
        }
    }

    fn proto_vm_to_types(
        &self,
        vm: &VirtualMachine,
    ) -> Result<plasmavmc_types::VirtualMachine, Status> {
        let spec = Self::proto_spec_to_types(vm.spec.clone());
        let mut typed = plasmavmc_types::VirtualMachine::new(
            vm.name.clone(),
            vm.org_id.clone(),
            vm.project_id.clone(),
            spec,
        );
        typed.id = VmId::from_uuid(
            Uuid::parse_str(&vm.id).map_err(|e| Status::internal(format!("invalid VM id: {e}")))?,
        );
        typed.hypervisor = self.map_hv(
            ProtoHypervisorType::try_from(vm.hypervisor).unwrap_or(ProtoHypervisorType::Kvm),
        );
        typed.node_id = if vm.node_id.is_empty() {
            None
        } else {
            Some(NodeId::new(vm.node_id.clone()))
        };
        typed.state = match ProtoVmState::try_from(vm.state).unwrap_or(ProtoVmState::Pending) {
            ProtoVmState::Pending => VmState::Pending,
            ProtoVmState::Creating => VmState::Creating,
            ProtoVmState::Stopped => VmState::Stopped,
            ProtoVmState::Starting => VmState::Starting,
            ProtoVmState::Running => VmState::Running,
            ProtoVmState::Stopping => VmState::Stopping,
            ProtoVmState::Migrating => VmState::Migrating,
            ProtoVmState::Error => VmState::Error,
            ProtoVmState::Failed => VmState::Failed,
            ProtoVmState::Deleted => VmState::Deleted,
            ProtoVmState::Unspecified => VmState::Pending,
        };
        typed.status = vm
            .status
            .clone()
            .map(Self::proto_status_to_types)
            .unwrap_or_default();
        typed.metadata = vm.metadata.clone();
        typed.labels = vm.labels.clone();
        typed.created_at = vm.created_at as u64;
        typed.updated_at = vm.updated_at as u64;
        typed.created_by = vm.created_by.clone();
        Ok(typed)
    }

    fn proto_status_to_types(status: ProtoVmStatus) -> plasmavmc_types::VmStatus {
        plasmavmc_types::VmStatus {
            actual_state: match ProtoVmState::try_from(status.actual_state)
                .unwrap_or(ProtoVmState::Pending)
            {
                ProtoVmState::Pending => VmState::Pending,
                ProtoVmState::Creating => VmState::Creating,
                ProtoVmState::Stopped => VmState::Stopped,
                ProtoVmState::Starting => VmState::Starting,
                ProtoVmState::Running => VmState::Running,
                ProtoVmState::Stopping => VmState::Stopping,
                ProtoVmState::Migrating => VmState::Migrating,
                ProtoVmState::Error => VmState::Error,
                ProtoVmState::Failed => VmState::Failed,
                ProtoVmState::Deleted => VmState::Deleted,
                ProtoVmState::Unspecified => VmState::Pending,
            },
            host_pid: if status.host_pid == 0 {
                None
            } else {
                Some(status.host_pid)
            },
            started_at: if status.started_at == 0 {
                None
            } else {
                Some(status.started_at as u64)
            },
            ip_addresses: status.ip_addresses,
            resource_usage: status
                .resource_usage
                .map(|usage| plasmavmc_types::ResourceUsage {
                    cpu_percent: usage.cpu_percent,
                    memory_used_mib: usage.memory_used_mib,
                    disk_read_bytes: usage.disk_read_bytes,
                    disk_write_bytes: usage.disk_write_bytes,
                    network_rx_bytes: usage.network_rx_bytes,
                    network_tx_bytes: usage.network_tx_bytes,
                })
                .unwrap_or_default(),
            last_error: if status.last_error.is_empty() {
                None
            } else {
                Some(status.last_error)
            },
        }
    }

    fn types_status_to_proto(status: plasmavmc_types::VmStatus) -> ProtoVmStatus {
        ProtoVmStatus {
            actual_state: Self::map_state(status.actual_state) as i32,
            host_pid: status.host_pid.unwrap_or_default(),
            started_at: status.started_at.unwrap_or_default() as i64,
            ip_addresses: status.ip_addresses,
            resource_usage: Some(plasmavmc_api::proto::ResourceUsage {
                cpu_percent: status.resource_usage.cpu_percent,
                memory_used_mib: status.resource_usage.memory_used_mib,
                disk_read_bytes: status.resource_usage.disk_read_bytes,
                disk_write_bytes: status.resource_usage.disk_write_bytes,
                network_rx_bytes: status.resource_usage.network_rx_bytes,
                network_tx_bytes: status.resource_usage.network_tx_bytes,
            }),
            last_error: status.last_error.unwrap_or_default(),
        }
    }

    fn types_node_to_proto(node: &Node) -> ProtoNode {
        ProtoNode {
            id: node.id.to_string(),
            name: node.name.clone(),
            state: Self::map_node_state_proto(node.state) as i32,
            capacity: Some(Self::types_capacity_to_proto(&node.capacity)),
            allocatable: Some(Self::types_capacity_to_proto(&node.allocatable)),
            allocated: Some(Self::types_capacity_to_proto(&node.allocated)),
            hypervisors: node
                .hypervisors
                .iter()
                .map(|h| Self::map_hv_proto(*h) as i32)
                .collect(),
            labels: node.labels.clone(),
            agent_version: node.agent_version.clone(),
            last_heartbeat: node.last_heartbeat as i64,
            supported_volume_drivers: node
                .supported_volume_drivers
                .iter()
                .map(|driver| Self::map_volume_driver_proto(*driver) as i32)
                .collect(),
            supported_storage_classes: node.supported_storage_classes.clone(),
            shared_live_migration: node.shared_live_migration,
        }
    }

    fn map_visibility(proto: ProtoVisibility) -> Visibility {
        match proto {
            ProtoVisibility::Public => Visibility::Public,
            ProtoVisibility::Shared => Visibility::Shared,
            ProtoVisibility::Private | ProtoVisibility::Unspecified => Visibility::Private,
        }
    }

    fn map_visibility_proto(visibility: Visibility) -> ProtoVisibility {
        match visibility {
            Visibility::Public => ProtoVisibility::Public,
            Visibility::Private => ProtoVisibility::Private,
            Visibility::Shared => ProtoVisibility::Shared,
        }
    }

    fn map_image_format(proto: ProtoImageFormat) -> ImageFormat {
        match proto {
            ProtoImageFormat::Raw => ImageFormat::Raw,
            ProtoImageFormat::Qcow2 | ProtoImageFormat::Unspecified => ImageFormat::Qcow2,
            ProtoImageFormat::Vmdk => ImageFormat::Vmdk,
            ProtoImageFormat::Vhd => ImageFormat::Vhd,
        }
    }

    fn map_image_format_proto(format: ImageFormat) -> ProtoImageFormat {
        match format {
            ImageFormat::Raw => ProtoImageFormat::Raw,
            ImageFormat::Qcow2 => ProtoImageFormat::Qcow2,
            ImageFormat::Vmdk => ProtoImageFormat::Vmdk,
            ImageFormat::Vhd => ProtoImageFormat::Vhd,
        }
    }

    fn map_os_type(proto: ProtoOsType) -> OsType {
        match proto {
            ProtoOsType::Linux => OsType::Linux,
            ProtoOsType::Windows => OsType::Windows,
            ProtoOsType::Bsd => OsType::Bsd,
            ProtoOsType::Unspecified => OsType::Unspecified,
        }
    }

    fn map_os_type_proto(os_type: OsType) -> ProtoOsType {
        match os_type {
            OsType::Linux => ProtoOsType::Linux,
            OsType::Windows => ProtoOsType::Windows,
            OsType::Bsd => ProtoOsType::Bsd,
            OsType::Unspecified => ProtoOsType::Unspecified,
        }
    }

    fn map_architecture(proto: i32) -> Architecture {
        match proto {
            1 => Architecture::X86_64,
            2 => Architecture::Aarch64,
            _ => Architecture::Unspecified,
        }
    }

    fn map_architecture_proto(architecture: Architecture) -> i32 {
        match architecture {
            Architecture::Unspecified => ProtoArchitecture::Unspecified as i32,
            Architecture::X86_64 => 1,
            Architecture::Aarch64 => 2,
        }
    }

    fn map_image_status_proto(status: ImageStatus) -> ProtoImageStatus {
        match status {
            ImageStatus::Pending => ProtoImageStatus::Pending,
            ImageStatus::Uploading => ProtoImageStatus::Uploading,
            ImageStatus::Available => ProtoImageStatus::Available,
            ImageStatus::Error => ProtoImageStatus::Error,
        }
    }

    fn map_volume_driver(proto: ProtoVolumeDriverKind) -> VolumeDriverKind {
        match proto {
            ProtoVolumeDriverKind::CephRbd => VolumeDriverKind::CephRbd,
            ProtoVolumeDriverKind::Managed | ProtoVolumeDriverKind::Unspecified => {
                VolumeDriverKind::Managed
            }
        }
    }

    fn map_volume_driver_proto(driver: VolumeDriverKind) -> ProtoVolumeDriverKind {
        match driver {
            VolumeDriverKind::Managed => ProtoVolumeDriverKind::Managed,
            VolumeDriverKind::CephRbd => ProtoVolumeDriverKind::CephRbd,
        }
    }

    fn map_volume_format_proto(format: VolumeFormat) -> ProtoVolumeFormat {
        match format {
            VolumeFormat::Raw => ProtoVolumeFormat::Raw,
            VolumeFormat::Qcow2 => ProtoVolumeFormat::Qcow2,
        }
    }

    fn map_volume_status_proto(status: VolumeStatus) -> ProtoVolumeStatus {
        match status {
            VolumeStatus::Pending => ProtoVolumeStatus::Pending,
            VolumeStatus::Available => ProtoVolumeStatus::Available,
            VolumeStatus::InUse => ProtoVolumeStatus::InUse,
            VolumeStatus::Error => ProtoVolumeStatus::Error,
        }
    }

    fn types_volume_to_proto(volume: &Volume) -> ProtoVolume {
        let backing = match &volume.backing {
            VolumeBacking::Managed => ProtoVolumeBacking {
                backing: Some(plasmavmc_api::proto::volume_backing::Backing::Managed(
                    ManagedVolumeBacking {},
                )),
            },
            VolumeBacking::CephRbd {
                cluster_id,
                pool,
                image,
            } => ProtoVolumeBacking {
                backing: Some(plasmavmc_api::proto::volume_backing::Backing::CephRbd(
                    CephRbdBacking {
                        cluster_id: cluster_id.clone(),
                        pool: pool.clone(),
                        image: image.clone(),
                    },
                )),
            },
        };

        ProtoVolume {
            id: volume.id.clone(),
            name: volume.name.clone(),
            org_id: volume.org_id.clone(),
            project_id: volume.project_id.clone(),
            size_gib: volume.size_gib,
            driver: Self::map_volume_driver_proto(volume.driver) as i32,
            storage_class: volume.storage_class.clone(),
            format: Self::map_volume_format_proto(volume.format) as i32,
            status: Self::map_volume_status_proto(volume.status) as i32,
            attached_to_vm: volume.attached_to_vm.clone().unwrap_or_default(),
            attached_to_node: volume.attached_to_node.clone().unwrap_or_default(),
            attachment_generation: volume.attachment_generation,
            last_flushed_attachment_generation: volume.last_flushed_attachment_generation,
            metadata: volume.metadata.clone(),
            labels: volume.labels.clone(),
            created_at: volume.created_at as i64,
            updated_at: volume.updated_at as i64,
            backing: Some(backing),
        }
    }

    fn types_image_to_proto(image: &Image) -> ProtoImage {
        ProtoImage {
            id: image.id.clone(),
            name: image.name.clone(),
            org_id: image.org_id.clone(),
            visibility: Self::map_visibility_proto(image.visibility) as i32,
            format: Self::map_image_format_proto(image.format) as i32,
            size_bytes: image.size_bytes,
            checksum: image.checksum.clone(),
            os_type: Self::map_os_type_proto(image.os_type) as i32,
            os_version: image.os_version.clone(),
            architecture: Self::map_architecture_proto(image.architecture),
            min_disk_gib: image.min_disk_gib,
            min_memory_mib: image.min_memory_mib,
            status: Self::map_image_status_proto(image.status) as i32,
            created_at: image.created_at as i64,
            updated_at: image.updated_at as i64,
            metadata: image.metadata.clone(),
        }
    }

    async fn persist_vm(&self, vm: &plasmavmc_types::VirtualMachine) {
        match tokio::time::timeout(STORE_OP_TIMEOUT, self.store.save_vm(vm)).await {
            Ok(Ok(())) => {}
            Ok(Err(e)) => {
                tracing::warn!("Failed to persist VM {}: {}", vm.id, e);
            }
            Err(_) => {
                tracing::warn!(
                    vm_id = %vm.id,
                    timeout_secs = STORE_OP_TIMEOUT.as_secs(),
                    "Timed out persisting VM state"
                );
            }
        }
    }

    async fn persist_node(&self, node: &Node) {
        match tokio::time::timeout(STORE_OP_TIMEOUT, self.store.save_node(node)).await {
            Ok(Ok(())) => {}
            Ok(Err(e)) => {
                tracing::warn!("Failed to persist node {}: {}", node.id, e);
            }
            Err(_) => {
                tracing::warn!(
                    node_id = %node.id,
                    timeout_secs = STORE_OP_TIMEOUT.as_secs(),
                    "Timed out persisting node state"
                );
            }
        }
    }

    async fn persist_image(&self, image: &Image) {
        match tokio::time::timeout(STORE_OP_TIMEOUT, self.store.save_image(image)).await {
            Ok(Ok(())) => {}
            Ok(Err(e)) => {
                tracing::warn!("Failed to persist image {}: {}", image.id, e);
            }
            Err(_) => {
                tracing::warn!(
                    image_id = %image.id,
                    timeout_secs = STORE_OP_TIMEOUT.as_secs(),
                    "Timed out persisting image state"
                );
            }
        }
    }

    async fn persist_handle(
        &self,
        org_id: &str,
        project_id: &str,
        vm_id: &str,
        handle: &plasmavmc_types::VmHandle,
    ) {
        match tokio::time::timeout(
            STORE_OP_TIMEOUT,
            self.store.save_handle(org_id, project_id, vm_id, handle),
        )
        .await
        {
            Ok(Ok(())) => {}
            Ok(Err(e)) => {
                tracing::warn!("Failed to persist handle for VM {}: {}", vm_id, e);
            }
            Err(_) => {
                tracing::warn!(
                    vm_id,
                    timeout_secs = STORE_OP_TIMEOUT.as_secs(),
                    "Timed out persisting VM handle"
                );
            }
        }
    }

    async fn delete_persisted_handle(&self, org_id: &str, project_id: &str, vm_id: &str) {
        match tokio::time::timeout(
            STORE_OP_TIMEOUT,
            self.store.delete_handle(org_id, project_id, vm_id),
        )
        .await
        {
            Ok(Ok(())) => {}
            Ok(Err(e)) => {
                tracing::warn!("Failed to delete persisted handle for VM {}: {}", vm_id, e);
            }
            Err(_) => {
                tracing::warn!(
                    vm_id,
                    timeout_secs = STORE_OP_TIMEOUT.as_secs(),
                    "Timed out deleting persisted VM handle"
                );
            }
        }
    }

    async fn load_state(&self) {
        // Load all VMs from storage (we'll filter by tenant on-demand)
        // For now, we'll load on first access per tenant
        tracing::info!("VM service initialized with storage backend");
    }

    async fn ensure_vm_loaded(
        &self,
        org_id: &str,
        project_id: &str,
        vm_id: &str,
    ) -> Option<plasmavmc_types::VirtualMachine> {
        let key = TenantKey::new(org_id, project_id, vm_id);
        if self.vms.contains_key(&key) {
            return self.vms.get(&key).map(|v| v.clone());
        }

        // Load from storage
        if let Ok(Some(vm)) = self.store.load_vm(org_id, project_id, vm_id).await {
            let handle = self
                .store
                .load_handle(org_id, project_id, vm_id)
                .await
                .ok()
                .flatten();
            self.vms.insert(key.clone(), vm.clone());
            if let Some(handle) = handle {
                self.handles.insert(key, handle);
            }
            return Some(vm);
        }
        None
    }

    async fn ensure_destination_slot_available(
        &self,
        org_id: &str,
        project_id: &str,
        vm_id: &str,
    ) -> Result<(), Status> {
        let Some(existing_vm) = self.ensure_vm_loaded(org_id, project_id, vm_id).await else {
            return Ok(());
        };

        let Some(local_node_id) = self.local_node_id.as_deref() else {
            return Err(Status::already_exists("VM already exists on destination"));
        };

        let existing_owner = existing_vm.node_id.as_ref().map(|id| id.as_str());
        if existing_owner.is_none() || existing_owner == Some(local_node_id) {
            return Err(Status::already_exists("VM already exists on destination"));
        }

        let key = TenantKey::new(org_id, project_id, vm_id.to_string());
        if let Some(handle) = self.handles.get(&key) {
            let runtime_exists = std::path::Path::new(&handle.runtime_dir).exists();
            if runtime_exists || handle_qmp_socket_exists(&handle) {
                tracing::warn!(
                    vm_id,
                    org_id,
                    project_id,
                    local_node_id,
                    existing_owner = ?existing_owner,
                    runtime_dir = %handle.runtime_dir,
                    "Refusing to overwrite destination VM slot with live local runtime state"
                );
                return Err(Status::already_exists("VM already exists on destination"));
            }
        }

        tracing::info!(
            vm_id,
            org_id,
            project_id,
            local_node_id,
            existing_owner = ?existing_owner,
            "Clearing mirrored VM state on destination before taking ownership"
        );
        self.vms.remove(&key);
        self.handles.remove(&key);
        Ok(())
    }

    async fn ensure_tenant_loaded(&self, org_id: &str, project_id: &str) {
        // Check if we've already loaded this tenant
        // Simple check: if any VM exists for this tenant, assume loaded
        let has_any = self
            .vms
            .iter()
            .any(|entry| entry.key().org_id == org_id && entry.key().project_id == project_id);
        if has_any {
            return;
        }

        // Load all VMs for this tenant
        if let Ok(vms) = self.store.list_vms(org_id, project_id).await {
            for vm in vms {
                let key = TenantKey::new(&vm.org_id, &vm.project_id, vm.id.to_string());
                if !self.vms.contains_key(&key) {
                    let handle = self
                        .store
                        .load_handle(&vm.org_id, &vm.project_id, &vm.id.to_string())
                        .await
                        .ok()
                        .flatten();
                    self.vms.insert(key.clone(), vm.clone());
                    if let Some(handle) = handle {
                        self.handles.insert(key, handle);
                    }
                }
            }
        }
    }

    async fn ensure_image_loaded(
        &self,
        org_id: &str,
        project_id: &str,
        image_id: &str,
    ) -> Option<Image> {
        let key = TenantKey::new(org_id, project_id, image_id);
        if self.images.contains_key(&key) {
            return self.images.get(&key).map(|image| image.clone());
        }

        if let Ok(Some(image)) = self.store.load_image(org_id, project_id, image_id).await {
            self.images.insert(key, image.clone());
            return Some(image);
        }
        None
    }

    async fn ensure_images_loaded(&self, org_id: &str, project_id: &str) {
        let has_any = self
            .images
            .iter()
            .any(|entry| entry.key().org_id == org_id && entry.key().project_id == project_id);
        if has_any {
            return;
        }

        if let Ok(images) = self.store.list_images(org_id, project_id).await {
            for image in images {
                let key = TenantKey::new(&image.org_id, &image.project_id, &image.id);
                self.images.insert(key, image);
            }
        }
    }

    fn resolve_image_tenant(
        tenant: &iam_service_auth::TenantContext,
        req_org_id: &str,
    ) -> Result<(String, String), Status> {
        if !req_org_id.is_empty() && req_org_id != tenant.org_id {
            return Err(Status::permission_denied("org_id does not match token"));
        }
        Ok((tenant.org_id.clone(), tenant.project_id.clone()))
    }

    async fn ensure_node_loaded(&self, node_id: &str) -> Option<Node> {
        if self.nodes.contains_key(node_id) {
            return self.nodes.get(node_id).map(|n| n.clone());
        }

        if let Ok(Some(node)) = self.store.load_node(node_id).await {
            self.nodes.insert(node_id.to_string(), node.clone());
            return Some(node);
        }
        None
    }

    /// Attach VM to PrismNET ports
    async fn attach_prismnet_ports(
        &self,
        vm: &mut plasmavmc_types::VirtualMachine,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let Some(ref endpoint) = self.prismnet_endpoint else {
            return Ok(());
        };

        let auth_token = self
            .issue_internal_token(&vm.org_id, &vm.project_id)
            .await?;
        let mut client = PrismNETClient::new(endpoint.clone(), auth_token).await?;

        for nic_index in 0..vm.spec.network.len() {
            let nic = vm.spec.network[nic_index].clone();
            let Some(subnet_id) = nic.subnet_id.clone() else {
                continue;
            };

            let port = if let Some(port_id) = nic.port_id.clone() {
                client
                    .get_port(&vm.org_id, &vm.project_id, &subnet_id, &port_id)
                    .await?
            } else {
                let port = client
                    .create_port(
                        &vm.org_id,
                        &vm.project_id,
                        &subnet_id,
                        &format!("{}-{}", vm.name, nic.id),
                        Some(&format!("Auto-managed port for VM {}", vm.id)),
                        nic.ip_address.as_deref(),
                        nic.security_groups.clone(),
                    )
                    .await?;
                Self::record_auto_prismnet_port(
                    vm,
                    AutoPrismnetPort {
                        nic_id: nic.id.clone(),
                        subnet_id: subnet_id.clone(),
                        port_id: port.id.clone(),
                    },
                )?;
                port
            };
            let subnet = client
                .get_subnet(&vm.org_id, &vm.project_id, &subnet_id)
                .await?;

            {
                let net_spec = &mut vm.spec.network[nic_index];
                net_spec.port_id = Some(port.id.clone());
                net_spec.mac_address = Some(port.mac_address.clone());
                net_spec.ip_address = if port.ip_address.is_empty() {
                    None
                } else {
                    Some(port.ip_address.clone())
                };
                net_spec.cidr_block = (!subnet.cidr_block.is_empty()).then_some(subnet.cidr_block);
                net_spec.gateway_ip = (!subnet.gateway_ip.is_empty()).then_some(subnet.gateway_ip);
                net_spec.dhcp_enabled = subnet.dhcp_enabled;
            }

            client
                .attach_device(
                    &vm.org_id,
                    &vm.project_id,
                    &subnet_id,
                    &port.id,
                    &vm.id.to_string(),
                    PRISMNET_VM_DEVICE_TYPE,
                )
                .await?;

            tracing::info!(
                vm_id = %vm.id,
                nic_id = %nic.id,
                port_id = %port.id,
                mac = %port.mac_address,
                "Attached VM to PrismNET port"
            );
        }
        Ok(())
    }

    /// Detach VM from PrismNET ports
    async fn detach_prismnet_ports(
        &self,
        vm: &plasmavmc_types::VirtualMachine,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let Some(ref endpoint) = self.prismnet_endpoint else {
            return Ok(());
        };

        let auth_token = self
            .issue_internal_token(&vm.org_id, &vm.project_id)
            .await?;
        let mut client = PrismNETClient::new(endpoint.clone(), auth_token).await?;

        for net_spec in &vm.spec.network {
            if let (Some(ref subnet_id), Some(ref port_id)) =
                (&net_spec.subnet_id, &net_spec.port_id)
            {
                // Detach VM from port
                client
                    .detach_device(&vm.org_id, &vm.project_id, subnet_id, port_id)
                    .await?;

                tracing::info!(
                    vm_id = %vm.id,
                    port_id = %port_id,
                    "Detached VM from PrismNET port"
                );
            }
        }
        Ok(())
    }

    async fn delete_auto_prismnet_ports(
        &self,
        vm: &plasmavmc_types::VirtualMachine,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let Some(ref endpoint) = self.prismnet_endpoint else {
            return Ok(());
        };
        let auto_ports = Self::load_auto_prismnet_ports(vm);
        if auto_ports.is_empty() {
            return Ok(());
        }

        let auth_token = self
            .issue_internal_token(&vm.org_id, &vm.project_id)
            .await?;
        let mut client = PrismNETClient::new(endpoint.clone(), auth_token).await?;

        for auto_port in auto_ports {
            let result = client
                .delete_port(
                    &vm.org_id,
                    &vm.project_id,
                    &auto_port.subnet_id,
                    &auto_port.port_id,
                )
                .await;
            if let Err(error) = result {
                if error
                    .downcast_ref::<tonic::Status>()
                    .is_some_and(|status| status.code() == tonic::Code::NotFound)
                {
                    continue;
                }
                return Err(error);
            }

            tracing::info!(
                vm_id = %vm.id,
                nic_id = %auto_port.nic_id,
                port_id = %auto_port.port_id,
                "Deleted auto-managed PrismNET port"
            );
        }

        Ok(())
    }

    async fn rollback_prepared_vm_resources(
        &self,
        vm: &plasmavmc_types::VirtualMachine,
        delete_auto_delete_volumes: bool,
    ) {
        if let Err(error) = self.detach_prismnet_ports(vm).await {
            tracing::warn!(
                vm_id = %vm.id,
                error = %error,
                "Failed to detach PrismNET ports during VM rollback"
            );
        }
        if let Err(error) = self.delete_auto_prismnet_ports(vm).await {
            tracing::warn!(
                vm_id = %vm.id,
                error = %error,
                "Failed to delete auto-managed PrismNET ports during VM rollback"
            );
        }
        if let Err(error) = self
            .volume_manager
            .rollback_vm_volumes(vm, delete_auto_delete_volumes)
            .await
        {
            tracing::warn!(
                vm_id = %vm.id,
                error = %error,
                "Failed to roll back provisional VM volumes"
            );
        }
    }

    /// Spawn a background health monitor that periodically refreshes VM status.
    pub fn start_health_monitor(self: Arc<Self>, interval: Duration) {
        if interval.as_secs() == 0 {
            tracing::warn!("Health monitor interval is 0; skipping start");
            return;
        }
        tracing::info!(
            interval_secs = interval.as_secs(),
            "Starting PlasmaVMC health monitor"
        );
        tokio::spawn(async move {
            let mut ticker = tokio::time::interval(interval);
            loop {
                ticker.tick().await;
                self.refresh_all_vm_statuses().await;
            }
        });
    }

    /// Spawn a background health monitor that marks nodes NotReady when heartbeats expire.
    pub fn start_node_health_monitor(self: Arc<Self>, interval: Duration, timeout: Duration) {
        if interval.as_secs() == 0 || timeout.as_secs() == 0 {
            tracing::warn!("Node health monitor disabled (interval/timeout is 0)");
            return;
        }
        tracing::info!(
            interval_secs = interval.as_secs(),
            timeout_secs = timeout.as_secs(),
            "Starting PlasmaVMC node health monitor"
        );
        tokio::spawn(async move {
            let mut ticker = tokio::time::interval(interval);
            loop {
                ticker.tick().await;
                self.refresh_node_health(timeout).await;
            }
        });
    }

    async fn refresh_all_vm_statuses(&self) {
        // Snapshot keys to avoid holding map locks across await points.
        let entries: Vec<(TenantKey, plasmavmc_types::VirtualMachine)> = self
            .vms
            .iter()
            .map(|entry| (entry.key().clone(), entry.value().clone()))
            .collect();

        let local_node = self.local_node_id.as_deref();

        for (key, mut vm) in entries {
            if let (Some(local), Some(node_id)) = (local_node, vm.node_id.as_ref()) {
                if node_id.as_str() != local {
                    continue;
                }
            }

            let Some(handle) = self.handles.get(&key).map(|h| h.clone()) else {
                tracing::warn!(
                    vm_id = %vm.id,
                    org_id = %vm.org_id,
                    project_id = %vm.project_id,
                    "Health monitor: VM handle missing"
                );
                vm.state = VmState::Error;
                vm.status.actual_state = VmState::Error;
                vm.status.last_error = Some("VM handle missing".to_string());
                self.vms.insert(key, vm.clone());
                self.persist_vm(&vm).await;
                continue;
            };

            let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
                tracing::warn!(
                    vm_id = %vm.id,
                    hypervisor = ?vm.hypervisor,
                    "Health monitor: hypervisor backend not available"
                );
                vm.state = VmState::Error;
                vm.status.actual_state = VmState::Error;
                vm.status.last_error = Some("Hypervisor backend not available".to_string());
                self.vms.insert(key, vm.clone());
                self.persist_vm(&vm).await;
                continue;
            };

            match backend.status(&handle).await {
                Ok(status) => {
                    if self.auto_restart
                        && vm.state == VmState::Running
                        && matches!(status.actual_state, VmState::Stopped | VmState::Error)
                    {
                        tracing::warn!(
                            vm_id = %vm.id,
                            "Health monitor: attempting auto-restart"
                        );
                        if let Err(e) = backend.start(&handle).await {
                            vm.state = VmState::Error;
                            vm.status.actual_state = VmState::Error;
                            vm.status.last_error = Some(e.to_string());
                            self.vms.insert(key, vm.clone());
                            self.persist_vm(&vm).await;
                            continue;
                        }
                        if let Ok(status) = backend.status(&handle).await {
                            vm.status = status.clone();
                            vm.state = status.actual_state;
                            self.vms.insert(key, vm.clone());
                            self.persist_vm(&vm).await;
                            continue;
                        }
                    }

                    vm.status = status.clone();
                    vm.state = status.actual_state;
                    self.vms.insert(key, vm.clone());
                    self.persist_vm(&vm).await;
                }
                Err(e) => {
                    tracing::warn!(
                        vm_id = %vm.id,
                        error = %e,
                        "Health monitor: status check failed"
                    );
                    vm.state = VmState::Error;
                    vm.status.actual_state = VmState::Error;
                    vm.status.last_error = Some(e.to_string());
                    self.vms.insert(key, vm.clone());
                    self.persist_vm(&vm).await;
                }
            }
        }
    }

    async fn refresh_node_health(&self, timeout: Duration) {
        let now = Self::now_epoch();
        let Ok(nodes) = self.store.list_nodes().await else {
            return;
        };

        let mut unhealthy = HashSet::new();
        for mut node in nodes.iter().cloned() {
            let age = now.saturating_sub(node.last_heartbeat);
            let mut changed = false;
            if age > timeout.as_secs()
                && !matches!(node.state, NodeState::Cordoned | NodeState::Maintenance)
            {
                if node.state != NodeState::NotReady {
                    node.state = NodeState::NotReady;
                    changed = true;
                }
            }

            if changed {
                self.persist_node(&node).await;
            }
            self.nodes.insert(node.id.to_string(), node.clone());

            if node.state == NodeState::NotReady {
                unhealthy.insert(node.id.to_string());
            }
        }

        if unhealthy.is_empty() {
            return;
        }

        let mut failed_over: HashSet<String> = HashSet::new();
        if self.failover_enabled {
            failed_over = self
                .failover_vms_on_unhealthy(&unhealthy, &nodes, self.failover_min_interval_secs)
                .await;
        }

        let entries: Vec<(TenantKey, plasmavmc_types::VirtualMachine)> = self
            .vms
            .iter()
            .map(|entry| (entry.key().clone(), entry.value().clone()))
            .collect();

        for (key, mut vm) in entries {
            let vm_id = vm.id.to_string();
            if failed_over.contains(&vm_id) {
                continue;
            }
            let Some(node_id) = vm.node_id.as_ref() else {
                continue;
            };
            if !unhealthy.contains(node_id.as_str()) {
                continue;
            }
            if matches!(vm.state, VmState::Running | VmState::Starting) {
                vm.state = VmState::Error;
                vm.status.actual_state = VmState::Error;
                vm.status.last_error = Some("Host not ready".to_string());
                self.vms.insert(key, vm.clone());
                self.persist_vm(&vm).await;
            }
        }
    }

    async fn failover_vms_on_unhealthy(
        &self,
        unhealthy: &HashSet<String>,
        nodes: &[Node],
        min_interval_secs: u64,
    ) -> HashSet<String> {
        let now = Self::now_epoch();
        let mut succeeded = HashSet::new();

        let entries: Vec<(TenantKey, plasmavmc_types::VirtualMachine)> = self
            .vms
            .iter()
            .map(|entry| (entry.key().clone(), entry.value().clone()))
            .collect();

        for (key, mut vm) in entries {
            let Some(node_id) = vm.node_id.as_ref() else {
                continue;
            };
            if !unhealthy.contains(node_id.as_str()) {
                continue;
            }
            if !matches!(vm.state, VmState::Running | VmState::Starting) {
                continue;
            }

            if let Some(last) = vm.metadata.get(FAILOVER_META_KEY) {
                if let Ok(ts) = last.parse::<u64>() {
                    if now.saturating_sub(ts) < min_interval_secs {
                        continue;
                    }
                }
            }

            let Some(target) = self.select_failover_target(nodes, &vm) else {
                vm.status.last_error = Some("No eligible failover target".to_string());
                self.vms.insert(key, vm.clone());
                self.persist_vm(&vm).await;
                continue;
            };

            match self.failover_vm_to_target(&mut vm, &target).await {
                Ok(()) => {
                    succeeded.insert(vm.id.to_string());
                    self.vms.insert(key.clone(), vm.clone());
                    self.persist_vm(&vm).await;
                    let _ = self
                        .store
                        .delete_handle(&vm.org_id, &vm.project_id, &vm.id.to_string())
                        .await;
                    self.handles.remove(&key);
                }
                Err(e) => {
                    vm.status.last_error = Some(e.to_string());
                    self.vms.insert(key, vm.clone());
                    self.persist_vm(&vm).await;
                }
            }
        }

        succeeded
    }

    fn select_failover_target(
        &self,
        nodes: &[Node],
        vm: &plasmavmc_types::VirtualMachine,
    ) -> Option<Node> {
        let current = vm.node_id.as_ref().map(|n| n.as_str().to_string());
        for node in nodes {
            if node.state != NodeState::Ready {
                continue;
            }
            if let Some(ref cur) = current {
                if node.id.as_str() == cur {
                    continue;
                }
            }
            if !node.labels.contains_key(NODE_ENDPOINT_LABEL) {
                continue;
            }
            if !node.hypervisors.is_empty() && !node.hypervisors.contains(&vm.hypervisor) {
                continue;
            }
            return Some(node.clone());
        }
        None
    }

    async fn failover_vm_to_target(
        &self,
        vm: &mut plasmavmc_types::VirtualMachine,
        target: &Node,
    ) -> Result<(), Status> {
        let endpoint = target
            .labels
            .get(NODE_ENDPOINT_LABEL)
            .ok_or_else(|| Status::failed_precondition("Destination node missing endpoint"))?;
        let mut client = Self::connect_vm_service(endpoint).await?;

        let mut req = Request::new(RecoverVmRequest {
            org_id: vm.org_id.clone(),
            project_id: vm.project_id.clone(),
            vm_id: vm.id.to_string(),
            name: vm.name.clone(),
            spec: Some(Self::types_spec_to_proto(&vm.spec)),
            hypervisor: Self::map_hv_proto(vm.hypervisor) as i32,
            metadata: vm.metadata.clone(),
            labels: vm.labels.clone(),
            start: true,
        });
        self.attach_internal_auth(&mut req, &vm.org_id, &vm.project_id)
            .await?;
        client.recover_vm(req).await.map_err(|e| {
            Status::failed_precondition(format!("RecoverVm failed: {}", e.message()))
        })?;

        vm.node_id = Some(NodeId::new(target.id.to_string()));
        vm.state = VmState::Starting;
        vm.status.actual_state = VmState::Starting;
        vm.metadata
            .insert(FAILOVER_META_KEY.to_string(), Self::now_epoch().to_string());
        vm.metadata
            .insert(FAILOVER_TARGET_KEY.to_string(), target.id.to_string());
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use async_trait::async_trait;
    use iam_api::{
        iam_admin_server::IamAdminServer, iam_authz_server::IamAuthzServer,
        iam_token_server::IamTokenServer, IamAdminService, IamAuthzService, IamTokenService,
    };
    use iam_authn::{InternalTokenConfig, InternalTokenService, SigningKey};
    use iam_authz::{PolicyCache, PolicyEvaluator};
    use iam_store::{
        Backend, BindingStore, GroupStore, OrgStore, PrincipalStore, ProjectStore, RoleStore,
        TokenStore,
    };
    use iam_types::{PolicyBinding, Principal, PrincipalRef, Scope};
    use plasmavmc_api::proto::{
        CpuSpec as ProtoCpuSpec, HypervisorType as ProtoHypervisorType,
        MemorySpec as ProtoMemorySpec, NetworkSpec as ProtoNetworkSpec,
    };
    use plasmavmc_hypervisor::{BackendCapabilities, HypervisorBackend, UnsupportedReason};
    use plasmavmc_types::{AttachedDisk, VmHandle, VmSpec, VmStatus};
    use prismnet_api::{
        port_service_server::PortServiceServer, subnet_service_server::SubnetServiceServer,
    };
    use prismnet_server::{NetworkMetadataStore, OvnClient, PortServiceImpl, SubnetServiceImpl};
    use prismnet_types::{
        DeviceType as PrismnetDeviceType, Port, PortId as PrismnetPortId, Subnet, Vpc,
    };
    use std::net::SocketAddr;
    use std::sync::Mutex;
    use tempfile::{tempdir, TempDir};
    use tokio_stream::wrappers::TcpListenerStream;
    use tonic::transport::Server;

    #[derive(Default)]
    struct MockBackend {
        created: Mutex<Vec<plasmavmc_types::VirtualMachine>>,
        deleted: Mutex<Vec<String>>,
    }

    impl MockBackend {
        fn last_created_vm(&self) -> Option<plasmavmc_types::VirtualMachine> {
            self.created.lock().unwrap().last().cloned()
        }
    }

    #[async_trait]
    impl HypervisorBackend for MockBackend {
        fn backend_type(&self) -> HypervisorType {
            HypervisorType::Kvm
        }

        fn capabilities(&self) -> BackendCapabilities {
            BackendCapabilities::default()
        }

        fn supports(&self, _spec: &VmSpec) -> std::result::Result<(), UnsupportedReason> {
            Ok(())
        }

        async fn create(
            &self,
            vm: &plasmavmc_types::VirtualMachine,
            disks: &[AttachedDisk],
        ) -> plasmavmc_types::Result<VmHandle> {
            self.created.lock().unwrap().push(vm.clone());
            let mut handle = VmHandle::new(vm.id, format!("/tmp/{}", vm.id));
            handle.attached_disks = disks.to_vec();
            Ok(handle)
        }

        async fn start(&self, _handle: &VmHandle) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn stop(
            &self,
            _handle: &VmHandle,
            _timeout: Duration,
        ) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn kill(&self, _handle: &VmHandle) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn reboot(&self, _handle: &VmHandle) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn migrate(
            &self,
            _handle: &VmHandle,
            _destination_uri: &str,
            _timeout: Duration,
            _wait: bool,
        ) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn prepare_incoming(
            &self,
            vm: &plasmavmc_types::VirtualMachine,
            _listen_uri: &str,
            disks: &[AttachedDisk],
        ) -> plasmavmc_types::Result<VmHandle> {
            self.create(vm, disks).await
        }

        async fn delete(&self, handle: &VmHandle) -> plasmavmc_types::Result<()> {
            self.deleted.lock().unwrap().push(handle.vm_id.to_string());
            Ok(())
        }

        async fn status(&self, _handle: &VmHandle) -> plasmavmc_types::Result<VmStatus> {
            Ok(VmStatus {
                actual_state: VmState::Stopped,
                ..VmStatus::default()
            })
        }

        async fn attach_disk(
            &self,
            _handle: &VmHandle,
            _disk: &AttachedDisk,
        ) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn detach_disk(
            &self,
            _handle: &VmHandle,
            _disk_id: &str,
        ) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn attach_nic(
            &self,
            _handle: &VmHandle,
            _nic: &NetworkSpec,
        ) -> plasmavmc_types::Result<()> {
            Ok(())
        }

        async fn detach_nic(
            &self,
            _handle: &VmHandle,
            _nic_id: &str,
        ) -> plasmavmc_types::Result<()> {
            Ok(())
        }
    }

    async fn wait_for_test_tcp(addr: SocketAddr) {
        let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
        loop {
            if tokio::net::TcpStream::connect(addr).await.is_ok() {
                return;
            }
            assert!(
                tokio::time::Instant::now() < deadline,
                "timed out waiting for test listener {}",
                addr
            );
            tokio::time::sleep(Duration::from_millis(25)).await;
        }
    }

    async fn start_test_iam_server() -> String {
        let backend = Arc::new(Backend::memory());
        let principal_store = Arc::new(PrincipalStore::new(backend.clone()));
        let role_store = Arc::new(RoleStore::new(backend.clone()));
        let binding_store = Arc::new(BindingStore::new(backend.clone()));
        let token_store = Arc::new(TokenStore::new(backend.clone()));
        let group_store = Arc::new(GroupStore::new(backend.clone()));
        let org_store = Arc::new(OrgStore::new(backend.clone()));
        let project_store = Arc::new(ProjectStore::new(backend));

        role_store.init_builtin_roles().await.unwrap();
        principal_store
            .create(&Principal::new_user("user-1", "User One"))
            .await
            .unwrap();
        binding_store
            .create(&PolicyBinding::new(
                "binding-user-1-project-admin",
                PrincipalRef::user("user-1"),
                "roles/ProjectAdmin",
                Scope::project("proj-1", "org-1"),
            ))
            .await
            .unwrap();

        let cache = Arc::new(PolicyCache::default_config());
        let evaluator = Arc::new(PolicyEvaluator::with_group_store(
            binding_store.clone(),
            role_store.clone(),
            group_store.clone(),
            cache,
        ));
        let token_service = Arc::new(InternalTokenService::new(InternalTokenConfig::new(
            SigningKey::generate("vm-service-test-key"),
            "vm-service-test",
        )));
        let authz_service = IamAuthzService::new(evaluator.clone(), principal_store.clone());
        let token_grpc_service =
            IamTokenService::new(token_service, principal_store.clone(), token_store, None);
        let admin_service = IamAdminService::new(
            principal_store,
            role_store,
            binding_store,
            org_store,
            project_store,
            group_store,
        )
        .with_evaluator(evaluator);

        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        tokio::spawn(async move {
            Server::builder()
                .add_service(IamAuthzServer::new(authz_service))
                .add_service(IamTokenServer::new(token_grpc_service))
                .add_service(IamAdminServer::new(admin_service))
                .serve_with_incoming(TcpListenerStream::new(listener))
                .await
                .unwrap();
        });

        wait_for_test_tcp(addr).await;
        format!("http://{}", addr)
    }

    async fn start_test_prismnet_server(
        iam_endpoint: &str,
        metadata: Arc<NetworkMetadataStore>,
    ) -> String {
        let auth_service = Arc::new(AuthService::new(iam_endpoint).await.unwrap());
        let ovn = Arc::new(OvnClient::new_mock());
        let port_service = PortServiceImpl::new(metadata.clone(), ovn, auth_service.clone());
        let subnet_service = SubnetServiceImpl::new(metadata, auth_service.clone());
        let auth_handle = tokio::runtime::Handle::current();
        let interceptor = move |mut req: Request<()>| -> Result<Request<()>, Status> {
            let auth = auth_service.clone();
            tokio::task::block_in_place(|| {
                auth_handle.block_on(async move {
                    let tenant_context = auth.authenticate_request(&req).await?;
                    req.extensions_mut().insert(tenant_context);
                    Ok(req)
                })
            })
        };

        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        tokio::spawn(async move {
            Server::builder()
                .add_service(SubnetServiceServer::with_interceptor(
                    subnet_service,
                    interceptor.clone(),
                ))
                .add_service(PortServiceServer::with_interceptor(
                    port_service,
                    interceptor,
                ))
                .serve_with_incoming(TcpListenerStream::new(listener))
                .await
                .unwrap();
        });

        wait_for_test_tcp(addr).await;
        format!("http://{}", addr)
    }

    async fn new_test_vm_service(
        iam_endpoint: &str,
        prismnet_endpoint: Option<String>,
    ) -> (TempDir, VmServiceImpl, Arc<MockBackend>) {
        let tempdir = tempdir().unwrap();
        let registry = Arc::new(HypervisorRegistry::new());
        let backend = Arc::new(MockBackend::default());
        registry.register(backend.clone());

        let mut config = ServerConfig::default();
        config.storage.backend = crate::config::StorageBackendKind::File;
        config.storage.state_path = Some(tempdir.path().join("state"));
        config.integrations.prismnet_endpoint = prismnet_endpoint;
        config.volumes.managed_volume_root = tempdir.path().join("managed-volumes");
        config.volumes.qemu_img_path = Some(std::env::current_exe().unwrap());

        let auth = Arc::new(AuthService::new(iam_endpoint).await.unwrap());
        let service = VmServiceImpl::new(registry, auth, iam_endpoint.to_string(), &config)
            .await
            .unwrap();
        (tempdir, service, backend)
    }

    fn test_tenant() -> TenantContext {
        TenantContext {
            org_id: "org-1".to_string(),
            project_id: "proj-1".to_string(),
            principal_id: "user-1".to_string(),
            principal_name: "User One".to_string(),
            principal_kind: PrincipalKind::User,
            node_id: None,
        }
    }

    fn test_vm_request(network: Vec<ProtoNetworkSpec>) -> CreateVmRequest {
        CreateVmRequest {
            name: "test-vm".to_string(),
            org_id: "org-1".to_string(),
            project_id: "proj-1".to_string(),
            spec: Some(ProtoVmSpec {
                cpu: Some(ProtoCpuSpec {
                    vcpus: 1,
                    cores_per_socket: 1,
                    sockets: 1,
                    cpu_model: String::new(),
                }),
                memory: Some(ProtoMemorySpec {
                    size_mib: 512,
                    hugepages: false,
                }),
                disks: vec![],
                network,
                boot: None,
                security: None,
            }),
            hypervisor: ProtoHypervisorType::Kvm as i32,
            metadata: Default::default(),
            labels: Default::default(),
        }
    }

    #[test]
    fn unspecified_disk_cache_defaults_to_writeback() {
        assert_eq!(
            VmServiceImpl::map_disk_cache(ProtoDiskCache::Unspecified as i32),
            DiskCache::Writeback
        );
    }

    #[test]
    fn prismnet_vm_device_type_matches_generated_proto_enum() {
        assert_eq!(
            PRISMNET_VM_DEVICE_TYPE,
            prismnet_api::proto::DeviceType::Vm as i32
        );
        assert_ne!(
            PRISMNET_VM_DEVICE_TYPE,
            prismnet_api::proto::DeviceType::None as i32
        );
    }

    #[test]
    fn build_vm_event_classifies_lifecycle_changes() {
        let vm = plasmavmc_types::VirtualMachine::new(
            "watch-vm",
            "watch-org",
            "watch-project",
            VmSpec::default(),
        );

        let created = VmServiceImpl::build_vm_event(None, Some(&vm))
            .unwrap()
            .unwrap();
        assert_eq!(created.event_type, ProtoVmEventType::Created as i32);

        let mut updated_vm = vm.clone();
        updated_vm.updated_at += 1;
        updated_vm
            .metadata
            .insert("note".to_string(), "changed".to_string());
        let updated = VmServiceImpl::build_vm_event(Some(&vm), Some(&updated_vm))
            .unwrap()
            .unwrap();
        assert_eq!(updated.event_type, ProtoVmEventType::Updated as i32);

        let mut running_vm = updated_vm.clone();
        running_vm.state = VmState::Running;
        running_vm.status.actual_state = VmState::Running;
        let state_changed = VmServiceImpl::build_vm_event(Some(&updated_vm), Some(&running_vm))
            .unwrap()
            .unwrap();
        assert_eq!(
            state_changed.event_type,
            ProtoVmEventType::StateChanged as i32
        );

        let deleted = VmServiceImpl::build_vm_event(Some(&running_vm), None)
            .unwrap()
            .unwrap();
        assert_eq!(deleted.event_type, ProtoVmEventType::Deleted as i32);
    }

    #[test]
    fn internal_rpc_guard_requires_plasmavmc_service_account() {
        let internal = TenantContext {
            org_id: "org".to_string(),
            project_id: "project".to_string(),
            principal_id: "plasmavmc-org-project".to_string(),
            principal_name: "plasmavmc".to_string(),
            principal_kind: PrincipalKind::ServiceAccount,
            node_id: None,
        };
        let external = TenantContext {
            principal_id: "user-1".to_string(),
            principal_kind: PrincipalKind::User,
            ..internal.clone()
        };

        assert!(VmServiceImpl::ensure_internal_rpc(&internal).is_ok());
        assert!(VmServiceImpl::ensure_internal_rpc(&external).is_err());
    }

    #[test]
    fn vm_disk_reference_validation_requires_uuid_identifiers() {
        let mut spec = VmSpec::default();
        spec.disks.push(plasmavmc_types::DiskSpec {
            id: "root".to_string(),
            source: DiskSource::Image {
                image_id: "../passwd".to_string(),
            },
            size_gib: 10,
            bus: DiskBus::Virtio,
            cache: DiskCache::Writeback,
            boot_index: Some(1),
        });

        assert!(VmServiceImpl::validate_vm_disk_references(&spec).is_err());
    }

    #[tokio::test]
    async fn vm_network_reference_validation_requires_subnet_for_port_id() {
        let iam_endpoint = start_test_iam_server().await;
        let (_tempdir, service, backend) = new_test_vm_service(&iam_endpoint, None).await;
        let mut request = Request::new(test_vm_request(vec![ProtoNetworkSpec {
            id: "nic0".to_string(),
            network_id: "default".to_string(),
            subnet_id: String::new(),
            port_id: Uuid::new_v4().to_string(),
            mac_address: String::new(),
            ip_address: String::new(),
            cidr_block: String::new(),
            gateway_ip: String::new(),
            dhcp_enabled: false,
            model: ProtoNicModel::VirtioNet as i32,
            security_groups: vec![],
        }]));
        request.extensions_mut().insert(test_tenant());

        let error = service.create_vm(request).await.unwrap_err();
        assert_eq!(error.code(), tonic::Code::InvalidArgument);
        assert!(backend.last_created_vm().is_none());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn create_vm_rejects_unknown_security_group_reference() {
        let iam_endpoint = start_test_iam_server().await;
        let metadata = Arc::new(NetworkMetadataStore::new_in_memory());
        let vpc = Vpc::new("tenant-vpc", "org-1", "proj-1", "10.0.0.0/16");
        metadata.create_vpc(vpc.clone()).await.unwrap();
        let subnet = Subnet::new("tenant-subnet", vpc.id, "10.0.1.0/24");
        metadata.create_subnet(subnet.clone()).await.unwrap();
        let prismnet_endpoint = start_test_prismnet_server(&iam_endpoint, metadata.clone()).await;
        let (_tempdir, service, backend) =
            new_test_vm_service(&iam_endpoint, Some(prismnet_endpoint)).await;

        let mut create_request = Request::new(test_vm_request(vec![ProtoNetworkSpec {
            id: "nic0".to_string(),
            network_id: "default".to_string(),
            subnet_id: subnet.id.to_string(),
            port_id: String::new(),
            mac_address: String::new(),
            ip_address: String::new(),
            cidr_block: String::new(),
            gateway_ip: String::new(),
            dhcp_enabled: false,
            model: ProtoNicModel::VirtioNet as i32,
            security_groups: vec![Uuid::new_v4().to_string()],
        }]));
        create_request.extensions_mut().insert(test_tenant());

        let error = service.create_vm(create_request).await.unwrap_err();
        assert_eq!(error.code(), tonic::Code::NotFound);
        assert!(backend.last_created_vm().is_none());
        assert!(metadata
            .list_ports(Some(&subnet.id), None)
            .await
            .unwrap()
            .is_empty());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn create_vm_auto_manages_prismnet_port_lifecycle() {
        let iam_endpoint = start_test_iam_server().await;
        let metadata = Arc::new(NetworkMetadataStore::new_in_memory());
        let vpc = Vpc::new("tenant-vpc", "org-1", "proj-1", "10.0.0.0/16");
        metadata.create_vpc(vpc.clone()).await.unwrap();
        let subnet = Subnet::new("tenant-subnet", vpc.id, "10.0.1.0/24");
        metadata.create_subnet(subnet.clone()).await.unwrap();
        let prismnet_endpoint = start_test_prismnet_server(&iam_endpoint, metadata.clone()).await;
        let (_tempdir, service, backend) =
            new_test_vm_service(&iam_endpoint, Some(prismnet_endpoint)).await;

        let mut create_request = Request::new(test_vm_request(vec![ProtoNetworkSpec {
            id: "nic0".to_string(),
            network_id: "default".to_string(),
            subnet_id: subnet.id.to_string(),
            port_id: String::new(),
            mac_address: String::new(),
            ip_address: String::new(),
            cidr_block: String::new(),
            gateway_ip: String::new(),
            dhcp_enabled: false,
            model: ProtoNicModel::VirtioNet as i32,
            security_groups: vec![],
        }]));
        create_request.extensions_mut().insert(test_tenant());

        let vm = service
            .create_vm(create_request)
            .await
            .unwrap()
            .into_inner();
        let nic = vm.spec.as_ref().unwrap().network.first().unwrap();
        assert!(!nic.port_id.is_empty());
        assert!(!nic.mac_address.is_empty());
        assert!(!nic.ip_address.is_empty());

        let backend_vm = backend.last_created_vm().unwrap();
        assert!(backend_vm
            .metadata
            .contains_key(PRISMNET_AUTO_PORTS_METADATA_KEY));
        assert_eq!(
            backend_vm
                .spec
                .network
                .first()
                .and_then(|entry| entry.port_id.as_deref()),
            Some(nic.port_id.as_str())
        );

        let port_id = PrismnetPortId::from_uuid(Uuid::parse_str(&nic.port_id).unwrap());
        let port = metadata
            .get_port(&subnet.id, &port_id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(port.device_id.as_deref(), Some(vm.id.as_str()));
        assert_eq!(port.device_type, PrismnetDeviceType::Vm);

        let mut delete_request = Request::new(DeleteVmRequest {
            org_id: "org-1".to_string(),
            project_id: "proj-1".to_string(),
            vm_id: vm.id.clone(),
            force: true,
        });
        delete_request.extensions_mut().insert(test_tenant());
        service.delete_vm(delete_request).await.unwrap();

        assert!(metadata
            .get_port(&subnet.id, &port_id)
            .await
            .unwrap()
            .is_none());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn delete_vm_preserves_tenant_managed_prismnet_port() {
        let iam_endpoint = start_test_iam_server().await;
        let metadata = Arc::new(NetworkMetadataStore::new_in_memory());
        let vpc = Vpc::new("tenant-vpc", "org-1", "proj-1", "10.0.0.0/16");
        metadata.create_vpc(vpc.clone()).await.unwrap();
        let subnet = Subnet::new("tenant-subnet", vpc.id, "10.0.1.0/24");
        metadata.create_subnet(subnet.clone()).await.unwrap();

        let mut existing_port = Port::new("tenant-managed-port", subnet.id);
        existing_port.ip_address = Some("10.0.1.25".to_string());
        let existing_port_id = existing_port.id;
        metadata.create_port(existing_port).await.unwrap();

        let prismnet_endpoint = start_test_prismnet_server(&iam_endpoint, metadata.clone()).await;
        let (_tempdir, service, backend) =
            new_test_vm_service(&iam_endpoint, Some(prismnet_endpoint)).await;

        let mut create_request = Request::new(test_vm_request(vec![ProtoNetworkSpec {
            id: "nic0".to_string(),
            network_id: "default".to_string(),
            subnet_id: subnet.id.to_string(),
            port_id: existing_port_id.to_string(),
            mac_address: String::new(),
            ip_address: String::new(),
            cidr_block: String::new(),
            gateway_ip: String::new(),
            dhcp_enabled: false,
            model: ProtoNicModel::VirtioNet as i32,
            security_groups: vec![],
        }]));
        create_request.extensions_mut().insert(test_tenant());

        let vm = service
            .create_vm(create_request)
            .await
            .unwrap()
            .into_inner();
        let nic = vm.spec.as_ref().unwrap().network.first().unwrap();
        assert_eq!(nic.port_id, existing_port_id.to_string());
        assert!(!nic.mac_address.is_empty());
        assert_eq!(nic.ip_address.as_str(), "10.0.1.25");

        let backend_vm = backend.last_created_vm().unwrap();
        assert!(!backend_vm
            .metadata
            .contains_key(PRISMNET_AUTO_PORTS_METADATA_KEY));

        let port = metadata
            .get_port(&subnet.id, &existing_port_id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(port.device_id.as_deref(), Some(vm.id.as_str()));
        assert_eq!(port.device_type, PrismnetDeviceType::Vm);

        let mut delete_request = Request::new(DeleteVmRequest {
            org_id: "org-1".to_string(),
            project_id: "proj-1".to_string(),
            vm_id: vm.id.clone(),
            force: true,
        });
        delete_request.extensions_mut().insert(test_tenant());
        service.delete_vm(delete_request).await.unwrap();

        let port = metadata
            .get_port(&subnet.id, &existing_port_id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(port.device_id, None);
        assert_eq!(port.device_type, PrismnetDeviceType::None);
    }
}

impl StateSink for VmServiceImpl {
    fn on_vm_updated(
        &self,
        org_id: &str,
        project_id: &str,
        vm_id: &str,
        vm: plasmavmc_types::VirtualMachine,
    ) {
        let key = TenantKey::new(org_id, project_id, vm_id);
        self.vms.insert(key, vm);
    }

    fn on_vm_deleted(&self, org_id: &str, project_id: &str, vm_id: &str) {
        let key = TenantKey::new(org_id, project_id, vm_id);
        self.vms.remove(&key);
        self.handles.remove(&key);
    }

    fn on_handle_updated(
        &self,
        org_id: &str,
        project_id: &str,
        vm_id: &str,
        handle: plasmavmc_types::VmHandle,
    ) {
        let key = TenantKey::new(org_id, project_id, vm_id);
        self.handles.insert(key, handle);
    }

    fn on_handle_deleted(&self, org_id: &str, project_id: &str, vm_id: &str) {
        let key = TenantKey::new(org_id, project_id, vm_id);
        self.handles.remove(&key);
    }

    fn on_node_updated(&self, node_id: &str, node: Node) {
        self.nodes.insert(node_id.to_string(), node);
    }

    fn on_node_deleted(&self, node_id: &str) {
        self.nodes.remove(node_id);
    }
}

#[tonic::async_trait]
impl VmService for VmServiceImpl {
    async fn create_vm(
        &self,
        request: Request<CreateVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_CREATE,
                &resource_for_tenant("instance", "*", &org_id, &project_id),
            )
            .await?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        tracing::info!(
            name = %req.name,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "CreateVm request"
        );

        let hv = self.map_hv(
            ProtoHypervisorType::try_from(req.hypervisor).unwrap_or(ProtoHypervisorType::Kvm),
        );
        if req.spec.is_none() {
            return Err(Status::invalid_argument("spec is required"));
        }
        let spec = Self::proto_spec_to_types(req.spec.clone());
        Self::validate_vm_disk_references(&spec)?;
        Self::validate_vm_network_references(&spec)?;
        if self.is_control_plane_scheduler() {
            if let Some(target) = self
                .select_target_node(hv, &req.org_id, &req.project_id, &spec)
                .await
            {
                if let Some(endpoint) = target.labels.get(NODE_ENDPOINT_LABEL) {
                    let forwarded = self
                        .forward_create_to_node(endpoint, &req.org_id, &req.project_id, &req)
                        .await?;
                    let forwarded_vm = self.proto_vm_to_types(&forwarded)?;
                    let key = TenantKey::new(
                        &forwarded_vm.org_id,
                        &forwarded_vm.project_id,
                        forwarded_vm.id.to_string(),
                    );
                    self.vms.insert(key, forwarded_vm.clone());
                    self.persist_vm(&forwarded_vm).await;
                    return Ok(Response::new(forwarded));
                }
            }
        }
        let backend = self
            .hypervisor_registry
            .get(hv)
            .ok_or_else(|| Status::failed_precondition("Hypervisor not available"))?;
        let mut vm =
            plasmavmc_types::VirtualMachine::new(req.name, req.org_id, req.project_id, spec);
        vm.hypervisor = hv;
        vm.metadata = req.metadata;
        vm.labels = req.labels;
        if let Some(ref node_id) = self.local_node_id {
            vm.node_id = Some(NodeId::new(node_id.clone()));
        }

        // CreditService Admission Control (2-phase commit)
        let reservation_id: Option<String> = if let Some(ref credit_svc) = self.credit_service {
            let mut client = credit_svc.write().await;

            // Calculate estimated cost based on VM spec
            let vcpus = vm.spec.cpu.vcpus as i64;
            let memory_gb = (vm.spec.memory.size_mib / 1024) as i64;
            let estimated_hourly_cost = vcpus * 10 + memory_gb * 5; // Simple pricing: 10/vCPU + 5/GB

            // Phase 0: Check quota
            match client
                .check_quota(
                    &vm.project_id,
                    CreditResourceType::VmInstance,
                    1,
                    estimated_hourly_cost,
                )
                .await
            {
                Ok(resp) if !resp.allowed => {
                    let reason = if resp.reason.is_empty() {
                        "Insufficient quota or balance".to_string()
                    } else {
                        resp.reason
                    };
                    tracing::warn!(project_id = %vm.project_id, reason = %reason, "VM creation denied by CreditService");
                    return Err(Status::resource_exhausted(format!(
                        "Admission denied: {}",
                        reason
                    )));
                }
                Err(e) => {
                    tracing::warn!("CreditService check_quota failed (allowing request): {}", e);
                    // Continue without reservation if CreditService is unavailable
                }
                Ok(_) => {}
            }

            // Phase 1: Reserve credits
            match client
                .reserve_credits(
                    &vm.project_id,
                    estimated_hourly_cost,
                    format!("VM {} creation", vm.name),
                    "VmInstance",
                    300, // 5 minute TTL
                )
                .await
            {
                Ok(reservation) => {
                    tracing::info!(
                        reservation_id = %reservation.id,
                        amount = estimated_hourly_cost,
                        "Credits reserved for VM creation"
                    );
                    Some(reservation.id)
                }
                Err(e) => {
                    tracing::warn!("CreditService reserve_credits failed: {}", e);
                    return Err(Status::resource_exhausted(format!(
                        "Failed to reserve credits: {}",
                        e
                    )));
                }
            }
        } else {
            None
        };

        let attached_disks = match self.volume_manager.prepare_vm_volumes(&mut vm).await {
            Ok(attached_disks) => attached_disks,
            Err(error) => {
                if let (Some(ref credit_svc), Some(ref res_id)) =
                    (&self.credit_service, &reservation_id)
                {
                    let mut client = credit_svc.write().await;
                    if let Err(release_err) = client
                        .release_reservation(
                            res_id,
                            format!("VM volume preparation failed: {}", error),
                        )
                        .await
                    {
                        tracing::warn!("Failed to release reservation {}: {}", res_id, release_err);
                    }
                }
                return Err(error);
            }
        };

        if let Err(e) = self.attach_prismnet_ports(&mut vm).await {
            if let (Some(ref credit_svc), Some(ref res_id)) =
                (&self.credit_service, &reservation_id)
            {
                let mut client = credit_svc.write().await;
                if let Err(release_err) = client
                    .release_reservation(res_id, format!("VM network attachment failed: {}", e))
                    .await
                {
                    tracing::warn!("Failed to release reservation {}: {}", res_id, release_err);
                }
            }
            self.rollback_prepared_vm_resources(&vm, true).await;
            return Err(Self::map_prismnet_error(
                e.as_ref(),
                "attach PrismNET ports",
            ));
        }

        // Create VM
        let handle = match backend.create(&vm, &attached_disks).await {
            Ok(h) => h,
            Err(e) => {
                tracing::error!(
                    vm_id = %vm.id,
                    vm_name = %vm.name,
                    org_id = %vm.org_id,
                    project_id = %vm.project_id,
                    error = %e,
                    "VM create failed in hypervisor backend"
                );
                // Rollback: Release reservation on failure
                if let (Some(ref credit_svc), Some(ref res_id)) =
                    (&self.credit_service, &reservation_id)
                {
                    let mut client = credit_svc.write().await;
                    if let Err(release_err) = client
                        .release_reservation(res_id, format!("VM creation failed: {}", e))
                        .await
                    {
                        tracing::warn!("Failed to release reservation {}: {}", res_id, release_err);
                    } else {
                        tracing::info!(reservation_id = %res_id, "Released reservation after VM creation failure");
                    }
                }
                self.rollback_prepared_vm_resources(&vm, true).await;
                return Err(Self::to_status_code(e));
            }
        };

        let status = match backend.status(&handle).await {
            Ok(status) => status,
            Err(error) => {
                if let (Some(ref credit_svc), Some(ref res_id)) =
                    (&self.credit_service, &reservation_id)
                {
                    let mut client = credit_svc.write().await;
                    if let Err(release_err) = client
                        .release_reservation(
                            res_id,
                            format!("VM status failed after creation: {}", error),
                        )
                        .await
                    {
                        tracing::warn!("Failed to release reservation {}: {}", res_id, release_err);
                    }
                }
                let _ = backend.delete(&handle).await;
                self.rollback_prepared_vm_resources(&vm, true).await;
                return Err(Self::to_status_code(error));
            }
        };
        vm.status = status.clone();
        vm.state = status.actual_state;

        // Phase 2: Commit reservation on success
        if let (Some(ref credit_svc), Some(ref res_id)) = (&self.credit_service, &reservation_id) {
            let mut client = credit_svc.write().await;
            let vcpus = vm.spec.cpu.vcpus as i64;
            let memory_gb = (vm.spec.memory.size_mib / 1024) as i64;
            let actual_cost = vcpus * 10 + memory_gb * 5;

            if let Err(e) = client
                .commit_reservation(res_id, actual_cost, vm.id.to_string())
                .await
            {
                tracing::warn!("Failed to commit reservation {}: {}", res_id, e);
                // VM is already created, so we don't fail here - billing will reconcile
            } else {
                tracing::info!(reservation_id = %res_id, vm_id = %vm.id, "Committed credits for VM");
            }
        }

        let key = TenantKey::new(&vm.org_id, &vm.project_id, vm.id.to_string());
        self.vms.insert(key.clone(), vm.clone());
        self.handles.insert(key.clone(), handle.clone());

        // Persist to storage
        self.persist_vm(&vm).await;
        self.persist_handle(&vm.org_id, &vm.project_id, &vm.id.to_string(), &handle)
            .await;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn get_vm(
        &self,
        request: Request<GetVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "GetVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_READ,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        // Ensure VM is loaded from storage
        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        if self.is_control_plane_scheduler() {
            if let Some(node_id) = vm.node_id.as_ref() {
                if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await {
                    if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) {
                        let mut client = Self::connect_vm_service(endpoint).await?;
                        let mut remote_req = Request::new(GetVmRequest {
                            org_id: req.org_id.clone(),
                            project_id: req.project_id.clone(),
                            vm_id: req.vm_id.clone(),
                        });
                        self.attach_internal_auth(&mut remote_req, &req.org_id, &req.project_id)
                            .await?;
                        let remote_vm = client
                            .get_vm(remote_req)
                            .await
                            .map_err(|status| Status::from_error(Box::new(status)))?
                            .into_inner();
                        let typed_vm = self.proto_vm_to_types(&remote_vm)?;
                        self.vms.insert(key.clone(), typed_vm.clone());
                        self.persist_vm(&typed_vm).await;
                        return Ok(Response::new(remote_vm));
                    }
                }
            }
        }
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition("VM handle missing"));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        vm.status = status.clone();
        vm.state = status.actual_state;
        self.vms.insert(key, vm.clone());
        self.persist_vm(&vm).await;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn list_vms(
        &self,
        request: Request<ListVmsRequest>,
    ) -> Result<Response<ListVmsResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        tracing::info!(
            org_id = %req.org_id,
            project_id = %req.project_id,
            page_size = req.page_size,
            "ListVms request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_LIST,
                &resource_for_tenant("instance", "*", &req.org_id, &req.project_id),
            )
            .await?;

        // Ensure tenant VMs are loaded
        self.ensure_tenant_loaded(&req.org_id, &req.project_id)
            .await;

        let vms: Vec<VirtualMachine> = self
            .vms
            .iter()
            .filter(|entry| {
                entry.key().org_id == req.org_id && entry.key().project_id == req.project_id
            })
            .map(|vm| Self::to_proto_vm(&vm, vm.status.clone()))
            .collect();
        Ok(Response::new(ListVmsResponse {
            vms,
            next_page_token: String::new(),
        }))
    }

    async fn update_vm(
        &self,
        request: Request<UpdateVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "UpdateVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };

        // Update spec if provided
        if req.spec.is_some() {
            let updated_spec = Self::proto_spec_to_types(req.spec);
            Self::validate_vm_disk_references(&updated_spec)?;
            vm.spec = updated_spec;
            tracing::info!(
                vm_id = %req.vm_id,
                vcpus = vm.spec.cpu.vcpus,
                memory_mib = vm.spec.memory.size_mib,
                "Updated VM spec (changes take effect on next boot)"
            );
        }

        // Update metadata if provided
        if !req.metadata.is_empty() {
            vm.metadata = req.metadata;
        }

        // Update labels if provided
        if !req.labels.is_empty() {
            vm.labels = req.labels;
        }

        // Get current status from backend if running (before moving key)
        let status = if let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) {
            if let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) {
                backend
                    .status(&handle)
                    .await
                    .map_err(Self::to_status_code)?
            } else {
                vm.status.clone()
            }
        } else {
            vm.status.clone()
        };

        // Persist updated VM to storage
        self.vms.insert(key, vm.clone());
        self.persist_vm(&vm).await;

        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn delete_vm(
        &self,
        request: Request<DeleteVmRequest>,
    ) -> Result<Response<Empty>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            force = req.force,
            "DeleteVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_DELETE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);

        // Detach from PrismNET ports first
        if let Some(vm) = self.vms.get(&key) {
            if let Err(e) = self.detach_prismnet_ports(&vm).await {
                tracing::warn!("Failed to detach PrismNET ports: {}", e);
                // Continue anyway - we still want to delete the VM
            }
            if let Err(e) = self.delete_auto_prismnet_ports(&vm).await {
                tracing::warn!("Failed to delete auto-managed PrismNET ports: {}", e);
            }
        }

        if self.is_control_plane_scheduler() {
            if let Some(vm) = self
                .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
                .await
            {
                if let Some(node_id) = vm.node_id.as_ref() {
                    if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await {
                        if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) {
                            let mut client = Self::connect_vm_service(endpoint).await?;
                            let mut remote_req = Request::new(DeleteVmRequest {
                                org_id: req.org_id.clone(),
                                project_id: req.project_id.clone(),
                                vm_id: req.vm_id.clone(),
                                force: req.force,
                            });
                            self.attach_internal_auth(
                                &mut remote_req,
                                &req.org_id,
                                &req.project_id,
                            )
                            .await?;
                            client
                                .delete_vm(remote_req)
                                .await
                                .map_err(|status| Status::from_error(Box::new(status)))?;
                            self.vms.remove(&key);
                            self.handles.remove(&key);
                            let _ = self
                                .store
                                .delete_vm(&req.org_id, &req.project_id, &req.vm_id)
                                .await;
                            let _ = self
                                .store
                                .delete_handle(&req.org_id, &req.project_id, &req.vm_id)
                                .await;
                            return Ok(Response::new(Empty {}));
                        }
                    }
                }
            }
        }

        let vm_for_cleanup = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await;
        if let Some((_, handle)) = self.handles.remove(&key) {
            if let Some(vm) = vm_for_cleanup.as_ref() {
                if let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) {
                    let _ = backend.delete(&handle).await;
                }
            }
        }
        if let Some(vm) = vm_for_cleanup.as_ref() {
            let _ = self.volume_manager.release_vm_volumes(vm).await;
            let _ = self.volume_manager.delete_vm_managed_volumes(vm).await;
        }
        self.vms.remove(&key);
        // Delete from storage
        let _ = self
            .store
            .delete_vm(&req.org_id, &req.project_id, &req.vm_id)
            .await;
        let _ = self
            .store
            .delete_handle(&req.org_id, &req.project_id, &req.vm_id)
            .await;
        Ok(Response::new(Empty {}))
    }

    async fn start_vm(
        &self,
        request: Request<StartVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "StartVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        if self.is_control_plane_scheduler() {
            if let Some(node_id) = vm.node_id.as_ref() {
                if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await {
                    if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) {
                        let mut client = Self::connect_vm_service(endpoint).await?;
                        let mut remote_req = Request::new(StartVmRequest {
                            org_id: req.org_id.clone(),
                            project_id: req.project_id.clone(),
                            vm_id: req.vm_id.clone(),
                        });
                        self.attach_internal_auth(&mut remote_req, &req.org_id, &req.project_id)
                            .await?;
                        let remote_vm = client
                            .start_vm(remote_req)
                            .await
                            .map_err(|status| Status::from_error(Box::new(status)))?
                            .into_inner();
                        let typed_vm = self.proto_vm_to_types(&remote_vm)?;
                        self.vms.insert(key, typed_vm.clone());
                        self.persist_vm(&typed_vm).await;
                        return Ok(Response::new(remote_vm));
                    }
                }
            }
        }
        self.ensure_local_owner(&vm)?;
        let Some(handle_entry) = self.handles.get(&key) else {
            return Err(Status::failed_precondition("VM handle missing"));
        };
        let mut handle = handle_entry.clone();
        drop(handle_entry);
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };
        if !handle_qmp_socket_exists(&handle) {
            tracing::info!(
                vm_id = %req.vm_id,
                org_id = %req.org_id,
                project_id = %req.project_id,
                "Recreating local VM handle for stopped VM restart"
            );
            let attached_disks = self.volume_manager.prepare_vm_volumes(&mut vm).await?;
            handle = match backend.create(&vm, &attached_disks).await {
                Ok(handle) => handle,
                Err(error) => {
                    let _ = self.volume_manager.release_vm_volumes(&vm).await;
                    return Err(Self::to_status_code(error));
                }
            };
            self.handles.insert(key.clone(), handle.clone());
            self.persist_handle(&vm.org_id, &vm.project_id, &vm.id.to_string(), &handle)
                .await;
        }
        backend.start(&handle).await.map_err(Self::to_status_code)?;
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        vm.status = status.clone();
        vm.state = status.actual_state;
        self.vms.insert(key, vm.clone());
        self.persist_vm(&vm).await;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn stop_vm(
        &self,
        request: Request<StopVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            force = req.force,
            "StopVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        if self.is_control_plane_scheduler() {
            if let Some(node_id) = vm.node_id.as_ref() {
                if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await {
                    if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) {
                        let mut client = Self::connect_vm_service(endpoint).await?;
                        let mut remote_req = Request::new(StopVmRequest {
                            org_id: req.org_id.clone(),
                            project_id: req.project_id.clone(),
                            vm_id: req.vm_id.clone(),
                            force: req.force,
                            timeout_seconds: req.timeout_seconds,
                        });
                        self.attach_internal_auth(&mut remote_req, &req.org_id, &req.project_id)
                            .await?;
                        let remote_vm = client
                            .stop_vm(remote_req)
                            .await
                            .map_err(|status| Status::from_error(Box::new(status)))?
                            .into_inner();
                        let typed_vm = self.proto_vm_to_types(&remote_vm)?;
                        self.vms.insert(key, typed_vm.clone());
                        self.persist_vm(&typed_vm).await;
                        return Ok(Response::new(remote_vm));
                    }
                }
            }
        }
        self.ensure_local_owner(&vm)?;
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition("VM handle missing"));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };
        let timeout = Duration::from_secs(if req.timeout_seconds == 0 {
            5
        } else {
            req.timeout_seconds as u64
        });
        backend
            .stop(&handle, timeout)
            .await
            .map_err(Self::to_status_code)?;
        self.volume_manager.flush_vm_volumes(&vm).await?;
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        vm.status = status.clone();
        vm.state = status.actual_state;
        self.vms.insert(key, vm.clone());
        self.persist_vm(&vm).await;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn reboot_vm(
        &self,
        request: Request<RebootVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "RebootVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        if self.is_control_plane_scheduler() {
            if let Some(node_id) = vm.node_id.as_ref() {
                if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await {
                    if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) {
                        let mut client = Self::connect_vm_service(endpoint).await?;
                        let mut remote_req = Request::new(RebootVmRequest {
                            org_id: req.org_id.clone(),
                            project_id: req.project_id.clone(),
                            vm_id: req.vm_id.clone(),
                        });
                        self.attach_internal_auth(&mut remote_req, &req.org_id, &req.project_id)
                            .await?;
                        let remote_vm = client
                            .reboot_vm(remote_req)
                            .await
                            .map_err(|status| Status::from_error(Box::new(status)))?
                            .into_inner();
                        let typed_vm = self.proto_vm_to_types(&remote_vm)?;
                        self.vms.insert(key, typed_vm.clone());
                        self.persist_vm(&typed_vm).await;
                        return Ok(Response::new(remote_vm));
                    }
                }
            }
        }
        self.ensure_local_owner(&vm)?;
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition("VM handle missing"));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };
        backend
            .reboot(&handle)
            .await
            .map_err(Self::to_status_code)?;
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        vm.status = status.clone();
        vm.state = status.actual_state;
        self.vms.insert(key, vm.clone());
        self.persist_vm(&vm).await;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn reset_vm(
        &self,
        request: Request<ResetVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "ResetVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        if self.is_control_plane_scheduler() {
            if let Some(node_id) = vm.node_id.as_ref() {
                if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await {
                    if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) {
                        let mut client = Self::connect_vm_service(endpoint).await?;
                        let mut remote_req = Request::new(ResetVmRequest {
                            org_id: req.org_id.clone(),
                            project_id: req.project_id.clone(),
                            vm_id: req.vm_id.clone(),
                        });
                        self.attach_internal_auth(&mut remote_req, &req.org_id, &req.project_id)
                            .await?;
                        let remote_vm = client
                            .reset_vm(remote_req)
                            .await
                            .map_err(|status| Status::from_error(Box::new(status)))?
                            .into_inner();
                        let typed_vm = self.proto_vm_to_types(&remote_vm)?;
                        self.vms.insert(key, typed_vm.clone());
                        self.persist_vm(&typed_vm).await;
                        return Ok(Response::new(remote_vm));
                    }
                }
            }
        }
        self.ensure_local_owner(&vm)?;
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition("VM handle missing"));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };

        // Reset via QMP system_reset (same as reboot, but for hard reset semantics)
        backend
            .reboot(&handle)
            .await
            .map_err(Self::to_status_code)?;
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        vm.status = status.clone();
        vm.state = status.actual_state;
        self.vms.insert(key, vm.clone());
        self.persist_vm(&vm).await;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn migrate_vm(
        &self,
        request: Request<MigrateVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            destination_node_id = %req.destination_node_id,
            "MigrateVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        if req.destination_node_id.is_empty() {
            return Err(Status::invalid_argument("destination_node_id is required"));
        }

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        if self.is_control_plane_scheduler() {
            if let Some(node_id) = vm.node_id.as_ref() {
                if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await {
                    if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) {
                        let mut client = Self::connect_vm_service(endpoint).await?;
                        let mut remote_req = Request::new(MigrateVmRequest {
                            org_id: req.org_id.clone(),
                            project_id: req.project_id.clone(),
                            vm_id: req.vm_id.clone(),
                            destination_node_id: req.destination_node_id.clone(),
                            timeout_seconds: req.timeout_seconds,
                            wait: req.wait,
                        });
                        self.attach_internal_auth(&mut remote_req, &req.org_id, &req.project_id)
                            .await?;
                        let remote_vm = client
                            .migrate_vm(remote_req)
                            .await
                            .map_err(|status| Status::from_error(Box::new(status)))?
                            .into_inner();
                        let typed_vm = self.proto_vm_to_types(&remote_vm)?;
                        self.vms.insert(key, typed_vm.clone());
                        self.persist_vm(&typed_vm).await;
                        return Ok(Response::new(remote_vm));
                    }
                }
            }
        }
        self.ensure_local_owner(&vm)?;
        let Some(handle_entry) = self.handles.get(&key) else {
            return Err(Status::failed_precondition("VM handle missing"));
        };
        let handle = handle_entry.clone();
        drop(handle_entry);
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };
        let backend_supports_live_migration = backend.capabilities().live_migration;
        if let Some(local_node) = self.local_node_id.as_deref() {
            if !req.destination_node_id.is_empty() && req.destination_node_id == local_node {
                return Err(Status::invalid_argument(
                    "destination_node_id must not be the local node",
                ));
            }
        }

        if !req.destination_node_id.is_empty() {
            let Some(dest_node) = self.ensure_node_loaded(&req.destination_node_id).await else {
                return Err(Status::not_found("Destination node not found"));
            };
            let Some(endpoint) = dest_node.labels.get(NODE_ENDPOINT_LABEL) else {
                return Err(Status::failed_precondition(
                    "Destination node missing plasmavmc_endpoint label",
                ));
            };
            let (required_drivers, required_storage_classes) = self
                .required_storage_for_spec(&vm.org_id, &vm.project_id, &vm.spec)
                .await?;
            if !Self::node_supports_storage(
                &dest_node,
                &required_drivers,
                &required_storage_classes,
            ) {
                return Err(Status::failed_precondition(
                    "Destination node does not support the VM's required storage backends",
                ));
            }
            if !dest_node.shared_live_migration {
                if !self
                    .volume_manager
                    .supports_cold_relocation_for_vm(&vm)
                    .await?
                {
                    return Err(Status::failed_precondition(
                        "Destination node requires cold relocation, but the VM uses non-portable storage",
                    ));
                }

                let restart_on_destination = matches!(
                    vm.state,
                    VmState::Running | VmState::Starting | VmState::Migrating
                );
                let stop_timeout = Duration::from_secs(if req.timeout_seconds == 0 {
                    120
                } else {
                    req.timeout_seconds as u64
                });
                if restart_on_destination {
                    backend
                        .stop(&handle, stop_timeout)
                        .await
                        .map_err(Self::to_status_code)?;
                    self.volume_manager.flush_vm_volumes(&vm).await?;
                    let status = backend
                        .status(&handle)
                        .await
                        .map_err(Self::to_status_code)?;
                    vm.status = status.clone();
                    vm.state = status.actual_state;
                    self.vms.insert(key.clone(), vm.clone());
                    self.persist_vm(&vm).await;
                }

                self.volume_manager.release_vm_volumes(&vm).await?;
                backend
                    .delete(&handle)
                    .await
                    .map_err(Self::to_status_code)?;
                self.handles.remove(&key);
                self.delete_persisted_handle(&vm.org_id, &vm.project_id, &vm.id.to_string())
                    .await;

                let mut client = Self::connect_vm_service(endpoint).await?;
                let mut recover_req = Request::new(RecoverVmRequest {
                    org_id: vm.org_id.clone(),
                    project_id: vm.project_id.clone(),
                    vm_id: vm.id.to_string(),
                    name: vm.name.clone(),
                    spec: Some(Self::types_spec_to_proto(&vm.spec)),
                    hypervisor: Self::map_hv_proto(vm.hypervisor) as i32,
                    metadata: vm.metadata.clone(),
                    labels: vm.labels.clone(),
                    start: restart_on_destination,
                });
                self.attach_internal_auth(&mut recover_req, &vm.org_id, &vm.project_id)
                    .await?;

                return match client.recover_vm(recover_req).await {
                    Ok(remote_vm) => {
                        let remote_vm = remote_vm.into_inner();
                        let typed_vm = self.proto_vm_to_types(&remote_vm)?;
                        self.vms.insert(key, typed_vm.clone());
                        self.persist_vm(&typed_vm).await;
                        Ok(Response::new(remote_vm))
                    }
                    Err(status) => {
                        tracing::warn!(
                            vm_id = %req.vm_id,
                            destination_node_id = %req.destination_node_id,
                            error = %status,
                            "Cold relocation failed; attempting source-side recovery"
                        );
                        let restore_result: Result<(), Status> = async {
                            let attached_disks =
                                self.volume_manager.prepare_vm_volumes(&mut vm).await?;
                            let restored_handle = match backend.create(&vm, &attached_disks).await {
                                Ok(handle) => handle,
                                Err(error) => {
                                    let _ = self.volume_manager.release_vm_volumes(&vm).await;
                                    return Err(Self::to_status_code(error));
                                }
                            };
                            if restart_on_destination {
                                if let Err(error) = backend.start(&restored_handle).await {
                                    let _ = backend.delete(&restored_handle).await;
                                    let _ = self.volume_manager.release_vm_volumes(&vm).await;
                                    return Err(Self::to_status_code(error));
                                }
                            }
                            let restored_status = match backend.status(&restored_handle).await {
                                Ok(status) => status,
                                Err(error) => {
                                    let _ = backend.delete(&restored_handle).await;
                                    let _ = self.volume_manager.release_vm_volumes(&vm).await;
                                    return Err(Self::to_status_code(error));
                                }
                            };
                            vm.status = restored_status.clone();
                            vm.state = restored_status.actual_state;
                            self.handles.insert(key.clone(), restored_handle.clone());
                            self.persist_handle(
                                &vm.org_id,
                                &vm.project_id,
                                &vm.id.to_string(),
                                &restored_handle,
                            )
                            .await;
                            Ok(())
                        }
                        .await;

                        match restore_result {
                            Ok(()) => {
                                vm.status.last_error = Some(format!(
                                    "cold relocation to {} failed: {}",
                                    req.destination_node_id,
                                    status.message()
                                ));
                            }
                            Err(restore_error) => {
                                vm.state = VmState::Error;
                                vm.status.actual_state = VmState::Error;
                                vm.status.last_error = Some(format!(
                                    "cold relocation to {} failed: {}; source recovery failed: {}",
                                    req.destination_node_id,
                                    status.message(),
                                    restore_error.message()
                                ));
                            }
                        }
                        self.vms.insert(key, vm.clone());
                        self.persist_vm(&vm).await;
                        Err(Status::failed_precondition(format!(
                            "cold relocation to {} failed: {}",
                            req.destination_node_id,
                            status.message()
                        )))
                    }
                };
            }

            if !backend_supports_live_migration {
                return Err(Status::failed_precondition(
                    "Live migration not supported by hypervisor",
                ));
            }

            let (destination_uri, listen_uri) =
                Self::derive_migration_uris(endpoint, &req.destination_node_id, &req.vm_id)?;
            let mut client = Self::connect_vm_service(endpoint).await?;
            let mut prep_req = Request::new(PrepareVmMigrationRequest {
                org_id: vm.org_id.clone(),
                project_id: vm.project_id.clone(),
                vm_id: vm.id.to_string(),
                spec: Some(Self::types_spec_to_proto(&vm.spec)),
                hypervisor: Self::map_hv_proto(vm.hypervisor) as i32,
                listen_uri,
                metadata: vm.metadata.clone(),
                labels: vm.labels.clone(),
                name: vm.name.clone(),
            });
            self.attach_internal_auth(&mut prep_req, &vm.org_id, &vm.project_id)
                .await?;
            client.prepare_vm_migration(prep_req).await.map_err(|e| {
                Status::failed_precondition(format!("Prepare migration failed: {}", e.message()))
            })?;

            let timeout = Duration::from_secs(if req.timeout_seconds == 0 {
                120
            } else {
                req.timeout_seconds as u64
            });

            vm.state = VmState::Migrating;
            vm.status.actual_state = VmState::Migrating;
            vm.status.last_error = None;
            self.vms.insert(key.clone(), vm.clone());
            self.persist_vm(&vm).await;

            return match backend
                .migrate(&handle, &destination_uri, timeout, req.wait)
                .await
            {
                Ok(()) => {
                    tracing::info!(
                        vm_id = %req.vm_id,
                        destination_node_id = %req.destination_node_id,
                        destination_uri = %destination_uri,
                        wait = req.wait,
                        "Live migration backend completed"
                    );
                    vm.node_id = Some(NodeId::new(req.destination_node_id.clone()));

                    if req.wait {
                        vm.state = VmState::Running;
                        vm.status.actual_state = VmState::Running;
                        tracing::info!(
                            vm_id = %req.vm_id,
                            destination_node_id = %req.destination_node_id,
                            "Cleaning up source runtime after completed live migration"
                        );
                        if let Err(e) = backend.delete(&handle).await {
                            tracing::warn!(
                                vm_id = %req.vm_id,
                                error = %e,
                                "Failed to clean up source VM runtime after live migration"
                            );
                        }
                        self.handles.remove(&key);
                        self.delete_persisted_handle(
                            &vm.org_id,
                            &vm.project_id,
                            &vm.id.to_string(),
                        )
                        .await;
                    }

                    self.vms.insert(key, vm.clone());
                    self.persist_vm(&vm).await;
                    Ok(Response::new(Self::to_proto_vm(&vm, vm.status.clone())))
                }
                Err(e) => {
                    vm.state = VmState::Error;
                    vm.status.actual_state = VmState::Error;
                    vm.status.last_error = Some(e.to_string());
                    self.vms.insert(key, vm.clone());
                    self.persist_vm(&vm).await;
                    Err(Self::to_status_code(e))
                }
            };
        }
        Err(Status::failed_precondition(
            "destination_node_id is required for migration",
        ))
    }

    async fn prepare_vm_migration(
        &self,
        request: Request<PrepareVmMigrationRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        Self::ensure_internal_rpc(&tenant)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            listen_uri = %req.listen_uri,
            "PrepareVmMigration request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_CREATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        if req.listen_uri.is_empty() {
            return Err(Status::invalid_argument("listen_uri is required"));
        }
        let expected_listen_uri = self.expected_migration_listener_uri(&req.vm_id)?;
        if req.listen_uri != expected_listen_uri {
            return Err(Status::invalid_argument(format!(
                "listen_uri must exactly match {expected_listen_uri}",
            )));
        }

        self.ensure_destination_slot_available(&req.org_id, &req.project_id, &req.vm_id)
            .await?;

        let vm_uuid = Uuid::parse_str(&req.vm_id)
            .map_err(|_| Status::invalid_argument("vm_id must be a UUID"))?;

        let hv = self.map_hv(
            ProtoHypervisorType::try_from(req.hypervisor).unwrap_or(ProtoHypervisorType::Kvm),
        );
        let backend = self
            .hypervisor_registry
            .get(hv)
            .ok_or_else(|| Status::failed_precondition("Hypervisor not available"))?;
        if !backend.capabilities().live_migration {
            return Err(Status::failed_precondition(
                "Live migration not supported by hypervisor",
            ));
        }

        let spec = Self::proto_spec_to_types(req.spec);
        Self::validate_vm_disk_references(&spec)?;
        Self::validate_vm_network_references(&spec)?;
        let name = if req.name.is_empty() {
            req.vm_id.clone()
        } else {
            req.name.clone()
        };
        let mut vm = plasmavmc_types::VirtualMachine::new(name, req.org_id, req.project_id, spec);
        vm.id = VmId::from_uuid(vm_uuid);
        vm.hypervisor = hv;
        vm.metadata = req.metadata;
        vm.labels = req.labels;
        if let Some(ref node_id) = self.local_node_id {
            vm.node_id = Some(NodeId::new(node_id.clone()));
        }
        let attached_disks = self.volume_manager.prepare_vm_volumes(&mut vm).await?;

        let handle = backend
            .prepare_incoming(&vm, &req.listen_uri, &attached_disks)
            .await
            .map_err(Self::to_status_code)?;
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        vm.status = status.clone();
        vm.state = VmState::Migrating;

        let key = TenantKey::new(&vm.org_id, &vm.project_id, vm.id.to_string());
        self.vms.insert(key.clone(), vm.clone());
        self.handles.insert(key, handle.clone());
        self.persist_vm(&vm).await;
        self.persist_handle(&vm.org_id, &vm.project_id, &vm.id.to_string(), &handle)
            .await;

        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn recover_vm(
        &self,
        request: Request<RecoverVmRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        Self::ensure_internal_rpc(&tenant)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;

        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "RecoverVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_CREATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        if req.spec.is_none() {
            return Err(Status::invalid_argument("spec is required"));
        }
        let vm_uuid = Uuid::parse_str(&req.vm_id)
            .map_err(|_| Status::invalid_argument("vm_id must be a UUID"))?;

        let hv = self.map_hv(
            ProtoHypervisorType::try_from(req.hypervisor).unwrap_or(ProtoHypervisorType::Kvm),
        );
        let backend = self
            .hypervisor_registry
            .get(hv)
            .ok_or_else(|| Status::failed_precondition("Hypervisor not available"))?;

        self.ensure_destination_slot_available(&req.org_id, &req.project_id, &req.vm_id)
            .await?;

        let spec = Self::proto_spec_to_types(req.spec);
        Self::validate_vm_disk_references(&spec)?;
        let name = if req.name.is_empty() {
            req.vm_id.clone()
        } else {
            req.name.clone()
        };
        let mut vm = plasmavmc_types::VirtualMachine::new(name, req.org_id, req.project_id, spec);
        vm.id = VmId::from_uuid(vm_uuid);
        vm.hypervisor = hv;
        vm.metadata = req.metadata;
        vm.labels = req.labels;
        if let Some(ref node_id) = self.local_node_id {
            vm.node_id = Some(NodeId::new(node_id.clone()));
        }
        let attached_disks = self.volume_manager.prepare_vm_volumes(&mut vm).await?;

        if let Err(e) = self.attach_prismnet_ports(&mut vm).await {
            self.rollback_prepared_vm_resources(&vm, false).await;
            return Err(Self::map_prismnet_error(
                e.as_ref(),
                "attach PrismNET ports",
            ));
        }

        let handle = match backend.create(&vm, &attached_disks).await {
            Ok(handle) => handle,
            Err(error) => {
                self.rollback_prepared_vm_resources(&vm, false).await;
                return Err(Self::to_status_code(error));
            }
        };
        if req.start {
            if let Err(error) = backend.start(&handle).await {
                let _ = backend.delete(&handle).await;
                self.rollback_prepared_vm_resources(&vm, false).await;
                return Err(Self::to_status_code(error));
            }
        }
        let status = match backend.status(&handle).await {
            Ok(status) => status,
            Err(error) => {
                let _ = backend.delete(&handle).await;
                self.rollback_prepared_vm_resources(&vm, false).await;
                return Err(Self::to_status_code(error));
            }
        };
        vm.status = status.clone();
        vm.state = status.actual_state;

        let key = TenantKey::new(&vm.org_id, &vm.project_id, vm.id.to_string());
        self.vms.insert(key.clone(), vm.clone());
        self.handles.insert(key, handle.clone());
        self.persist_vm(&vm).await;
        self.persist_handle(&vm.org_id, &vm.project_id, &vm.id.to_string(), &handle)
            .await;

        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn attach_disk(
        &self,
        request: Request<AttachDiskRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "AttachDisk request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        self.ensure_local_owner(&vm)?;
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition(
                "VM handle missing (VM not running?)",
            ));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };

        // Convert proto DiskSpec to domain type
        let proto_disk = req
            .disk
            .ok_or_else(|| Status::invalid_argument("disk spec required"))?;
        let mut disk_spec = Self::proto_disk_to_types(proto_disk);
        Self::validate_disk_reference(&disk_spec)?;
        if vm.spec.disks.iter().any(|disk| disk.id == disk_spec.id) {
            return Err(Status::already_exists("disk already attached"));
        }
        let mut staged_vm = vm.clone();
        staged_vm.spec.disks = vec![disk_spec.clone()];
        let mut attached_disks = self
            .volume_manager
            .prepare_vm_volumes(&mut staged_vm)
            .await?;
        disk_spec = staged_vm
            .spec
            .disks
            .first()
            .cloned()
            .ok_or_else(|| Status::internal("failed to materialize disk spec"))?;
        let attached_disk = attached_disks
            .pop()
            .ok_or_else(|| Status::internal("failed to resolve attached disk"))?;

        // Attach disk via backend
        if let Err(error) = backend.attach_disk(&handle, &attached_disk).await {
            let _ = self
                .volume_manager
                .rollback_vm_volumes(&staged_vm, true)
                .await;
            return Err(Self::to_status_code(error));
        }

        vm.spec.disks.push(disk_spec);
        self.vms.insert(key.clone(), vm.clone());
        self.persist_vm(&vm).await;

        // Get updated status and return
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn detach_disk(
        &self,
        request: Request<DetachDiskRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            disk_id = %req.disk_id,
            "DetachDisk request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(mut vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        self.ensure_local_owner(&vm)?;
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition(
                "VM handle missing (VM not running?)",
            ));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };

        let detached_volume_id = vm.spec.disks.iter().find_map(|disk| {
            if disk.id == req.disk_id {
                match &disk.source {
                    DiskSource::Volume { volume_id } => Some(volume_id.clone()),
                    _ => None,
                }
            } else {
                None
            }
        });

        // Detach disk via backend
        backend
            .detach_disk(&handle, &req.disk_id)
            .await
            .map_err(Self::to_status_code)?;
        if let Some(volume_id) = detached_volume_id.as_deref() {
            self.volume_manager
                .release_volume_attachment(&req.org_id, &req.project_id, volume_id, &req.vm_id)
                .await?;
        }
        vm.spec.disks.retain(|disk| disk.id != req.disk_id);
        self.vms.insert(key.clone(), vm.clone());
        self.persist_vm(&vm).await;

        // Get updated status and return
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn attach_nic(
        &self,
        request: Request<AttachNicRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "AttachNic request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        self.ensure_local_owner(&vm)?;
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition(
                "VM handle missing (VM not running?)",
            ));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };

        // Convert proto NetworkSpec to domain type
        let proto_nic = req
            .nic
            .ok_or_else(|| Status::invalid_argument("nic spec required"))?;
        let nic_spec = Self::proto_nic_to_types(proto_nic);

        // Attach NIC via backend
        backend
            .attach_nic(&handle, &nic_spec)
            .await
            .map_err(Self::to_status_code)?;

        // Get updated status and return
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    async fn detach_nic(
        &self,
        request: Request<DetachNicRequest>,
    ) -> Result<Response<VirtualMachine>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        Self::require_uuid(&req.vm_id, "vm_id")?;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            nic_id = %req.nic_id,
            "DetachNic request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_UPDATE,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let key = TenantKey::new(&req.org_id, &req.project_id, &req.vm_id);
        let Some(vm) = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
        else {
            return Err(Status::not_found("VM not found"));
        };
        self.ensure_local_owner(&vm)?;
        let Some(handle) = self.handles.get(&key).map(|entry| entry.clone()) else {
            return Err(Status::failed_precondition(
                "VM handle missing (VM not running?)",
            ));
        };
        let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else {
            return Err(Status::failed_precondition("Hypervisor not available"));
        };

        // Detach NIC via backend
        backend
            .detach_nic(&handle, &req.nic_id)
            .await
            .map_err(Self::to_status_code)?;

        // Get updated status and return
        let status = backend
            .status(&handle)
            .await
            .map_err(Self::to_status_code)?;
        Ok(Response::new(Self::to_proto_vm(&vm, status)))
    }

    type WatchVmStream = ReceiverStream<Result<VmEvent, Status>>;

    async fn watch_vm(
        &self,
        request: Request<WatchVmRequest>,
    ) -> Result<Response<Self::WatchVmStream>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let mut req = request.into_inner();
        req.org_id = org_id;
        req.project_id = project_id;
        tracing::info!(
            vm_id = %req.vm_id,
            org_id = %req.org_id,
            project_id = %req.project_id,
            "WatchVm request"
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_VM_READ,
                &resource_for_tenant("instance", req.vm_id.clone(), &req.org_id, &req.project_id),
            )
            .await?;

        let initial_vm = self
            .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id)
            .await
            .ok_or_else(|| Status::not_found("VM not found"))?;
        let poll_interval = self.watch_poll_interval();
        let store = Arc::clone(&self.store);
        let org_id = req.org_id.clone();
        let project_id = req.project_id.clone();
        let vm_id = req.vm_id.clone();
        let (tx, rx) = tokio::sync::mpsc::channel(32);

        tokio::spawn(async move {
            let mut last_seen = Some(initial_vm);
            let mut ticker = tokio::time::interval(poll_interval);
            ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);

            loop {
                ticker.tick().await;
                let next_vm = match tokio::time::timeout(
                    STORE_OP_TIMEOUT,
                    store.load_vm(&org_id, &project_id, &vm_id),
                )
                .await
                {
                    Ok(Ok(vm)) => vm,
                    Ok(Err(error)) => {
                        let _ = tx
                            .send(Err(Status::unavailable(format!(
                                "VM watch poll failed: {error}"
                            ))))
                            .await;
                        break;
                    }
                    Err(_) => {
                        let _ = tx
                            .send(Err(Status::deadline_exceeded("VM watch poll timed out")))
                            .await;
                        break;
                    }
                };

                let event = match Self::build_vm_event(last_seen.as_ref(), next_vm.as_ref()) {
                    Ok(event) => event,
                    Err(status) => {
                        let _ = tx.send(Err(status)).await;
                        break;
                    }
                };
                if let Some(event) = event {
                    if tx.send(Ok(event)).await.is_err() {
                        break;
                    }
                }

                match next_vm {
                    Some(vm) => last_seen = Some(vm),
                    None => break,
                }
            }
        });

        Ok(Response::new(ReceiverStream::new(rx)))
    }
}

#[tonic::async_trait]
impl VolumeService for VmServiceImpl {
    async fn create_volume(
        &self,
        request: Request<CreateVolumeRequest>,
    ) -> Result<Response<ProtoVolume>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_VOLUME_CREATE,
                &resource_for_tenant("volume", "*", &org_id, &project_id),
            )
            .await?;

        let mut req = request.into_inner();
        if req.name.trim().is_empty() {
            return Err(Status::invalid_argument("name is required"));
        }
        if req.size_gib == 0 {
            return Err(Status::invalid_argument(
                "size_gib must be greater than zero",
            ));
        }
        if !req.image_id.trim().is_empty() {
            Self::require_uuid(&req.image_id, "image_id")?;
        }

        let driver = Self::map_volume_driver(
            ProtoVolumeDriverKind::try_from(req.driver).unwrap_or(ProtoVolumeDriverKind::Managed),
        );
        let volume = match driver {
            VolumeDriverKind::Managed => {
                self.volume_manager
                    .create_managed_volume(
                        &org_id,
                        &project_id,
                        &req.name,
                        req.size_gib,
                        if req.storage_class.trim().is_empty() {
                            None
                        } else {
                            Some(req.storage_class.as_str())
                        },
                        if req.image_id.trim().is_empty() {
                            None
                        } else {
                            Some(req.image_id.as_str())
                        },
                        std::mem::take(&mut req.metadata),
                        std::mem::take(&mut req.labels),
                    )
                    .await?
            }
            VolumeDriverKind::CephRbd => {
                return Err(Status::invalid_argument(
                    "use RegisterExternalVolume for Ceph RBD-backed volumes",
                ));
            }
        };

        Ok(Response::new(Self::types_volume_to_proto(&volume)))
    }

    async fn get_volume(
        &self,
        request: Request<GetVolumeRequest>,
    ) -> Result<Response<ProtoVolume>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();
        self.auth
            .authorize(
                &tenant,
                ACTION_VOLUME_READ,
                &resource_for_tenant("volume", req.volume_id.clone(), &org_id, &project_id),
            )
            .await?;

        let volume = self
            .volume_manager
            .get_volume(&org_id, &project_id, &req.volume_id)
            .await?
            .ok_or_else(|| Status::not_found("volume not found"))?;
        Ok(Response::new(Self::types_volume_to_proto(&volume)))
    }

    async fn list_volumes(
        &self,
        request: Request<ListVolumesRequest>,
    ) -> Result<Response<ListVolumesResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_VOLUME_LIST,
                &resource_for_tenant("volume", "*", &org_id, &project_id),
            )
            .await?;

        let req = request.into_inner();
        let mut volumes = self
            .volume_manager
            .list_volumes(&org_id, &project_id)
            .await?;
        volumes.sort_by(|lhs, rhs| lhs.created_at.cmp(&rhs.created_at));
        let mut proto_volumes: Vec<ProtoVolume> =
            volumes.iter().map(Self::types_volume_to_proto).collect();
        if req.page_size > 0 && (req.page_size as usize) < proto_volumes.len() {
            proto_volumes.truncate(req.page_size as usize);
        }

        Ok(Response::new(ListVolumesResponse {
            volumes: proto_volumes,
            next_page_token: String::new(),
        }))
    }

    async fn delete_volume(
        &self,
        request: Request<DeleteVolumeRequest>,
    ) -> Result<Response<Empty>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();
        self.auth
            .authorize(
                &tenant,
                ACTION_VOLUME_DELETE,
                &resource_for_tenant("volume", req.volume_id.clone(), &org_id, &project_id),
            )
            .await?;

        if self
            .volume_manager
            .get_volume(&org_id, &project_id, &req.volume_id)
            .await?
            .is_none()
        {
            return Err(Status::not_found("volume not found"));
        }
        self.volume_manager
            .delete_volume(&org_id, &project_id, &req.volume_id)
            .await?;
        Ok(Response::new(Empty {}))
    }

    async fn resize_volume(
        &self,
        request: Request<ResizeVolumeRequest>,
    ) -> Result<Response<ProtoVolume>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();
        self.auth
            .authorize(
                &tenant,
                ACTION_VOLUME_UPDATE,
                &resource_for_tenant("volume", req.volume_id.clone(), &org_id, &project_id),
            )
            .await?;

        if req.size_gib == 0 {
            return Err(Status::invalid_argument(
                "size_gib must be greater than zero",
            ));
        }
        let volume = self
            .volume_manager
            .resize_volume(&org_id, &project_id, &req.volume_id, req.size_gib)
            .await?;
        Ok(Response::new(Self::types_volume_to_proto(&volume)))
    }

    async fn register_external_volume(
        &self,
        request: Request<RegisterExternalVolumeRequest>,
    ) -> Result<Response<ProtoVolume>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_VOLUME_CREATE,
                &resource_for_tenant("volume", "*", &org_id, &project_id),
            )
            .await?;

        let mut req = request.into_inner();
        if req.name.trim().is_empty() {
            return Err(Status::invalid_argument("name is required"));
        }
        if req.size_gib == 0 {
            return Err(Status::invalid_argument(
                "size_gib must be greater than zero",
            ));
        }
        let driver = Self::map_volume_driver(
            ProtoVolumeDriverKind::try_from(req.driver).unwrap_or(ProtoVolumeDriverKind::CephRbd),
        );
        if driver != VolumeDriverKind::CephRbd {
            return Err(Status::invalid_argument(
                "RegisterExternalVolume currently supports only Ceph RBD",
            ));
        }
        let ceph = req
            .ceph_rbd
            .take()
            .ok_or_else(|| Status::invalid_argument("ceph_rbd backing is required"))?;
        if ceph.cluster_id.trim().is_empty()
            || ceph.pool.trim().is_empty()
            || ceph.image.trim().is_empty()
        {
            return Err(Status::invalid_argument(
                "ceph_rbd.cluster_id, pool, and image are required",
            ));
        }

        let volume = self
            .volume_manager
            .register_external_ceph_volume(
                &org_id,
                &project_id,
                &req.name,
                req.size_gib,
                if req.storage_class.trim().is_empty() {
                    None
                } else {
                    Some(req.storage_class.as_str())
                },
                &ceph.cluster_id,
                &ceph.pool,
                &ceph.image,
                std::mem::take(&mut req.metadata),
                std::mem::take(&mut req.labels),
            )
            .await?;
        Ok(Response::new(Self::types_volume_to_proto(&volume)))
    }
}

#[tonic::async_trait]
impl ImageService for VmServiceImpl {
    async fn create_image(
        &self,
        request: Request<CreateImageRequest>,
    ) -> Result<Response<ProtoImage>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_CREATE,
                &resource_for_tenant("image", "*", &org_id, &project_id),
            )
            .await?;

        let mut req = request.into_inner();
        if req.name.trim().is_empty() {
            return Err(Status::invalid_argument("name is required"));
        }
        if req.source_url.trim().is_empty() {
            return Err(Status::invalid_argument("source_url is required"));
        }
        let Some(store) = self.artifact_store.as_ref() else {
            return Err(Status::failed_precondition(
                "LightningStor artifact backing is required for image imports",
            ));
        };

        let source_format = Self::map_image_format(
            ProtoImageFormat::try_from(req.format).unwrap_or(ProtoImageFormat::Qcow2),
        );

        let mut image = Image::new(req.name, &org_id, &project_id);
        image.visibility = Self::map_visibility(
            ProtoVisibility::try_from(req.visibility).unwrap_or(ProtoVisibility::Private),
        );
        image.os_type = Self::map_os_type(
            ProtoOsType::try_from(req.os_type).unwrap_or(ProtoOsType::Unspecified),
        );
        image.os_version = req.os_version;
        image.architecture = Self::map_architecture(req.architecture);
        image.min_disk_gib = req.min_disk_gib;
        image.min_memory_mib = req.min_memory_mib;
        image.metadata = std::mem::take(&mut req.metadata);
        image.status = ImageStatus::Uploading;

        let key = TenantKey::new(&org_id, &project_id, &image.id);
        self.images.insert(key.clone(), image.clone());
        self.persist_image(&image).await;

        match store
            .import_image(
                &org_id,
                &project_id,
                &image.id,
                &req.source_url,
                source_format,
            )
            .await
        {
            Ok(imported) => {
                Self::apply_imported_image_metadata(&mut image, &imported, source_format);
                self.images.insert(key, image.clone());
                self.persist_image(&image).await;
                Ok(Response::new(Self::types_image_to_proto(&image)))
            }
            Err(error) => {
                image.status = ImageStatus::Error;
                image.updated_at = Self::now_epoch();
                image
                    .metadata
                    .insert("last_error".to_string(), error.message().to_string());
                self.images.insert(key, image.clone());
                self.persist_image(&image).await;
                Err(error)
            }
        }
    }

    async fn begin_image_upload(
        &self,
        request: Request<BeginImageUploadRequest>,
    ) -> Result<Response<BeginImageUploadResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_CREATE,
                &resource_for_tenant("image", "*", &org_id, &project_id),
            )
            .await?;

        let mut req = request.into_inner();
        if req.name.trim().is_empty() {
            return Err(Status::invalid_argument("name is required"));
        }
        let Some(store) = self.artifact_store.as_ref() else {
            return Err(Status::failed_precondition(
                "LightningStor artifact backing is required for image uploads",
            ));
        };

        let source_format = Self::map_image_format(
            ProtoImageFormat::try_from(req.format).unwrap_or(ProtoImageFormat::Qcow2),
        );

        let mut image = Image::new(req.name, &org_id, &project_id);
        image.visibility = Self::map_visibility(
            ProtoVisibility::try_from(req.visibility).unwrap_or(ProtoVisibility::Private),
        );
        image.os_type = Self::map_os_type(
            ProtoOsType::try_from(req.os_type).unwrap_or(ProtoOsType::Unspecified),
        );
        image.os_version = req.os_version;
        image.architecture = Self::map_architecture(req.architecture);
        image.min_disk_gib = req.min_disk_gib;
        image.min_memory_mib = req.min_memory_mib;
        image.metadata = std::mem::take(&mut req.metadata);
        image.status = ImageStatus::Uploading;

        let key = TenantKey::new(&org_id, &project_id, &image.id);
        self.images.insert(key.clone(), image.clone());
        self.persist_image(&image).await;

        let begin_result = store
            .begin_image_upload(&org_id, &project_id, &image.id)
            .await;
        match begin_result {
            Ok((upload_id, staging_key)) => {
                let now = Self::now_epoch();
                let session = ImageUploadSession {
                    session_id: Uuid::new_v4().to_string(),
                    org_id: org_id.clone(),
                    project_id: project_id.clone(),
                    image_id: image.id.clone(),
                    upload_id,
                    staging_key,
                    source_format,
                    parts: Vec::new(),
                    committed_size_bytes: 0,
                    created_at: now,
                    updated_at: now,
                };
                if let Err(error) = self.store.save_image_upload_session(&session).await {
                    let _ = store
                        .abort_image_upload(
                            &org_id,
                            &project_id,
                            &session.staging_key,
                            &session.upload_id,
                        )
                        .await;
                    image.status = ImageStatus::Error;
                    image.updated_at = Self::now_epoch();
                    image.metadata.insert(
                        "last_error".to_string(),
                        format!("failed to persist image upload session: {error}"),
                    );
                    self.images.insert(key, image.clone());
                    self.persist_image(&image).await;
                    return Err(Status::internal(format!(
                        "failed to persist image upload session: {error}"
                    )));
                }
                Ok(Response::new(BeginImageUploadResponse {
                    image: Some(Self::types_image_to_proto(&image)),
                    upload_session_id: session.session_id,
                    minimum_part_size_bytes: store.minimum_upload_part_size(),
                }))
            }
            Err(error) => {
                image.status = ImageStatus::Error;
                image.updated_at = Self::now_epoch();
                image
                    .metadata
                    .insert("last_error".to_string(), error.message().to_string());
                self.images.insert(key, image.clone());
                self.persist_image(&image).await;
                Err(error)
            }
        }
    }

    async fn upload_image_part(
        &self,
        request: Request<UploadImagePartRequest>,
    ) -> Result<Response<UploadImagePartResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        let req = request.into_inner();
        Self::require_uuid(&req.image_id, "image_id")?;
        Self::require_uuid(&req.upload_session_id, "upload_session_id")?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_CREATE,
                &resource_for_tenant("image", req.image_id.clone(), &org_id, &project_id),
            )
            .await?;

        let Some(store) = self.artifact_store.as_ref() else {
            return Err(Status::failed_precondition(
                "LightningStor artifact backing is required for image uploads",
            ));
        };
        let Some(mut session) = self
            .store
            .load_image_upload_session(&org_id, &project_id, &req.image_id, &req.upload_session_id)
            .await
            .map_err(|error| {
                Status::internal(format!("failed to load image upload session: {error}"))
            })?
        else {
            return Err(Status::not_found("image upload session not found"));
        };
        let Some(image) = self
            .ensure_image_loaded(&org_id, &project_id, &req.image_id)
            .await
        else {
            return Err(Status::not_found("image not found"));
        };
        if image.status != ImageStatus::Uploading {
            return Err(Status::failed_precondition(
                "image is not accepting uploaded parts",
            ));
        }

        let part = store
            .upload_image_part(
                &org_id,
                &project_id,
                &session.staging_key,
                &session.upload_id,
                req.part_number,
                req.body,
            )
            .await?;
        session
            .parts
            .retain(|existing| existing.part_number != part.part_number);
        session.parts.push(part);
        session.parts.sort_by_key(|part| part.part_number);
        session.committed_size_bytes = session.parts.iter().map(|part| part.size_bytes).sum();
        session.updated_at = Self::now_epoch();
        self.store
            .save_image_upload_session(&session)
            .await
            .map_err(|error| {
                Status::internal(format!("failed to persist image upload session: {error}"))
            })?;

        Ok(Response::new(UploadImagePartResponse {
            part_number: req.part_number,
            committed_size_bytes: session.committed_size_bytes,
        }))
    }

    async fn complete_image_upload(
        &self,
        request: Request<CompleteImageUploadRequest>,
    ) -> Result<Response<ProtoImage>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        let req = request.into_inner();
        Self::require_uuid(&req.image_id, "image_id")?;
        Self::require_uuid(&req.upload_session_id, "upload_session_id")?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_CREATE,
                &resource_for_tenant("image", req.image_id.clone(), &org_id, &project_id),
            )
            .await?;

        let Some(store) = self.artifact_store.as_ref() else {
            return Err(Status::failed_precondition(
                "LightningStor artifact backing is required for image uploads",
            ));
        };
        let key = TenantKey::new(&org_id, &project_id, &req.image_id);
        let Some(mut image) = self
            .ensure_image_loaded(&org_id, &project_id, &req.image_id)
            .await
        else {
            return Err(Status::not_found("image not found"));
        };
        let Some(session) = self
            .store
            .load_image_upload_session(&org_id, &project_id, &req.image_id, &req.upload_session_id)
            .await
            .map_err(|error| {
                Status::internal(format!("failed to load image upload session: {error}"))
            })?
        else {
            return Err(Status::not_found("image upload session not found"));
        };

        match store
            .complete_image_upload(
                &org_id,
                &project_id,
                &req.image_id,
                &session.staging_key,
                &session.upload_id,
                &session.parts,
                session.source_format,
            )
            .await
        {
            Ok(imported) => {
                Self::apply_imported_image_metadata(&mut image, &imported, session.source_format);
                self.images.insert(key, image.clone());
                self.persist_image(&image).await;
                let _ = self
                    .store
                    .delete_image_upload_session(
                        &org_id,
                        &project_id,
                        &req.image_id,
                        &req.upload_session_id,
                    )
                    .await;
                Ok(Response::new(Self::types_image_to_proto(&image)))
            }
            Err(error) => {
                image.status = ImageStatus::Error;
                image.updated_at = Self::now_epoch();
                image
                    .metadata
                    .insert("last_error".to_string(), error.message().to_string());
                self.images.insert(key, image.clone());
                self.persist_image(&image).await;
                Err(error)
            }
        }
    }

    async fn abort_image_upload(
        &self,
        request: Request<AbortImageUploadRequest>,
    ) -> Result<Response<Empty>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        let req = request.into_inner();
        Self::require_uuid(&req.image_id, "image_id")?;
        Self::require_uuid(&req.upload_session_id, "upload_session_id")?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_CREATE,
                &resource_for_tenant("image", req.image_id.clone(), &org_id, &project_id),
            )
            .await?;

        let Some(store) = self.artifact_store.as_ref() else {
            return Err(Status::failed_precondition(
                "LightningStor artifact backing is required for image uploads",
            ));
        };
        let Some(session) = self
            .store
            .load_image_upload_session(&org_id, &project_id, &req.image_id, &req.upload_session_id)
            .await
            .map_err(|error| {
                Status::internal(format!("failed to load image upload session: {error}"))
            })?
        else {
            return Err(Status::not_found("image upload session not found"));
        };
        store
            .abort_image_upload(
                &org_id,
                &project_id,
                &session.staging_key,
                &session.upload_id,
            )
            .await?;
        self.store
            .delete_image_upload_session(
                &org_id,
                &project_id,
                &req.image_id,
                &req.upload_session_id,
            )
            .await
            .map_err(|error| {
                Status::internal(format!("failed to delete image upload session: {error}"))
            })?;
        if let Some(mut image) = self
            .ensure_image_loaded(&org_id, &project_id, &req.image_id)
            .await
        {
            let key = TenantKey::new(&org_id, &project_id, &req.image_id);
            image.status = ImageStatus::Error;
            image.updated_at = Self::now_epoch();
            image
                .metadata
                .insert("last_error".to_string(), "upload aborted".to_string());
            self.images.insert(key, image.clone());
            self.persist_image(&image).await;
        }
        Ok(Response::new(Empty {}))
    }

    async fn get_image(
        &self,
        request: Request<GetImageRequest>,
    ) -> Result<Response<ProtoImage>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        let req = request.into_inner();
        Self::require_uuid(&req.image_id, "image_id")?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_READ,
                &resource_for_tenant("image", req.image_id.clone(), &org_id, &project_id),
            )
            .await?;

        let Some(image) = self
            .ensure_image_loaded(&org_id, &project_id, &req.image_id)
            .await
        else {
            return Err(Status::not_found("image not found"));
        };

        Ok(Response::new(Self::types_image_to_proto(&image)))
    }

    async fn list_images(
        &self,
        request: Request<ListImagesRequest>,
    ) -> Result<Response<ListImagesResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_LIST,
                &resource_for_tenant("image", "*", &org_id, &project_id),
            )
            .await?;

        self.ensure_images_loaded(&org_id, &project_id).await;
        let mut images: Vec<ProtoImage> = self
            .images
            .iter()
            .filter(|entry| entry.key().org_id == org_id && entry.key().project_id == project_id)
            .map(|entry| Self::types_image_to_proto(entry.value()))
            .collect();
        images.sort_by(|lhs, rhs| lhs.created_at.cmp(&rhs.created_at));

        Ok(Response::new(ListImagesResponse {
            images,
            next_page_token: String::new(),
        }))
    }

    async fn update_image(
        &self,
        request: Request<UpdateImageRequest>,
    ) -> Result<Response<ProtoImage>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        let req = request.into_inner();
        Self::require_uuid(&req.image_id, "image_id")?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_UPDATE,
                &resource_for_tenant("image", req.image_id.clone(), &org_id, &project_id),
            )
            .await?;

        let key = TenantKey::new(&org_id, &project_id, &req.image_id);
        let Some(mut image) = self
            .ensure_image_loaded(&org_id, &project_id, &req.image_id)
            .await
        else {
            return Err(Status::not_found("image not found"));
        };

        if !req.name.trim().is_empty() {
            image.name = req.name;
        }
        if !req.metadata.is_empty() {
            image.metadata.extend(req.metadata);
        }
        if let Ok(visibility) = ProtoVisibility::try_from(req.visibility) {
            if visibility != ProtoVisibility::Unspecified {
                image.visibility = Self::map_visibility(visibility);
            }
        }
        image.updated_at = Self::now_epoch();

        self.images.insert(key, image.clone());
        self.persist_image(&image).await;
        Ok(Response::new(Self::types_image_to_proto(&image)))
    }

    async fn delete_image(
        &self,
        request: Request<DeleteImageRequest>,
    ) -> Result<Response<Empty>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?;
        let req = request.into_inner();
        Self::require_uuid(&req.image_id, "image_id")?;
        self.auth
            .authorize(
                &tenant,
                ACTION_IMAGE_DELETE,
                &resource_for_tenant("image", req.image_id.clone(), &org_id, &project_id),
            )
            .await?;

        let key = TenantKey::new(&org_id, &project_id, &req.image_id);
        if self
            .ensure_image_loaded(&org_id, &project_id, &req.image_id)
            .await
            .is_none()
        {
            return Err(Status::not_found("image not found"));
        }

        if let Some(store) = self.artifact_store.as_ref() {
            if let Ok(sessions) = self
                .store
                .list_image_upload_sessions(&org_id, &project_id, &req.image_id)
                .await
            {
                for session in sessions {
                    let _ = store
                        .abort_image_upload(
                            &org_id,
                            &project_id,
                            &session.staging_key,
                            &session.upload_id,
                        )
                        .await;
                    let _ = self
                        .store
                        .delete_image_upload_session(
                            &org_id,
                            &project_id,
                            &req.image_id,
                            &session.session_id,
                        )
                        .await;
                }
            }
            store
                .delete_image(&org_id, &project_id, &req.image_id)
                .await?;
        }
        self.images.remove(&key);
        self.store
            .delete_image(&org_id, &project_id, &req.image_id)
            .await
            .map_err(|error| {
                Status::internal(format!("failed to delete image metadata: {error}"))
            })?;

        Ok(Response::new(Empty {}))
    }
}

#[tonic::async_trait]
impl NodeService for VmServiceImpl {
    async fn list_nodes(
        &self,
        request: Request<ListNodesRequest>,
    ) -> Result<Response<ListNodesResponse>, Status> {
        let req = request.into_inner();
        let nodes = self
            .store
            .list_nodes()
            .await
            .map_err(|e| Status::internal(format!("Failed to list nodes: {e}")))?;

        let mut proto_nodes: Vec<ProtoNode> = nodes
            .iter()
            .map(|node| Self::types_node_to_proto(node))
            .collect();

        for node in nodes {
            self.nodes.insert(node.id.to_string(), node);
        }

        if req.page_size > 0 && (req.page_size as usize) < proto_nodes.len() {
            proto_nodes.truncate(req.page_size as usize);
        }

        Ok(Response::new(ListNodesResponse {
            nodes: proto_nodes,
            next_page_token: String::new(),
        }))
    }

    async fn get_node(
        &self,
        request: Request<GetNodeRequest>,
    ) -> Result<Response<ProtoNode>, Status> {
        let req = request.into_inner();
        if req.node_id.is_empty() {
            return Err(Status::invalid_argument("node_id is required"));
        }
        let Some(node) = self.ensure_node_loaded(&req.node_id).await else {
            return Err(Status::not_found("Node not found"));
        };
        Ok(Response::new(Self::types_node_to_proto(&node)))
    }

    async fn cordon_node(
        &self,
        request: Request<CordonNodeRequest>,
    ) -> Result<Response<ProtoNode>, Status> {
        let req = request.into_inner();
        let Some(mut node) = self.ensure_node_loaded(&req.node_id).await else {
            return Err(Status::not_found("Node not found"));
        };
        node.state = NodeState::Cordoned;
        node.last_heartbeat = Self::now_epoch();
        self.nodes.insert(req.node_id.clone(), node.clone());
        self.persist_node(&node).await;
        Ok(Response::new(Self::types_node_to_proto(&node)))
    }

    async fn uncordon_node(
        &self,
        request: Request<UncordonNodeRequest>,
    ) -> Result<Response<ProtoNode>, Status> {
        let req = request.into_inner();
        let Some(mut node) = self.ensure_node_loaded(&req.node_id).await else {
            return Err(Status::not_found("Node not found"));
        };
        node.state = NodeState::Ready;
        node.last_heartbeat = Self::now_epoch();
        self.nodes.insert(req.node_id.clone(), node.clone());
        self.persist_node(&node).await;
        Ok(Response::new(Self::types_node_to_proto(&node)))
    }

    async fn drain_node(
        &self,
        request: Request<DrainNodeRequest>,
    ) -> Result<Response<ProtoNode>, Status> {
        let req = request.into_inner();
        let Some(mut node) = self.ensure_node_loaded(&req.node_id).await else {
            return Err(Status::not_found("Node not found"));
        };
        node.state = NodeState::Draining;
        node.last_heartbeat = Self::now_epoch();
        self.nodes.insert(req.node_id.clone(), node.clone());
        self.persist_node(&node).await;
        Ok(Response::new(Self::types_node_to_proto(&node)))
    }

    async fn heartbeat_node(
        &self,
        request: Request<HeartbeatNodeRequest>,
    ) -> Result<Response<ProtoNode>, Status> {
        let mut req = request.into_inner();
        if req.node_id.is_empty() {
            return Err(Status::invalid_argument("node_id is required"));
        }

        let node_id = req.node_id.clone();
        let mut node = self
            .ensure_node_loaded(&node_id)
            .await
            .unwrap_or_else(|| Node::new(node_id.clone()));

        if !req.name.is_empty() {
            node.name = std::mem::take(&mut req.name);
        }

        if let Ok(state) = ProtoNodeState::try_from(req.state) {
            if state != ProtoNodeState::Unspecified {
                node.state = Self::map_node_state(state);
            }
        }

        if let Some(cap) = req.capacity.take() {
            node.capacity = Self::proto_capacity_to_types(Some(cap));
        }
        if let Some(alloc) = req.allocatable.take() {
            node.allocatable = Self::proto_capacity_to_types(Some(alloc));
        }

        if !req.hypervisors.is_empty() {
            node.hypervisors = req
                .hypervisors
                .iter()
                .filter_map(|h| ProtoHypervisorType::try_from(*h).ok())
                .map(|hypervisor| self.map_hv(hypervisor))
                .collect();
        }
        if !req.supported_volume_drivers.is_empty() {
            node.supported_volume_drivers = req
                .supported_volume_drivers
                .iter()
                .filter_map(|driver| ProtoVolumeDriverKind::try_from(*driver).ok())
                .map(Self::map_volume_driver)
                .collect();
        }
        if !req.supported_storage_classes.is_empty() {
            node.supported_storage_classes = std::mem::take(&mut req.supported_storage_classes);
        }
        node.shared_live_migration = req.shared_live_migration;

        if !req.labels.is_empty() {
            node.labels = std::mem::take(&mut req.labels);
        }

        if !req.agent_version.is_empty() {
            node.agent_version = std::mem::take(&mut req.agent_version);
        }

        node.last_heartbeat = Self::now_epoch();
        self.nodes.insert(node_id, node.clone());
        self.persist_node(&node).await;
        Ok(Response::new(Self::types_node_to_proto(&node)))
    }
}
