//! An abstraction on top of the libvirt bindings.

use crate::util::wait_for_ssh;
use log::debug;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
use virt::connect::Connect;
use virt::domain::Domain;
use virt::sys::{VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_CURRENT, VIR_DOMAIN_AFFECT_LIVE};

/// Errors from this module.
#[derive(Debug, thiserror::Error)]
pub enum VirtError {
    /// Error connecting to libvirtd.
    #[error("couldn't connect to the libvirt daemon")]
    Connect(#[source] virt::error::Error),

    /// Error listing domains.
    #[error("couldn't list all domains")]
    Domains(#[source] virt::error::Error),

    /// Error listing domains.
    #[error("couldn't get name of domain")]
    GetName(#[source] virt::error::Error),

    /// Error checking if domain is active.
    #[error("couldn't check is domain {0} is active")]
    IsActive(String, #[source] virt::error::Error),

    /// Error getting domain's XML description.
    #[error("couldn't get domain's XML description: {0}")]
    GetXml(String, #[source] virt::error::Error),

    /// Error detaching cloud-init ISO from domain
    #[error("couldn't detach cloud-init ISO file from domain {0}")]
    DetachIso(String, #[source] virt::error::Error),

    /// Error detaching drive from domain
    #[error("couldn't create domain {0}")]
    Create(String, #[source] virt::error::Error),

    /// Error shutting down domain
    #[error("couldn't shut down domain {0}")]
    Shutdown(String, #[source] virt::error::Error),

    /// Error undefining domain
    #[error("couldn't undefine domain {0}")]
    Undefine(String, #[source] virt::error::Error),

    /// Error undefining domain
    #[error("couldn't set domain {0} to be autostarted")]
    Autostart(String, #[source] virt::error::Error),

    /// Failed to delete image file.
    #[error("failed to delete image file {0}")]
    DeleteImage(PathBuf, #[source] std::io::Error),

    /// Error doing I/O.
    #[error(transparent)]
    IoError(#[from] std::io::Error),
}

/// Access libvirt for all the things this program needs.
pub struct Libvirt {
    conn: Connect,
}

impl Libvirt {
    pub fn connect(url: &str) -> Result<Self, VirtError> {
        debug!("connecting to libvirtd {}", url);
        let conn = Connect::open(Some(url)).map_err(VirtError::Connect)?;
        Ok(Self { conn })
    }

    fn get_domains(&self) -> Result<Vec<Domain>, VirtError> {
        debug!("listing all domains");
        self.conn.list_all_domains(0).map_err(VirtError::Domains)
    }

    fn get_domain(&self, name: &str) -> Result<Option<Domain>, VirtError> {
        for domain in self.get_domains()? {
            if get_name(&domain)? == name {
                return Ok(Some(domain));
            }
        }
        Ok(None)
    }

    pub fn names(&self) -> Result<Vec<String>, VirtError> {
        let mut ret = vec![];
        for domain in self.get_domains()? {
            ret.push(get_name(&domain)?);
        }
        Ok(ret)
    }

    pub fn is_active(&self, name: &str) -> Result<bool, VirtError> {
        if let Some(domain) = self.get_domain(name)? {
            Ok(is_active(&domain, name)?)
        } else {
            Ok(false)
        }
    }

    pub fn wait_for_inactive(&self, name: &str) -> Result<(), VirtError> {
        loop {
            if !self.is_active(name)? {
                break;
            }
        }
        Ok(())
    }

    pub fn detach_cloud_init_iso(&self, name: &str) -> Result<(), VirtError> {
        if let Some(domain) = self.get_domain(name)? {
            debug!("detaching cloud-init ISO from {}", name);
            let xml = get_xml(&domain, name)?;
            let disk = find_iso_xml(&xml);
            let flags =
                VIR_DOMAIN_AFFECT_CONFIG | VIR_DOMAIN_AFFECT_CURRENT | VIR_DOMAIN_AFFECT_LIVE;
            if !disk.is_empty() {
                domain
                    .detach_device_flags(&disk, flags)
                    .map_err(|err| VirtError::DetachIso(name.to_string(), err))?;
            }
        }
        Ok(())
    }

    pub fn trigger_start(&self, name: &str) -> Result<(), VirtError> {
        if let Some(domain) = self.get_domain(name)? {
            if !is_active(&domain, name)? {
                debug!("starting {}", name);
                domain
                    .create()
                    .map_err(|err| VirtError::Create(name.to_string(), err))?;
            }
        }
        Ok(())
    }

    pub fn start(&self, name: &str) -> Result<(), VirtError> {
        if self.get_domain(name)?.is_some() {
            self.trigger_start(name)?;
            wait_for_ssh(name);
        }
        Ok(())
    }

    pub fn trigger_shutdown(&self, name: &str) -> Result<(), VirtError> {
        if let Some(domain) = self.get_domain(name)? {
            debug!("asking {} to shut down", name);
            if is_active(&domain, name)? {
                domain
                    .shutdown()
                    .map_err(|err| VirtError::Shutdown(name.to_string(), err))?;
            }
        }
        Ok(())
    }

    pub fn shutdown(&self, name: &str) -> Result<(), VirtError> {
        if let Some(domain) = self.get_domain(name)? {
            debug!("shutting down {}", name);
            self.trigger_shutdown(name)?;
            wait_until_inactive(&domain, name);
        }
        Ok(())
    }

    pub fn delete(&self, name: &str, image: &Path) -> Result<(), VirtError> {
        if let Some(domain) = self.get_domain(name)? {
            self.shutdown(name)?;

            debug!("undefine {}", name);
            domain
                .undefine()
                .map_err(|err| VirtError::Undefine(name.to_string(), err))?;

            debug!("removing image file {}", image.display());
            std::fs::remove_file(image)
                .map_err(|e| VirtError::DeleteImage(image.to_path_buf(), e))?;
        }

        Ok(())
    }

    pub fn set_autostart(&self, name: &str, autostart: bool) -> Result<(), VirtError> {
        if let Some(domain) = self.get_domain(name)? {
            domain
                .set_autostart(autostart)
                .map_err(|err| VirtError::Autostart(name.to_string(), err))?;
        }
        Ok(())
    }
}

fn get_name(domain: &Domain) -> Result<String, VirtError> {
    let name = domain.get_name().map_err(VirtError::GetName)?;
    Ok(name)
}

fn is_active(domain: &Domain, name: &str) -> Result<bool, VirtError> {
    let is = domain
        .is_active()
        .map_err(|err| VirtError::IsActive(name.to_string(), err))?;
    Ok(is)
}

fn get_xml(domain: &Domain, name: &str) -> Result<String, VirtError> {
    let is = domain
        .get_xml_desc(0)
        .map_err(|err| VirtError::GetXml(name.to_string(), err))?;
    Ok(is)
}

fn wait_until_inactive(domain: &Domain, name: &str) {
    debug!("waiting for domain {} to become inactive", name);
    let briefly = Duration::from_millis(1000);
    loop {
        thread::sleep(briefly);
        match domain.is_active() {
            Ok(true) => (),
            Ok(false) => break,
            Err(err) => {
                debug!("is_active: {}", err);
            }
        }
        debug!("domain {} is still running", name);
    }
}

// This is a HACK. The XML description of a domain contains
// descriptions of attached virtual disks. We find one that contains
// ".iso", and return that.
//
//  <disk type='file' device='disk'>
//   <driver name='qemu' type='raw'/>
//   <source file='/tmp/.tmp2rAiVW/cloudinit.iso'/>
//   <backingStore/>
//   <target dev='vdb' bus='virtio'/>
//   <readonly/>
//   <alias name='virtio-disk1'/>
//   <address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x0'/>
// </disk>

fn find_iso_xml(xml: &str) -> String {
    let mut xml = xml;
    loop {
        let start = xml.find("<disk ");
        if start.is_none() {
            break;
        }
        let start = start.unwrap();
        xml = &xml[start..];

        let end = xml.find("</disk>");
        if end.is_none() {
            break;
        }
        let end = end.unwrap();
        let disk = &xml[..end + 7];
        if disk.contains(".iso") {
            return disk.to_string();
        }
        xml = &xml[end..];
    }
    "".to_string()
}
