use crate::app_config::AppConfig;
use crate::constants;
use crate::libvirt_lib_structures::XMLUuid;
use crate::utils::file_disks_utils::{DiskFileFormat, DiskFileInfo};
use crate::utils::file_size_utils::FileSize;
use crate::utils::files_utils;
use lazy_regex::regex;
use std::path::{Path, PathBuf};

#[derive(thiserror::Error, Debug)]
enum VMDisksError {
    #[error("DiskConfigError: {0}")]
    Config(&'static str),
}

#[derive(serde::Serialize, serde::Deserialize)]
pub enum VMDiskBus {
    Virtio,
    SATA,
}

/// Disk allocation type
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "format")]
pub enum VMDiskFormat {
    Raw {
        /// Is raw file a sparse file?
        is_sparse: bool,
    },
    QCow2,
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct VMFileDisk {
    /// Disk name
    pub name: String,
    /// Disk size, in bytes
    pub size: FileSize,
    /// Disk bus
    pub bus: VMDiskBus,
    /// Disk format
    #[serde(flatten)]
    pub format: VMDiskFormat,
    /// When creating a new disk, specify the disk image template to use
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from_image: Option<String>,
    /// Set this variable to true to resize disk image
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resize: Option<bool>,
    /// Set this variable to true to delete the disk
    pub delete: bool,
}

impl VMFileDisk {
    pub fn load_from_file(path: &str, bus: &str) -> anyhow::Result<Self> {
        let file = Path::new(path);

        let info = DiskFileInfo::load_file(file)?;

        Ok(Self {
            name: info.name,

            // Get only the virtual size of the file
            size: match info.format {
                DiskFileFormat::Raw { .. } => info.file_size,
                DiskFileFormat::QCow2 { virtual_size } => virtual_size,
                _ => anyhow::bail!("Unsupported image format: {:?}", info.format),
            },

            format: match info.format {
                DiskFileFormat::Raw { is_sparse } => VMDiskFormat::Raw { is_sparse },
                DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2,
                _ => anyhow::bail!("Unsupported image format: {:?}", info.format),
            },

            bus: match bus {
                "virtio" => VMDiskBus::Virtio,
                "sata" => VMDiskBus::SATA,
                _ => anyhow::bail!("Unsupported disk bus type: {bus}"),
            },

            delete: false,
            from_image: None,
            resize: None,
        })
    }

    pub fn check_config(&self) -> anyhow::Result<()> {
        if constants::DISK_NAME_MIN_LEN > self.name.len()
            || constants::DISK_NAME_MAX_LEN < self.name.len()
        {
            return Err(VMDisksError::Config("Disk name length is invalid").into());
        }

        if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
            return Err(VMDisksError::Config("Disk name contains invalid characters!").into());
        }

        // Check disk size
        if !(constants::DISK_SIZE_MIN..=constants::DISK_SIZE_MAX).contains(&self.size) {
            return Err(VMDisksError::Config("Disk size is invalid!").into());
        }

        // Check specified disk image template
        if let Some(disk_image) = &self.from_image {
            if !files_utils::check_file_name(disk_image) {
                return Err(VMDisksError::Config("Disk image template name is not valid!").into());
            }

            if !AppConfig::get().disk_images_file_path(disk_image).is_file() {
                return Err(
                    VMDisksError::Config("Specified disk image file does not exist!").into(),
                );
            }
        }

        Ok(())
    }

    /// Get disk path on file system
    pub fn disk_path(&self, id: XMLUuid) -> PathBuf {
        let domain_dir = AppConfig::get().vm_storage_path(id);
        let file_name = match self.format {
            VMDiskFormat::Raw { .. } => self.name.to_string(),
            VMDiskFormat::QCow2 => format!("{}.qcow2", self.name),
        };
        domain_dir.join(&file_name)
    }

    /// Apply disk configuration
    pub fn apply_config(&self, id: XMLUuid) -> anyhow::Result<()> {
        self.check_config()?;

        let file = self.disk_path(id);
        files_utils::create_directory_if_missing(file.parent().unwrap())?;

        // Delete file if requested
        if self.delete {
            if !file.exists() {
                log::debug!("File {file:?} does not exists, so it was not deleted");
                return Ok(());
            }

            log::info!("Deleting {file:?}");
            std::fs::remove_file(file)?;
            return Ok(());
        }

        if file.exists() {
            log::debug!("File {file:?} does not exists, so it was not touched");
        }
        // Create disk if required
        else {
            // Determine file format
            let format = match self.format {
                VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
                VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
                    virtual_size: self.size,
                },
            };

            // Create / Restore disk file
            match &self.from_image {
                // Create disk file
                None => {
                    DiskFileInfo::create(&file, format, self.size)?;
                }

                // Restore disk image template
                Some(disk_img) => {
                    let src_file =
                        DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
                    src_file.convert(&file, format)?;
                }
            }
        }

        // Resize disk file if requested
        if self.resize == Some(true) {
            let disk = DiskFileInfo::load_file(&file)?;

            // Can only increase disk size
            if let Err(e) = disk.resize(self.size) {
                log::error!("Failed to resize disk file {}: {e:?}", self.name);
            }
        }

        Ok(())
    }
}