use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::net::Ipv6Addr;
use ipnet::{IpBitOr, IpSub};
use std::str::FromStr;
use log::{error, warn};
use serde_derive::{Deserialize, Serialize};
use systemd_journal_logger::connected_to_journal;
use crate::cloudflare::DnsRecordType;
use crate::cloudflare::DnsRecordType::{A, AAAA};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub(crate) struct InterfaceConfig {
    pub(crate) host_address: Ipv6Addr,
    pub(crate) interfaces: HashMap<String, Ipv6Addr>,
}

impl InterfaceConfig {
    pub(crate) fn load() -> Result<Self, Box<dyn Error>> {
        let cfg: Self = match confy::load(env!("CARGO_PKG_NAME"),"interfaces") {
            Ok(data) => data,
            Err(e) => {
                match connected_to_journal() {
                    true => error!("[ERROR] {e}"),
                    false => eprintln!("[ERROR] {e}")
                }
                return Err(Box::new(e));
            }
        };

        Ok(cfg)
    }

    pub(crate) fn full_v6(&self, interface_name: &String, host_v6: Ipv6Addr) -> Result<Ipv6Addr, ()> {
        let host_range = Ipv6Addr::from(host_v6.saturating_sub(self.host_address));
        let interface_address = match self.interfaces.get(interface_name) {
            Some(address) => *address,
            None => {
                let err_msg = format!("Malformed or missing IP in interfaces.toml for interface {}", interface_name);
                match connected_to_journal() {
                    true => error!("[ERROR] {err_msg}"),
                    false => eprintln!("[ERROR] {err_msg}"),
                }
                return Err(());
            }
        };
        Ok(host_range.bitor(interface_address))
    }
}

impl Default for InterfaceConfig {
    fn default() -> Self {
        InterfaceConfig {
            host_address: Ipv6Addr::from_str("::").expect("Malformed literal in code"),
            interfaces: HashMap::from([(" ".to_owned(), Ipv6Addr::from(0))]),
        }
    }
}

///////////////

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub(crate) struct ZoneEntry {
    pub(crate) name: String,
    pub(crate) r#type: Vec<DnsRecordType>,
    pub(crate) interface: String,
}

impl Default for ZoneEntry {
    fn default() -> Self {
        ZoneEntry {
            name: " ".to_owned(),
            r#type: vec![A, AAAA],
            interface: " ".to_owned(),
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub(crate) struct ZoneConfig {
    pub(crate) email: String,
    pub(crate) name: String,
    pub(crate) id: String,
    #[serde(alias="entry")]
    pub(crate) entries: Vec<ZoneEntry>
}

impl ZoneConfig {
    pub(crate) fn load() -> Result<Vec<Self>, Box<dyn Error>> {
        let path = confy::get_configuration_file_path(env!("CARGO_PKG_NAME"), "interfaces").expect("Something went wrong with confy");
        let zones_dir = path.parent().expect("Something went wrong with confy").join("zones.d/");

        let zones = fs::read_dir(zones_dir).expect("Directory creation handling does not yet exist");


        let mut zone_configs: Vec<Self> = vec![];

        for entry in zones {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                let warn_msg = "Subdirectory in zones.d detected, this should not be the case";
                match connected_to_journal() {
                    true => warn!("[WARN] {warn_msg}"),
                    false => eprintln!("[WARN] {warn_msg}"),
                }
            }
            else if !path.file_name().expect("Directories should have been filtered out by the leading if")
                .to_str().expect("Conversion to str should not fail for a file name")
                .starts_with('.') {
                let zone_config_path = format!("zones.d/{}", path.file_stem()
                    .expect("stem could not be extracted from filename").to_str()
                    .expect("&OsStr could not be converted to &str"));
                match confy::load(env!("CARGO_PKG_NAME"), zone_config_path.as_str()) {
                    Ok(data) => zone_configs.push(data),
                    Err(e) => {
                        match connected_to_journal() {
                            true => error!("[ERROR] {e}"),
                            false => eprintln!("[ERROR] {e}"),
                        }
                        return Err(Box::new(e));
                    }
                };
            }
        }

        Ok(zone_configs)
    }
}

impl Default for ZoneConfig {
    fn default() -> Self {
        ZoneConfig {
            email: " ".to_owned(),
            name: " ".to_owned(),
            id: " ".to_owned(),
            entries: vec![ZoneEntry::default()],
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub(crate) struct AppConfig {
    pub(crate) cloudflare_api_token: String,
    pub(crate) check_interval_seconds: Option<u16>,
    pub(crate) uptime_url: Option<String>,
}

impl AppConfig {
    pub(crate) fn load() -> Result<Self, Box<dyn Error>> {
        let cfg: Self = match confy::load(env!("CARGO_PKG_NAME"),"config") {
            Ok(data) => data,
            Err(e) => {
                match connected_to_journal() {
                    true => error!("[ERROR] {e}"),
                    false => eprintln!("[ERROR] {e}")
                }
                return Err(Box::new(e));
            }
        };

        if cfg.cloudflare_api_token.is_empty() {
            let err_msg = "Cloudflare api token not specified. The app cannot work without this";
            match connected_to_journal() {
                true => error!("[ERROR] {err_msg}"),
                false => eprintln!("[ERROR] {err_msg}")
            }
            panic!("{err_msg}");
        }

        Ok(cfg)
    }
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            cloudflare_api_token: "".to_owned(),
            check_interval_seconds: None,
            uptime_url: None
        }
    }
}