use std::collections::HashMap;
use std::error::Error;
use std::net::{Ipv4Addr, Ipv6Addr};
use log::{error, warn};
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{Url};
use reqwest::blocking::{Response, Client};
use serde_derive::{Deserialize, Serialize};
use strum_macros::{Display, IntoStaticStr};
use systemd_journal_logger::connected_to_journal;
use url::ParseError;
use crate::config::{AppConfig, ZoneConfig, ZoneEntry};

const API_BASE: &str = "https://api.cloudflare.com/client/v4";

#[derive(Serialize, Deserialize, Debug)]
struct CloudflareApiResults {
    result: Vec<CloudflareDnsRecord>,
    success: bool,
    result_info: CloudflareApiPagination,
}

#[derive(Serialize, Deserialize, Debug)]
struct  CloudflareApiPagination {
    count: u8,
    page: u8,
    per_page: u8,
    total_count: u16
}

#[derive(Serialize, Deserialize, Debug)]
struct CloudflareApiResult {
    success: bool,
}

pub(crate) struct CloudflareZone {
    name: String,
    email: String,
    key: String,
    id: String,
}

impl CloudflareZone {
    pub(crate) fn new(zone: &ZoneConfig, config: &AppConfig) -> Self {
        let key = config.cloudflare_api_token.clone();
        Self {
            name: zone.name.clone(),
            email: zone.email.clone(),
            key,
            id: zone.id.clone(),
        }
    }

    fn generate_auth_headers(&self) -> HeaderMap {
        let mut headers = HeaderMap::new();
        headers.insert(
            "X-Auth-Email",
            HeaderValue::from_str(self.email.as_str()).expect("After CloudflareZone gets created the required Values should exist"),
        );
        headers.insert(
            "X-Auth-Key",
            HeaderValue::from_str(self.key.as_str()).expect("After CloudflareZone gets created the required Values should exist"),
        );
        headers
    }

    pub(crate) fn get_entries(&self, page: Option<u8>) -> Result<Vec<CloudflareDnsRecord>, ()> {
        let endpoint = format!("{}/zones/{}/dns_records?page={}", API_BASE, self.id, page.unwrap_or(1));

        match self.get(&endpoint) {
            Ok(response) => {
                if response.status().is_success() {
                    let entries = match response.json::<CloudflareApiResults>() {
                        Ok(mut data) => { 
                            let actual_count: u16 = (data.result_info.per_page * (data.result_info.page - 1) + data.result_info.count) as u16;
                            if  actual_count < data.result_info.total_count { 
                                data.result.append(&mut self.get_entries(Some(page.unwrap_or(1) + 1)).expect("Pagination indicates more results"));
                            }
                            data 
                        },
                        Err(e) => {
                            let err_msg = format!("Unable to parse API response: {e}");
                            match connected_to_journal() {
                                true => error!("[ERROR] {err_msg}"),
                                false => eprintln!("[ERROR] {err_msg}"),
                            }
                            return Err(())
                        }
                    };

                    Ok(entries.result)
                } else {
                    let err_msg = format!("Unable to fetch Cloudflare Zone Entries for {}: {}",self.name ,response.status());
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                    Err(())
                }
            }
            Err(e) => {
                let err_msg = format!("Unable to access Cloudflare API: {e}");
                match connected_to_journal() {
                    true => error!("[ERROR] {err_msg}"),
                    false => eprintln!("[ERROR] {err_msg}"),
                }
                Err(())
            },
        }
    }

    pub(crate) fn update(&self, entry: &ZoneEntry, r#type: &DnsRecordType, id: &String, ipv6: Option<Ipv6Addr>, ipv4: Option<Ipv4Addr>) -> Result<(), ()> {
        let endpoint = format!("{}/zones/{}/dns_records/{}", API_BASE, self.id, id);

        return match r#type {
            DnsRecordType::A => {
                if let Some(ip) = ipv4 {
                    let json_body = self.create_body("A", &entry.name, ip.to_string().as_str());

                    match self.put(&endpoint, &json_body) {
                        Ok(response) => {
                            self.validate_response(response)
                        },
                        Err(e) => {
                            let err_msg = format!("Unable to access Cloudflare API: {e}");
                            match connected_to_journal() {
                                true => error!("[ERROR] {err_msg}"),
                                false => eprintln!("[ERROR] {err_msg}"),
                            }
                            Err(())
                        }
                    }
                } else {
                    let err_msg = "Missing IPv4 for Update.";
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                    Err(())
                }
            },
            DnsRecordType::AAAA => {
                if let Some(ip) = ipv6 {
                    let json_body = self.create_body("AAAA", &entry.name, ip.to_string().as_str());

                    match self.put(&endpoint, &json_body) {
                        Ok(response) => {
                            self.validate_response(response)
                        },
                        Err(e) => {
                            let err_msg = format!("Unable to access Cloudflare API: {e}");
                            match connected_to_journal() {
                                true => error!("[ERROR] {err_msg}"),
                                false => eprintln!("[ERROR] {err_msg}"),
                            }
                            Err(())
                        }
                    }
                } else {
                    let err_msg = "Missing IPv6 for Update.";
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                    Err(())
                }
            },
            _ => {
                let warn_msg = "Config contains unsupported type identifier";
                match connected_to_journal() {
                    true => warn!("[WARN] {warn_msg}"),
                    false => println!("[WARN] {warn_msg}"),
                }
                Err(())
            }
        }
    }

    pub(crate) fn create(&self, entry: &ZoneEntry, r#type: &DnsRecordType, ipv6: Option<Ipv6Addr>, ipv4: Option<Ipv4Addr>) -> Result<(), ()> {
        let endpoint = format!("{}/zones/{}/dns_records", API_BASE, self.id);

        return match r#type {
            DnsRecordType::A => {
                if let Some(ip) = ipv4 {
                    let json_body = self.create_body("A", &entry.name, ip.to_string().as_str());

                    match self.post(&endpoint, &json_body) {
                        Ok(response) => {
                            self.validate_response(response)
                        },
                        Err(e) => {
                            let err_msg = format!("Unable to access Cloudflare API: {e}");
                            match connected_to_journal() {
                                true => error!("[ERROR] {err_msg}"),
                                false => eprintln!("[ERROR] {err_msg}"),
                            }
                            Err(())
                        }
                    }
                } else {
                    let err_msg = "Missing IPv4 for Update.";
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                    Err(())
                }
            },
            DnsRecordType::AAAA => {
                if let Some(ip) = ipv6 {
                    let json_body = self.create_body("AAAA", &entry.name, ip.to_string().as_str());

                    match self.post(&endpoint, &json_body) {
                        Ok(response) => {
                            self.validate_response(response)
                        },
                        Err(e) => {
                            let err_msg = format!("Unable to access Cloudflare API: {e}");
                            match connected_to_journal() {
                                true => error!("[ERROR] {err_msg}"),
                                false => eprintln!("[ERROR] {err_msg}"),
                            }
                            Err(())
                        }
                    }
                } else {
                    let err_msg = "Missing IPv6 for Update.";
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                    Err(())
                }
            },
            _ => {
                let warn_msg = "Config contains unsupported type identifier";
                match connected_to_journal() {
                    true => warn!("[WARN] {warn_msg}"),
                    false => println!("[WARN] {warn_msg}"),
                }
                Err(())
            }
        }
    }

    fn get(&self, url: &str) -> Result<Response, Box<dyn Error>> {
        let url_parsed = self.parse_url(url)?;

        match Client::new()
            .get(url_parsed)
            .headers(self.generate_auth_headers())
            .send() {
            Ok(result) => Ok(result),
            Err(e) => Err(Box::new(e)),
        }
    }

    fn post(&self, url: &str, data: &HashMap<String, String>) -> Result<Response, Box<dyn Error>> {
        let url_parsed = self.parse_url(url)?;

        match Client::new()
            .post(url_parsed)
            .headers(self.generate_auth_headers())
            .json(data)
            .send() {
            Ok(result) => Ok(result),
            Err(e) => Err(Box::new(e)),
        }
    }

    fn put(&self, url: &str, data: &HashMap<String, String>) -> Result<Response, Box<dyn Error>> {
        let url_parsed = self.parse_url(url)?;

        match Client::new()
            .put(url_parsed)
            .headers(self.generate_auth_headers())
            .json(data)
            .send() {
            Ok(result) => Ok(result),
            Err(e) => Err(Box::new(e)),
        }
    }

    fn parse_url(&self, input: &str) -> Result<Url, ParseError> {
        match Url::parse(input) {
            Ok(url) => Ok(url),
            Err(e) => {
                let err_msg = format!("Unable to parse URL: {}", e);
                match connected_to_journal() {
                    true => error!("[ERROR] {err_msg}"),
                    false => eprintln!("[ERROR] {err_msg}"),
                }
                Err(e)
            }
        }
    }

    fn create_body(&self, r#type: &str, name: &str, ip: &str) -> HashMap<String, String> {
        let mut body = HashMap::new();
        body.insert("type".to_owned(), r#type.to_owned());
        body.insert("name".to_owned(), name.to_owned());
        body.insert("content".to_owned(), ip.to_owned());
        body
    }

    fn validate_response(&self, response: Response) -> Result<(), ()> {
        if response.status().is_success() {
            let data = match response.json::<CloudflareApiResult>() {
                Ok(data) => data,
                Err(e) => {
                    let err_msg = format!("Unable to parse API response: {e}");
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                    return Err(())
                }
            };

            match data.success {
                true => Ok(()),
                false => {
                    let err_msg = format!("Unexpected error while updating DNS record: {:?}", data);
                    match connected_to_journal() {
                        true => error!("[ERROR] {err_msg}"),
                        false => eprintln!("[ERROR] {err_msg}"),
                    }
                    Err(())
                }
            }
        } else {
            let err_msg = format!("Unable to post/put Cloudflare DNS entry: {}", response.status());
            match connected_to_journal() {
                true => error!("[ERROR] {err_msg}"),
                false => eprintln!("[ERROR] {err_msg}"),
            }
            Err(())
        }
    }
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Copy, Display, IntoStaticStr)]
#[allow(clippy::upper_case_acronyms)]
pub(crate) enum DnsRecordType {
    A,
    AAAA,
    CAA,
    CERT,
    CNAME,
    DNSKEY,
    DS,
    HTTPS,
    LOC,
    MX,
    NAPTR,
    NS,
    PTR,
    SMIMEA,
    SRV,
    SSHFP,
    SVCB,
    TLSA,
    TXT,
    URI
}

#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub(crate) struct CloudflareDnsRecord {
    pub(crate) id: String,
    pub(crate) name: String,
    pub(crate) r#type: DnsRecordType,
    pub(crate) content: String
}