/*use cloudflare_old::{Instance, CloudflareDnsType};*/
use reqwest::blocking::get;
use std::{thread::{sleep}};
use std::error::Error;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use chrono::{Utc, Duration};
use log::{info, warn, error, LevelFilter};
use reqwest::StatusCode;
use systemd_journal_logger::{connected_to_journal, JournalLog};
use crate::cloudflare::{CloudflareZone, DnsRecordType};
use crate::config::{AppConfig, InterfaceConfig, ZoneConfig, ZoneEntry};

mod config;
mod cloudflare;

struct Addresses {
    ipv4_uri: String,
    ipv6_uri: String,
    ipv4: Ipv4Addr,
    ipv6: Ipv6Addr,
}

impl Addresses {
    fn new() -> Result<Self, Box<dyn Error>> {
        let mut ret = Self {
            ipv4_uri: "https://ipinfo.io/ip".to_owned(),
            ipv6_uri: "https://v6.ipinfo.io/ip".to_owned(),
            ipv4: Ipv4Addr::new(0, 0, 0, 0),
            ipv6: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)
        };

        match ret.get_v4() {
            Ok(ip) => ret.ipv4 = ip,
            Err(_) => {
                let err_msg = format!("Unable to fetch IPv4 from '{}' during init. Aborting!", ret.ipv4_uri);
                match connected_to_journal() {
                    true => error!("[ERROR] {err_msg}"),
                    false => eprintln!("[ERROR] {err_msg}"),
                }
                panic!("{}", err_msg);
            }
        }

        match ret.get_v6() {
            Ok(ip) => ret.ipv6 = ip,
            Err(_) => {
                let err_msg = format!("Unable to fetch IPv6 from '{}' during init. Aborting!", ret.ipv6_uri);
                match connected_to_journal() {
                    true => error!("[ERROR] {err_msg}"),
                    false => eprintln!("[ERROR] {err_msg}"),
                }
                panic!("{}", err_msg);
            }
        }

        Ok(ret)
    }

    fn update(&mut self) {
        match self.get_v4() {
            Ok(ip) => {
                if ip != self.ipv4 {
                    if ip == Ipv4Addr::new(0,0,0,0) {
                        let warn_msg = "'0.0.0.0' detected as new IPv4, skipping changes".to_owned();
                        match connected_to_journal() {
                            true => warn!("[WARN] {warn_msg}"),
                            false => println!("[WARN] {warn_msg}"),
                        }
                    }
                    else {
                        let info_msg = format!("IPv4 changed from '{}' to '{}'", self.ipv4, ip);
                        match connected_to_journal() {
                            true => info!("[INFO] {info_msg}"),
                            false => println!("[INFO] {info_msg}"),
                        }
                        self.ipv4 = ip;
                    }
                }
            }
            Err(e) => {
                let error_msg = format!("Unable to fetch IPv4 from '{}': {}", self.ipv4_uri, e);
                match connected_to_journal() {
                    true => error!("[ERROR] {error_msg}"),
                    false => eprintln!("[ERROR] {error_msg}"),
                }
            }
        }

        match self.get_v6() {
            Ok(ip) => {
                if ip != self.ipv6 {
                    if ip == Ipv6Addr::new(0,0,0,0,0,0,0,0) {
                        let warn_msg = "'::' detected as new IPv6, skipping changes".to_owned();
                        match connected_to_journal() {
                            true => warn!("[WARN] {warn_msg}"),
                            false => println!("[WARN] {warn_msg}"),
                        }
                    }
                    else {
                        let info_msg = format!("IPv6 changed from '{}' to '{}'", self.ipv6, ip);
                        match connected_to_journal() {
                            true => info!("[INFO] {info_msg}"),
                            false => println!("[INFO] {info_msg}"),
                        }
                        self.ipv6 = ip;
                    }
                }
            }
            Err(e) => {
                let error_msg = format!("Unable to fetch IPv6 from '{}': {}", self.ipv6_uri, e);
                match connected_to_journal() {
                    true => error!("[ERROR] {error_msg}"),
                    false => eprintln!("[ERROR] {error_msg}"),
                }
            }
        }
    }

    fn get_v4(&self) -> Result<Ipv4Addr, reqwest::Error> {
        match get(&self.ipv4_uri) {
            Ok(res) => {
                match res.status() {
                    StatusCode::OK => {
                        let ip_string = res.text().expect("Returned data should always contain text");
                        Ok(Ipv4Addr::from_str(ip_string.as_str()).expect("Returned IP should always be parseable"))
                    },
                    _ => {
                        let warn_msg = format!("Unexpected HTTP status {}", res.status());
                        match connected_to_journal() {
                            true => warn!("[WARN] {warn_msg}"),
                            false => println!("[WARN] {warn_msg}"),
                        }
                        Ok(Ipv4Addr::new(0, 0, 0, 0))
                    }
                }
            }
            Err(e) => Err(e),
        }

    }

    fn get_v6(&self) -> Result<Ipv6Addr, reqwest::Error> {
        match get(&self.ipv6_uri) {
            Ok(res) => {
                match res.status() {
                    StatusCode::OK => {
                        let ip_string: String = res.text().expect("Returned data should always contain text");
                        Ok(Ipv6Addr::from_str(ip_string.as_str()).expect("Returned IP should always be parseable"))
                    },
                    _ => {
                        let warn_msg = format!("Unexpected HTTP status {}", res.status());
                        match connected_to_journal() {
                            true => warn!("[WARN] {warn_msg}"),
                            false => println!("[WARN] {warn_msg}"),
                        }
                        Ok(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0))
                    }
                }
            },
            Err(e) => Err(e),
        }
    }
}

fn compare_zones(old_zone: &ZoneConfig, new_zone: &ZoneConfig) -> Vec<String> {
    let mut info_pieces = vec![];
    if old_zone.id != new_zone.id || old_zone.email != new_zone.email {
        let msg = "changed id or email".to_owned();
        info_pieces.push(msg);
    }

    if old_zone.entries != new_zone.entries {
        let mut added: Vec<&ZoneEntry> = vec![];
        let mut modified: Vec<&ZoneEntry> = vec![];

        new_zone.entries.iter().for_each(|entry| {
            let matches: Vec<&ZoneEntry> = old_zone.entries.iter().filter(|old_entry| {
                if old_entry.name == entry.name {
                    if old_entry != &entry {
                        modified.push(entry);
                    }
                    true
                }
                else {
                    false
                }
            }).collect();

            if matches.is_empty() {
                added.push(entry);
            }
        });

        let deleted: Vec<&ZoneEntry> = old_zone.entries.iter().filter(|old_entry| {
            !new_zone.entries.contains(old_entry) &&
                !new_zone.entries.iter().any(|changed_entry| { changed_entry.name == old_entry.name })
        }).collect();

        if !deleted.is_empty() {
            let mut deleted_entries_vec = vec![];

            for entry in deleted {
                deleted_entries_vec.push(entry.name.clone());
            }

            let deleted_entries = match deleted_entries_vec.len() {
                1 => deleted_entries_vec[0].clone(),
                2 => deleted_entries_vec.join(" & "),
                _ => deleted_entries_vec.join(", "),
            };
            let msg = format!("deleted {deleted_entries}");
            info_pieces.push(msg);
        }

        if !added.is_empty() {
            let mut added_entries_vec = vec![];

            for entry in added {
                added_entries_vec.push(entry.name.clone());
            }

            let added_entries = match added_entries_vec.len() {
                1 => added_entries_vec[0].clone(),
                2 => added_entries_vec.join(" & "),
                _ => added_entries_vec.join(", "),
            };
            let msg = format!("added {added_entries}");
            info_pieces.push(msg);
        }

        if !modified.is_empty() {
            let mut modified_entries_vec = vec![];

            for entry in modified {
                modified_entries_vec.push(entry.name.clone());
            }

            let modified_entries = match modified_entries_vec.len() {
                1 => modified_entries_vec[0].clone(),
                2 => modified_entries_vec.join(" & "),
                _ => modified_entries_vec.join(", "),
            };
            let msg = format!("modified {modified_entries}");
            info_pieces.push(msg);
        }
    }

    info_pieces
}

fn main() {
    JournalLog::new().expect("Systemd-Logger crate error").install().expect("Systemd-Logger crate error");
    log::set_max_level(LevelFilter::Info);

    let mut config = AppConfig::load().unwrap();
    let mut ifaces = InterfaceConfig::load().unwrap();
    let mut zone_cfgs = ZoneConfig::load().unwrap();

    let mut now = Utc::now() - Duration::seconds(59);
    let mut start = now;

    let mut ips = match Addresses::new() {
        Ok(ips) => ips,
        Err(e) => panic!("{}", e)
    };

    let reload_interval = config.check_interval_seconds.unwrap_or_else(|| {
        let warn_msg = "Reload interval option not set, defaulting to 60";
        match connected_to_journal() {
            true => warn!("[WARN] {warn_msg}"),
            false => println!("[WARN] {warn_msg}"),
        }
        60
    }) as i64;

    loop {
        now = Utc::now();
        if now >= start + Duration::seconds(reload_interval) {
            start = now;

            if let Some(uptime_url) = &config.uptime_url {
                let _ = get(uptime_url);
            }

            match InterfaceConfig::load() {
                Ok(new_cfg) => {
                    if ifaces != new_cfg {
                        if ifaces.host_address != new_cfg.host_address {
                            let info_msg = format!("Host address in interfaces.toml changed from '{}' to '{}'", ifaces.host_address, new_cfg.host_address);
                            match connected_to_journal() {
                                true => info!("[INFO] {info_msg}"),
                                false => println!("[INFO] {info_msg}"),
                            }
                        }

                        if ifaces.interfaces != new_cfg.interfaces {
                            let mut new: Vec<(&String, &Ipv6Addr)> = vec![];
                            let mut modified: Vec<(&String, &Ipv6Addr)> = vec![];

                            new_cfg.interfaces.iter().for_each(|(interface, address)| {
                                if ifaces.interfaces.contains_key(interface) {
                                    if ifaces.interfaces.get(interface) != Some(address) {
                                        modified.push((interface, address));
                                    }
                                }
                                else {
                                    let matches: Vec<&Ipv6Addr> = ifaces.interfaces.values().filter(|addr| {
                                        addr == &address
                                    }).collect();

                                    if matches.is_empty() {
                                        new.push((interface, address));
                                    }
                                    else {
                                        modified.push((interface, address));
                                    }
                                }
                            });

                            let deleted: Vec<(&String, &Ipv6Addr)> = ifaces.interfaces.iter().filter(|(interface, address)| {
                                !new_cfg.interfaces.contains_key(*interface) && !modified.iter().any(|(_, new_addr)| { new_addr == address })
                            }).collect();

                            for (name, addr) in deleted {
                                let info_msg = format!("Deleted interface '{name}' with address '{addr}'");
                                match connected_to_journal() {
                                    true => info!("[INFO] {info_msg}"),
                                    false => println!("[INFO] {info_msg}"),
                                }
                            }

                            for (name, addr) in new {
                                let info_msg = format!("Added interface '{name}' with address '{addr}'");
                                match connected_to_journal() {
                                    true => info!("[INFO] {info_msg}"),
                                    false => println!("[INFO] {info_msg}"),
                                }
                            }

                            for (name, addr) in modified {
                                let info_msg= if ifaces.interfaces.contains_key(name) {
                                    let old_addr = ifaces.interfaces.get(name).expect("contains check on ifaces was successful");
                                    format!("Changed interface address of '{name}' from '{old_addr}' to '{addr}'")
                                }
                                else {
                                    let old_name = ifaces.interfaces.iter()
                                        .find(|(_, old_addr)| { old_addr == &addr })
                                        .expect("modified entry should not exist if this fails")
                                        .0;
                                    format!("Changed interface name for '{addr}' from '{old_name}' to '{name}'")
                                };

                                match connected_to_journal() {
                                    true => info!("[INFO] {info_msg}"),
                                    false => println!("[INFO] {info_msg}"),
                                }
                            }
                        }

                        ifaces = new_cfg
                    }
                },
                Err(e) => {
                    let err_msg = format!("Unable to load ínterfaces.toml with error: {e}");
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                }
            }

            match ZoneConfig::load() {
                Ok(new_cfgs) => {
                    if zone_cfgs != new_cfgs {
                        if zone_cfgs.len() != new_cfgs.len() {
                            let new_zones: Vec<&ZoneConfig> = new_cfgs.iter().filter(|zone_cfg| {
                                !zone_cfgs.contains(zone_cfg)
                            }).collect();

                            let deleted_zones: Vec<&ZoneConfig> = zone_cfgs.iter().filter(|zone_cfg| {
                                !new_cfgs.contains(zone_cfg)
                            }).collect();

                            for new_zone in new_zones {
                                let name = new_zone.name.as_str();
                                let entry_count = new_zone.entries.len();
                                let info_msg = format!("Added Zone '{name}' with {entry_count} entries");
                                match connected_to_journal() {
                                    true => info!("[INFO] {info_msg}"),
                                    false => println!("[INFO] {info_msg}"),
                                }
                            }

                            for deleted_zone in deleted_zones {
                                let name = deleted_zone.name.as_str();
                                let entry_count = deleted_zone.entries.len();
                                let info_msg = format!("Deleted Zone '{name}' with {entry_count} entries");
                                match connected_to_journal() {
                                    true => info!("[INFO] {info_msg}"),
                                    false => println!("[INFO] {info_msg}"),
                                }
                            }
                        }
                        else {
                            let changed_zones: Vec<&ZoneConfig> = new_cfgs.iter().filter(|new_cfg| {
                                zone_cfgs.iter().any(|old_cfg| {
                                    old_cfg.name == new_cfg.name && &old_cfg != new_cfg
                                })
                            }).collect();

                            let deleted_zones: Vec<&ZoneConfig> = zone_cfgs.iter().filter(|old_cfg| {
                                !new_cfgs.iter().any(|new_cfg| {
                                    old_cfg.name == new_cfg.name
                                })
                            }).collect();

                            let added_zones: Vec<&ZoneConfig> = new_cfgs.iter().filter(|new_cfg| {
                                !zone_cfgs.iter().any(|old_cfg| {
                                    old_cfg.name == new_cfg.name
                                })
                            }).collect();

                            if deleted_zones.len() == 1 && added_zones.len() == 1 {
                                let new_zone = added_zones[0];
                                let old_zone = deleted_zones[0];

                                let mut info_pieces = vec![];

                                {
                                    let msg = format!("name from '{}' to '{}'", old_zone.name, new_zone.name);
                                    info_pieces.push(msg);
                                }

                                let info_pieces = [info_pieces, compare_zones(old_zone, new_zone)].concat();

                                println!("{}", info_pieces.join(", "));
                            }
                            else {
                                if !deleted_zones.is_empty() {
                                    let mut info_pieces = vec![];
                                    for deleted_zone in deleted_zones {
                                        info_pieces.push(deleted_zone.name.clone());
                                    }
                                    let deleted_info = match info_pieces.len() {
                                        1 => info_pieces[0].clone(),
                                        2 => info_pieces.join(" & "),
                                        _ => info_pieces.join(", "),
                                    };
                                    let info_msg = format!("Deleted {deleted_info}");
                                    match connected_to_journal() {
                                        true => info!("[INFO] {info_msg}"),
                                        false => println!("[INFO] {info_msg}"),
                                    }
                                }

                                if !added_zones.is_empty() {
                                    let mut info_pieces = vec![];
                                    for added_zone in added_zones {
                                        info_pieces.push(added_zone.name.clone());
                                    }
                                    let added_info = match info_pieces.len() {
                                        1 => info_pieces[0].clone(),
                                        2 => info_pieces.join(" & "),
                                        _ => info_pieces.join(", "),
                                    };
                                    let info_msg = format!("Added {added_info}");
                                    match connected_to_journal() {
                                        true => info!("[INFO] {info_msg}"),
                                        false => println!("[INFO] {info_msg}"),
                                    }
                                }
                            }

                            for changed_zone in changed_zones {
                                // try find element where one of these is unchanged
                                let old_zone = zone_cfgs.iter().find(|zone_cfg| {
                                        zone_cfg.name == changed_zone.name
                                }).expect("This element should exist because it was added to the changed_zones vector");

                                let info_pieces = compare_zones(old_zone, changed_zone);

                                let changed_info = match info_pieces.len() {
                                    1 => info_pieces[0].clone(),
                                    2 => info_pieces.join(" & "),
                                    _ => info_pieces.join(", "),
                                };
                                let info_msg_raw = format!("{changed_info} for {}", changed_zone.name);
                                let mut info_msg_chars = info_msg_raw.chars();
                                let info_msg = match info_msg_chars.next() {
                                    None => String::new(),
                                    Some(first) => first.to_uppercase().collect::<String>() + info_msg_chars.as_str(),
                                };
                                match connected_to_journal() {
                                    true => info!("[INFO] {info_msg}"),
                                    false => println!("[INFO] {info_msg}"),
                                }
                            }
                        }

                        zone_cfgs = new_cfgs
                    }
                }
                Err(e) => {
                    let err_msg = format!("Unable to load from zones.d with error: {e}");
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                }
            }

            match AppConfig::load() {
                Ok(new_cfg) => {
                    if config != new_cfg {
                        if config.cloudflare_api_token != new_cfg.cloudflare_api_token {
                            let info_msg = "API token in config.toml changed";
                            match connected_to_journal() {
                                true => info!("[INFO] {info_msg}"),
                                false => println!("[INFO] {info_msg}"),
                            }
                        }

                        if config.check_interval_seconds != new_cfg.check_interval_seconds {
                            let info_msg = match config.check_interval_seconds {
                                Some(old_interval) => {
                                    match new_cfg.check_interval_seconds {
                                        Some(new_interval) => format!("Check interval in config.toml changed from {old_interval}s to {new_interval}s"),
                                        None => format!("Check interval in config.toml changed from {old_interval}s to 60s"),
                                    }
                                },
                                None => {
                                    match new_cfg.check_interval_seconds {
                                        Some(new_interval) => format!("Check interval in config.toml changed from 60s to {new_interval}s"),
                                        None => "This is a unicorn error, congratulations.".to_owned(),
                                    }
                                }
                            };
                            match connected_to_journal() {
                                true => info!("[INFO] {info_msg}"),
                                false => println!("[INFO] {info_msg}"),
                            }
                        }

                        if config.uptime_url != new_cfg.uptime_url {
                            let info_msg = match &config.uptime_url {
                                Some(old_url) => {
                                    match &new_cfg.uptime_url {
                                        Some(new_url) => format!("Uptime URL in config.toml changed from '{old_url}' to '{new_url}'"),
                                        None => "Uptime URL in config.toml was removed".to_owned(),
                                    }
                                },
                                None => {
                                    match &new_cfg.uptime_url {
                                        Some(new_url) => format!("Uptime URL '{new_url}' was added to config.toml"),
                                        None => "This is a unicorn error, congratulations.".to_owned(),
                                    }
                                }
                            };
                            match connected_to_journal() {
                                true => info!("[INFO] {info_msg}"),
                                false => println!("[INFO] {info_msg}"),
                            }
                        }

                        config = new_cfg
                    }
                }
                Err(e) => {
                    let err_msg = format!("Unable to load config.toml with error: {e}");
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                }
            }

            ips.update();
            for zone in &zone_cfgs {
                let cf_zone = CloudflareZone::new(zone, &config);

                let cf_entries = match cf_zone.get_entries(None) {
                    Ok(entries) => entries,
                    Err(_) => {
                        continue
                    }
                };

                for entry in &zone.entries {
                    let ipv6;
                    let ipv4;
                    match entry.r#type[..] {
                        [DnsRecordType::AAAA, DnsRecordType::A] => {
                            ipv6 = match ifaces.full_v6(&entry.interface, ips.ipv6) {
                                Ok(ip) => Some(ip),
                                Err(_) => {
                                    continue
                                }
                            };
                            ipv4 = Some(ips.ipv4);
                        },
                        [DnsRecordType::A, DnsRecordType::AAAA] => {
                            ipv6 = match ifaces.full_v6(&entry.interface, ips.ipv6) {
                                Ok(ip) => Some(ip),
                                Err(_) => {
                                    continue
                                }
                            };
                            ipv4 = Some(ips.ipv4);
                        },
                        [DnsRecordType::AAAA] => {
                            ipv6 = match ifaces.full_v6(&entry.interface, ips.ipv6) {
                                Ok(ip) => Some(ip),
                                Err(_) => {
                                    continue
                                }
                            };
                            ipv4 = None;
                        },
                        [DnsRecordType::A] => {
                            ipv6 = None;
                            ipv4 = Some(ips.ipv4);
                        },
                        _ => {
                            let warn_msg = "Config contains unsupported type identifier";
                            match connected_to_journal() {
                                true => warn!("[WARN] {warn_msg}"),
                                false => println!("[WARN] {warn_msg}"),
                            }
                            continue
                        }
                    }

                    for r#type in &entry.r#type {
                        let cf_entry = cf_entries.iter().find(|cf_entry| {
                            cf_entry.name == entry.name && &cf_entry.r#type == r#type
                        });

                        if let Some(cf_entry) = cf_entry {
                            match cf_entry.r#type {
                                DnsRecordType::A => {
                                    let cf_ip = Ipv4Addr::from_str(cf_entry.content.as_str()).expect("Cloudflare return should always be valid IP");
                                    if Some(cf_ip) == ipv4 {
                                        continue
                                    }
                                },
                                DnsRecordType::AAAA => {
                                    let cf_ip = Ipv6Addr::from_str(cf_entry.content.as_str()).expect("Cloudflare return should always be valid IP");
                                    if Some(cf_ip) == ipv6 {
                                        continue
                                    }
                                },
                                _ => {},
                            }

                            if cf_zone.update(entry, r#type, &cf_entry.id, ipv6, ipv4).is_ok() {
                                let info_msg = format!("Updated {} DNS Record for entry '{}' in zone '{}'", r#type, entry.name, zone.name);
                                match connected_to_journal() {
                                    true => info!("[INFO] {info_msg}"),
                                    false => println!("[INFO] {info_msg}"),
                                }
                            }
                        }
                        else if cf_zone.create(entry, r#type, ipv6, ipv4).is_ok() {
                            let info_msg = format!("Created {} DNS Record for entry '{}' in zone '{}'", r#type, entry.name, zone.name);
                            match connected_to_journal() {
                                true => info!("[INFO] {info_msg}"),
                                false => println!("[INFO] {info_msg}"),
                            };
                        }
                    }
                    // handle return values
                }
            }
        }
        else {
            sleep(std::time::Duration::from_millis(200));
        }
    }
}