use log::info;
use opnsense_config_xml::MaybeString;
use opnsense_config_xml::StaticMap;
use std::net::Ipv4Addr;
use std::sync::Arc;

use opnsense_config_xml::OPNsense;

use crate::config::OPNsenseShell;
use crate::Error;

pub struct DhcpConfig<'a> {
    opnsense: &'a mut OPNsense,
    opnsense_shell: Arc<dyn OPNsenseShell>,
}

#[derive(Debug)]
pub enum DhcpError {
    InvalidMacAddress(String),
    InvalidIpAddress(String),
    IpAddressAlreadyMapped(String),
    MacAddressAlreadyMapped(String),
    IpAddressOutOfRange(String),
}

impl std::fmt::Display for DhcpError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DhcpError::InvalidMacAddress(mac) => write!(f, "Invalid MAC address format: {}", mac),
            DhcpError::InvalidIpAddress(ip) => write!(f, "Invalid IP address format: {}", ip),
            DhcpError::IpAddressAlreadyMapped(ip) => {
                write!(f, "IP address {} is already mapped", ip)
            }
            DhcpError::MacAddressAlreadyMapped(mac) => {
                write!(f, "MAC address {} is already mapped", mac)
            }
            DhcpError::IpAddressOutOfRange(ip) => {
                write!(f, "IP address {} is out of interface range", ip)
            }
        }
    }
}

impl std::error::Error for DhcpError {}

impl<'a> DhcpConfig<'a> {
    pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
        Self {
            opnsense,
            opnsense_shell,
        }
    }

    pub fn remove_static_mapping(&mut self, mac: &str) {
        let lan_dhcpd = self.get_lan_dhcpd();
        lan_dhcpd
            .staticmaps
            .retain(|static_entry| static_entry.mac != mac);
    }

    fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
        &mut self
            .opnsense
            .dhcpd
            .elements
            .iter_mut()
            .find(|(name, _config)| return name == "lan")
            .expect("Interface lan should have dhcpd activated")
            .1
    }

    pub fn add_static_mapping(
        &mut self,
        mac: &str,
        ipaddr: Ipv4Addr,
        hostname: &str,
    ) -> Result<(), DhcpError> {
        let mac = mac.to_string();
        let hostname = hostname.to_string();
        let lan_dhcpd = self.get_lan_dhcpd();
        let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;

        if !Self::is_valid_mac(&mac) {
            return Err(DhcpError::InvalidMacAddress(mac));
        }

        // TODO validate that address is in subnet range

        if existing_mappings.iter().any(|m| {
            m.ipaddr
                .parse::<Ipv4Addr>()
                .expect("Mapping contains invalid ipv4")
                == ipaddr
                && m.mac == mac
        }) {
            info!(
                "Mapping already exists for {} [{}], skipping",
                ipaddr.to_string(),
                mac
            );
            return Ok(());
        }

        if existing_mappings.iter().any(|m| {
            m.ipaddr
                .parse::<Ipv4Addr>()
                .expect("Mapping contains invalid ipv4")
                == ipaddr
        }) {
            return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string()));
        }

        if existing_mappings.iter().any(|m| m.mac == mac) {
            return Err(DhcpError::MacAddressAlreadyMapped(mac));
        }

        let static_map = StaticMap {
            mac,
            ipaddr: ipaddr.to_string(),
            hostname,
            descr: Default::default(),
            winsserver: Default::default(),
            dnsserver: Default::default(),
            ntpserver: Default::default(),
        };

        existing_mappings.push(static_map);
        Ok(())
    }

    fn is_valid_mac(mac: &str) -> bool {
        let parts: Vec<&str> = mac.split(':').collect();
        if parts.len() != 6 {
            return false;
        }

        parts
            .iter()
            .all(|part| part.len() <= 2 && part.chars().all(|c| c.is_ascii_hexdigit()))
    }

    pub async fn get_static_mappings(&self) -> Result<Vec<StaticMap>, Error> {
        let list_static_output = self
            .opnsense_shell
            .exec("configctl dhcpd list static")
            .await?;

        let value: serde_json::Value = serde_json::from_str(&list_static_output).expect(&format!(
            "Got invalid json from configctl {list_static_output}"
        ));
        let static_maps = value["dhcpd"]
            .as_array()
            .ok_or(Error::Command(format!(
                "Invalid DHCP data from configctl command, got {list_static_output}"
            )))?
            .iter()
            .map(|entry| StaticMap {
                mac: entry["mac"].as_str().unwrap_or_default().to_string(),
                ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(),
                hostname: entry["hostname"].as_str().unwrap_or_default().to_string(),
                descr: entry["descr"].as_str().map(MaybeString::from),
                winsserver: MaybeString::default(),
                dnsserver: MaybeString::default(),
                ntpserver: MaybeString::default(),
            })
            .collect();

        Ok(static_maps)
    }
    pub fn enable_netboot(&mut self) {
        self.get_lan_dhcpd().netboot = Some(1);
    }

    pub fn set_next_server(&mut self, ip: Ipv4Addr) {
        self.enable_netboot();
        self.get_lan_dhcpd().nextserver = Some(ip.to_string());
        self.get_lan_dhcpd().tftp = Some(ip.to_string());
    }

    pub fn set_boot_filename(&mut self, boot_filename: &str) {
        self.enable_netboot();
        self.get_lan_dhcpd().bootfilename = Some(boot_filename.to_string());
    }

    pub fn set_filename(&mut self, filename: &str) {
        self.enable_netboot();
        self.get_lan_dhcpd().filename = Some(filename.to_string());
    }

    pub fn set_filename64(&mut self, filename64: &str) {
        self.enable_netboot();
        self.get_lan_dhcpd().filename64 = Some(filename64.to_string());
    }

    pub fn set_filenameipxe(&mut self, filenameipxe: &str) {
        self.enable_netboot();
        self.get_lan_dhcpd().filenameipxe = Some(filenameipxe.to_string());
    }
}
