use async_trait::async_trait;
use derive_new::new;
use log::info;
use serde::Serialize;

use crate::{
    domain::{data::Version, interpret::InterpretStatus},
    interpret::{Interpret, InterpretError, InterpretName, Outcome},
    inventory::Inventory,
    topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, Topology},
};

use crate::domain::score::Score;

#[derive(Debug, new, Clone, Serialize)]
pub struct DhcpScore {
    pub host_binding: Vec<HostBinding>,
    pub next_server: Option<IpAddress>,
    pub boot_filename: Option<String>,
    pub filename: Option<String>,
    pub filename64: Option<String>,
    pub filenameipxe: Option<String>,
}

impl<T: Topology + DhcpServer> Score<T> for DhcpScore {
    fn create_interpret(&self) -> Box<dyn Interpret<T>> {
        Box::new(DhcpInterpret::new(self.clone()))
    }

    fn name(&self) -> String {
        "DhcpScore".to_string()
    }
}

// https://docs.opnsense.org/manual/dhcp.html#advanced-settings
#[derive(Debug, Clone)]
pub struct DhcpInterpret {
    score: DhcpScore,
    version: Version,
    status: InterpretStatus,
}

impl DhcpInterpret {
    pub fn new(score: DhcpScore) -> Self {
        let version = Version::from("1.0.0").expect("Version should be valid");

        Self {
            version,
            score,
            status: InterpretStatus::QUEUED,
        }
    }
    async fn add_static_entries<D: DhcpServer>(
        &self,
        _inventory: &Inventory,
        dhcp_server: &D,
    ) -> Result<Outcome, InterpretError> {
        let dhcp_entries: Vec<DHCPStaticEntry> = self
            .score
            .host_binding
            .iter()
            .map(|binding| {
                let ip = match binding.logical_host.ip {
                    std::net::IpAddr::V4(ipv4) => ipv4,
                    std::net::IpAddr::V6(_) => {
                        unimplemented!("DHCPStaticEntry only supports ipv4 at the moment")
                    }
                };

                DHCPStaticEntry {
                    name: binding.logical_host.name.clone(),
                    mac: binding.physical_host.cluster_mac(),
                    ip,
                }
            })
            .collect();
        info!("DHCPStaticEntry : {:?}", dhcp_entries);

        info!("DHCP server : {:?}", dhcp_server);

        let number_new_entries = dhcp_entries.len();

        for entry in dhcp_entries.into_iter() {
            match dhcp_server.add_static_mapping(&entry).await {
                Ok(_) => info!("Successfully registered DHCPStaticEntry {}", entry),
                Err(_) => todo!(),
            }
        }

        Ok(Outcome::new(
            InterpretStatus::SUCCESS,
            format!("Dhcp Interpret registered {} entries", number_new_entries),
        ))
    }

    async fn set_pxe_options<D: DhcpServer>(
        &self,
        _inventory: &Inventory,
        dhcp_server: &D,
    ) -> Result<Outcome, InterpretError> {
        let next_server_outcome = match self.score.next_server {
            Some(next_server) => {
                dhcp_server.set_next_server(next_server).await?;
                Outcome::new(
                    InterpretStatus::SUCCESS,
                    format!("Dhcp Interpret Set next boot to {next_server}"),
                )
            }
            None => Outcome::noop(),
        };

        let boot_filename_outcome = match &self.score.boot_filename {
            Some(boot_filename) => {
                dhcp_server.set_boot_filename(&boot_filename).await?;
                Outcome::new(
                    InterpretStatus::SUCCESS,
                    format!("Dhcp Interpret Set boot filename to {boot_filename}"),
                )
            }
            None => Outcome::noop(),
        };

        let filename_outcome = match &self.score.filename {
            Some(filename) => {
                dhcp_server.set_filename(&filename).await?;
                Outcome::new(
                    InterpretStatus::SUCCESS,
                    format!("Dhcp Interpret Set filename to {filename}"),
                )
            }
            None => Outcome::noop(),
        };

        let filename64_outcome = match &self.score.filename64 {
            Some(filename64) => {
                dhcp_server.set_filename64(&filename64).await?;
                Outcome::new(
                    InterpretStatus::SUCCESS,
                    format!("Dhcp Interpret Set filename64 to {filename64}"),
                )
            }
            None => Outcome::noop(),
        };

        let filenameipxe_outcome = match &self.score.filenameipxe {
            Some(filenameipxe) => {
                dhcp_server.set_filenameipxe(&filenameipxe).await?;
                Outcome::new(
                    InterpretStatus::SUCCESS,
                    format!("Dhcp Interpret Set filenameipxe to {filenameipxe}"),
                )
            }
            None => Outcome::noop(),
        };

        if next_server_outcome.status == InterpretStatus::NOOP
            && boot_filename_outcome.status == InterpretStatus::NOOP
            && filename_outcome.status == InterpretStatus::NOOP
            && filename64_outcome.status == InterpretStatus::NOOP
            && filenameipxe_outcome.status == InterpretStatus::NOOP
        {
            return Ok(Outcome::noop());
        }

        Ok(Outcome::new(
            InterpretStatus::SUCCESS,
            format!(
                "Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]",
                self.score.boot_filename,
                self.score.boot_filename,
                self.score.filename,
                self.score.filename64,
                self.score.filenameipxe
            ),
        ))
    }
}

#[async_trait]
impl<T: DhcpServer> Interpret<T> for DhcpInterpret {
    fn get_name(&self) -> InterpretName {
        InterpretName::OPNSenseDHCP
    }

    fn get_version(&self) -> crate::domain::data::Version {
        self.version.clone()
    }

    fn get_status(&self) -> InterpretStatus {
        self.status.clone()
    }

    fn get_children(&self) -> Vec<crate::domain::data::Id> {
        todo!()
    }

    async fn execute(
        &self,
        inventory: &Inventory,
        topology: &T,
    ) -> Result<Outcome, InterpretError> {
        info!("Executing DhcpInterpret on inventory {inventory:?}");

        self.set_pxe_options(inventory, topology).await?;

        self.add_static_entries(inventory, topology).await?;

        topology.commit_config().await?;

        Ok(Outcome::new(
            InterpretStatus::SUCCESS,
            format!("Dhcp Interpret execution successful"),
        ))
    }
}
