use crate::app_config::AppConfig;
use crate::constants;
use crate::libvirt_lib_structures::domain::*;
use crate::libvirt_lib_structures::XMLUuid;
use crate::libvirt_rest_structures::LibVirtStructError;
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
use crate::utils::disks_utils::Disk;
use crate::utils::files_utils;
use crate::utils::files_utils::convert_size_unit_to_mb;
use lazy_regex::regex;
use num::Integer;

#[derive(
    Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd,
)]
pub struct VMGroupId(pub String);

#[derive(serde::Serialize, serde::Deserialize)]
pub enum BootType {
    UEFI,
    UEFISecureBoot,
}

#[derive(serde::Serialize, serde::Deserialize)]
pub enum VMArchitecture {
    #[serde(rename = "i686")]
    I686,
    #[serde(rename = "x86_64")]
    X86_64,
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct NWFilterParam {
    name: String,
    value: String,
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct NWFilterRef {
    name: String,
    parameters: Vec<NWFilterParam>,
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct Network {
    #[serde(flatten)]
    r#type: NetworkType,
    mac: String,
    nwfilterref: Option<NWFilterRef>,
}

#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
pub enum NetworkType {
    UserspaceSLIRPStack,
    DefinedNetwork { network: String }, // TODO : complete network types
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct VMInfo {
    /// VM name (alphanumeric characters only)
    pub name: String,
    pub uuid: Option<XMLUuid>,
    pub genid: Option<XMLUuid>,
    pub title: Option<String>,
    pub description: Option<String>,
    /// Group associated with the VM (VirtWeb specific field)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub group: Option<VMGroupId>,
    pub boot_type: BootType,
    pub architecture: VMArchitecture,
    /// VM allocated memory, in megabytes
    pub memory: usize,
    /// Number of vCPU for the VM
    pub number_vcpu: usize,
    /// Enable VNC access through admin console
    pub vnc_access: bool,
    /// Attach ISO file(s)
    pub iso_files: Vec<String>,
    /// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest
    pub disks: Vec<Disk>,
    /// Network cards
    pub networks: Vec<Network>,
    /// Add a TPM v2.0 module
    pub tpm_module: bool,
}

impl VMInfo {
    /// Turn this VM into a domain
    pub fn as_domain(&self) -> anyhow::Result<DomainXML> {
        if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
            return Err(StructureExtraction("VM name is invalid!").into());
        }

        let uuid = if let Some(n) = self.uuid {
            if !n.is_valid() {
                return Err(StructureExtraction("VM UUID is invalid!").into());
            }
            n
        } else {
            XMLUuid::new_random()
        };

        if let Some(n) = &self.genid {
            if !n.is_valid() {
                return Err(StructureExtraction("VM genid is invalid!").into());
            }
        }

        if let Some(n) = &self.title {
            if n.contains('\n') {
                return Err(StructureExtraction("VM title contain newline char!").into());
            }
        }

        if let Some(group) = &self.group {
            if !regex!("^[a-zA-Z0-9]+$").is_match(&group.0) {
                return Err(StructureExtraction("VM group name is invalid!").into());
            }
        }

        if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY {
            return Err(StructureExtraction("VM memory is invalid!").into());
        }

        if self.number_vcpu == 0 || (self.number_vcpu != 1 && self.number_vcpu.is_odd()) {
            return Err(StructureExtraction("Invalid number of vCPU specified!").into());
        }

        let mut disks = vec![];

        for iso_file in &self.iso_files {
            if !files_utils::check_file_name(iso_file) {
                return Err(StructureExtraction("ISO filename is invalid!").into());
            }

            let path = AppConfig::get().iso_storage_path().join(iso_file);

            if !path.exists() {
                return Err(StructureExtraction("Specified ISO file does not exists!").into());
            }

            disks.push(DiskXML {
                r#type: "file".to_string(),
                device: "cdrom".to_string(),
                driver: DiskDriverXML {
                    name: "qemu".to_string(),
                    r#type: "raw".to_string(),
                    cache: "none".to_string(),
                },
                source: DiskSourceXML {
                    file: path.to_string_lossy().to_string(),
                },
                target: DiskTargetXML {
                    dev: format!(
                        "hd{}",
                        ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()]
                    ),
                    bus: "sata".to_string(),
                },
                readonly: Some(DiskReadOnlyXML {}),
                boot: DiskBootXML {
                    order: (disks.len() + 1).to_string(),
                },
                address: None,
            })
        }

        let (vnc_graphics, vnc_video) = match self.vnc_access {
            true => (
                Some(GraphicsXML {
                    r#type: "vnc".to_string(),
                    socket: AppConfig::get()
                        .vnc_socket_for_domain(&self.name)
                        .to_string_lossy()
                        .to_string(),
                }),
                Some(VideoXML {
                    model: VideoModelXML {
                        r#type: "virtio".to_string(), //"qxl".to_string(),
                    },
                }),
            ),
            false => (None, None),
        };

        // Process network card
        let mut networks = vec![];
        for n in &self.networks {
            let mac = NetMacAddress {
                address: n.mac.to_string(),
            };

            let model = Some(NetIntModelXML {
                r#type: "virtio".to_string(),
            });

            let filterref = if let Some(n) = &n.nwfilterref {
                if !regex!("^[a-zA-Z0-9\\_\\-]+$").is_match(&n.name) {
                    log::error!("Filter ref name {} is invalid", n.name);
                    return Err(StructureExtraction("Network filter ref name is invalid!").into());
                }

                for p in &n.parameters {
                    if !regex!("^[a-zA-Z0-9_-]+$").is_match(&p.name) {
                        return Err(StructureExtraction(
                            "Network filter ref parameter name is invalid!",
                        )
                        .into());
                    }
                }

                Some(NetIntfilterRefXML {
                    filter: n.name.to_string(),
                    parameters: n
                        .parameters
                        .iter()
                        .map(|f| NetIntFilterParameterXML {
                            name: f.name.to_string(),
                            value: f.value.to_string(),
                        })
                        .collect(),
                })
            } else {
                None
            };

            networks.push(match &n.r#type {
                NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML {
                    mac,
                    r#type: "user".to_string(),
                    source: None,
                    model,
                    filterref,
                },
                NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML {
                    mac,
                    r#type: "network".to_string(),
                    source: Some(NetIntSourceXML {
                        network: network.to_string(),
                    }),
                    model,
                    filterref,
                },
            })
        }

        // Check disks name for duplicates
        for disk in &self.disks {
            if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 {
                return Err(StructureExtraction("Two different disks have the same name!").into());
            }
        }

        // Apply disks configuration. Starting from now, the function should ideally never fail due to
        // bad user input
        for disk in &self.disks {
            disk.check_config()?;
            disk.apply_config(uuid)?;

            if disk.delete {
                continue;
            }

            disks.push(DiskXML {
                r#type: "file".to_string(),
                device: "disk".to_string(),
                driver: DiskDriverXML {
                    name: "qemu".to_string(),
                    r#type: "raw".to_string(),
                    cache: "none".to_string(),
                },
                source: DiskSourceXML {
                    file: disk.disk_path(uuid).to_string_lossy().to_string(),
                },
                target: DiskTargetXML {
                    dev: format!(
                        "vd{}",
                        ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()]
                    ),
                    bus: "virtio".to_string(),
                },
                readonly: None,
                boot: DiskBootXML {
                    order: (disks.len() + 1).to_string(),
                },
                address: None,
            })
        }

        Ok(DomainXML {
            r#type: "kvm".to_string(),
            name: self.name.to_string(),
            uuid: Some(uuid),
            genid: self.genid.map(|i| i.0),
            title: self.title.clone(),
            description: self.description.clone(),

            metadata: Some(DomainMetadataXML {
                virtweb: DomainMetadataVirtWebXML {
                    ns: "https://virtweb.communiquons.org".to_string(),
                    group: self.group.clone().map(|g| g.0),
                },
            }),
            os: OSXML {
                r#type: OSTypeXML {
                    arch: match self.architecture {
                        VMArchitecture::I686 => "i686",
                        VMArchitecture::X86_64 => "x86_64",
                    }
                    .to_string(),
                    machine: "q35".to_string(),
                    body: "hvm".to_string(),
                },
                firmware: "efi".to_string(),
                loader: Some(OSLoaderXML {
                    secure: match self.boot_type {
                        BootType::UEFI => "no".to_string(),
                        BootType::UEFISecureBoot => "yes".to_string(),
                    },
                }),
            },

            features: FeaturesXML { acpi: ACPIXML {} },

            devices: DevicesXML {
                graphics: vnc_graphics,
                video: vnc_video,
                disks,
                net_interfaces: networks,
                inputs: vec![
                    DomainInputXML {
                        r#type: "mouse".to_string(),
                    },
                    DomainInputXML {
                        r#type: "keyboard".to_string(),
                    },
                    DomainInputXML {
                        r#type: "tablet".to_string(),
                    },
                ],
                tpm: match self.tpm_module {
                    true => Some(TPMDeviceXML {
                        model: "tpm-tis".to_string(),
                        backend: TPMBackendXML {
                            r#type: "emulator".to_string(),
                            version: "2.0".to_string(),
                        },
                    }),
                    false => None,
                },
            },

            memory: DomainMemoryXML {
                unit: "MB".to_string(),
                memory: self.memory,
            },

            vcpu: DomainVCPUXML {
                body: self.number_vcpu,
            },

            cpu: DomainCPUXML {
                mode: "host-passthrough".to_string(),
                topology: Some(DomainCPUTopology {
                    sockets: 1,
                    cores: match self.number_vcpu {
                        1 => 1,
                        v => v / 2,
                    },
                    threads: match self.number_vcpu {
                        1 => 1,
                        _ => 2,
                    },
                }),
            },

            on_poweroff: "destroy".to_string(),
            on_reboot: "restart".to_string(),
            on_crash: "destroy".to_string(),
        })
    }

    /// Turn a domain into a vm
    pub fn from_domain(domain: DomainXML) -> anyhow::Result<Self> {
        Ok(Self {
            name: domain.name,
            uuid: domain.uuid,
            genid: domain.genid.map(XMLUuid),
            title: domain.title,
            description: domain.description,
            group: domain
                .metadata
                .clone()
                .unwrap_or_default()
                .virtweb
                .group
                .map(VMGroupId),
            boot_type: match domain.os.loader {
                None => BootType::UEFI,
                Some(l) => match l.secure.as_str() {
                    "yes" => BootType::UEFISecureBoot,
                    _ => BootType::UEFI,
                },
            },
            architecture: match domain.os.r#type.arch.as_str() {
                "i686" => VMArchitecture::I686,
                "x86_64" => VMArchitecture::X86_64,
                a => {
                    return Err(LibVirtStructError::DomainExtraction(format!(
                        "Unknown architecture: {a}! "
                    ))
                    .into());
                }
            },
            number_vcpu: domain.vcpu.body,
            memory: convert_size_unit_to_mb(&domain.memory.unit, domain.memory.memory)?,
            vnc_access: domain.devices.graphics.is_some(),
            iso_files: domain
                .devices
                .disks
                .iter()
                .filter(|d| d.device == "cdrom")
                .map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string())
                .collect(),

            disks: domain
                .devices
                .disks
                .iter()
                .filter(|d| d.device == "disk")
                .map(|d| Disk::load_from_file(&d.source.file).unwrap())
                .collect(),

            networks: domain
                .devices
                .net_interfaces
                .iter()
                .map(|d| {
                    Ok(Network {
                        mac: d.mac.address.to_string(),
                        r#type: match d.r#type.as_str() {
                            "user" => NetworkType::UserspaceSLIRPStack,
                            "network" => NetworkType::DefinedNetwork {
                                network: d.source.as_ref().unwrap().network.to_string(),
                            },
                            a => {
                                return Err(LibVirtStructError::DomainExtraction(format!(
                                    "Unknown network interface type: {a}! "
                                )));
                            }
                        },
                        nwfilterref: d.filterref.as_ref().map(|f| NWFilterRef {
                            name: f.filter.to_string(),
                            parameters: f
                                .parameters
                                .iter()
                                .map(|p| NWFilterParam {
                                    name: p.name.to_string(),
                                    value: p.value.to_string(),
                                })
                                .collect(),
                        }),
                    })
                })
                .collect::<Result<Vec<_>, _>>()?,

            tpm_module: domain.devices.tpm.is_some(),
        })
    }
}