use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::net::{Ipv4Addr, SocketAddrV4};
use ipnet::{IpNet, Ipv4Net};


use log::{debug, info};
use serde::{Deserialize, Serialize};
use dnapi_rs::client_blocking::EnrollMeta;
use dnapi_rs::credentials::Credentials;

use crate::dirs::{get_cdata_dir, get_cdata_file, get_config_dir, get_config_file};

pub const DEFAULT_PORT: u16 = 8157;
fn default_port() -> u16 { DEFAULT_PORT }

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TFClientConfig {
    #[serde(default = "default_port")]
    pub listen_port: u16,
    #[serde(default = "bool_false")]
    pub disable_automatic_config_updates: bool
}

#[derive(Serialize, Deserialize, Clone)]
pub struct TFClientData {
    pub dh_privkey: Option<Vec<u8>>,
    pub creds: Option<Credentials>,
    pub meta: Option<EnrollMeta>
}

pub fn create_config(instance: &str) -> Result<(), Box<dyn Error>> {
    info!("Creating config directory...");
    fs::create_dir_all(get_config_dir(instance).ok_or("Unable to load config dir")?)?;
    info!("Copying default config file to config directory...");
    let config = TFClientConfig {
        listen_port: DEFAULT_PORT,
        disable_automatic_config_updates: false,
    };
    let config_str = toml::to_string(&config)?;
    fs::write(get_config_file(instance).ok_or("Unable to load config dir")?, config_str)?;
    Ok(())
}

pub fn load_config(instance: &str) -> Result<TFClientConfig, Box<dyn Error>> {
    info!("Loading config...");
    let config_file = get_config_file(instance).ok_or("Unable to load config dir")?;

    if !config_file.exists() {
        create_config(instance)?;
    }

    debug!("opening {}", config_file.as_path().display());
    let config_str = fs::read_to_string(config_file)?;
    debug!("parsing config file");
    let config: TFClientConfig = toml::from_str(&config_str)?;
    info!("Loaded config successfully");
    Ok(config)
}

pub fn create_cdata(instance: &str) -> Result<(), Box<dyn Error>> {
    info!("Creating data directory...");
    fs::create_dir_all(get_cdata_dir(instance).ok_or("Unable to load data dir")?)?;
    info!("Copying default data file to config directory...");
    let config = TFClientData { dh_privkey: None, creds: None, meta: None };
    let config_str = toml::to_string(&config)?;
    fs::write(get_cdata_file(instance).ok_or("Unable to load data dir")?, config_str)?;
    Ok(())
}

pub fn load_cdata(instance: &str) -> Result<TFClientData, Box<dyn Error>> {
    info!("Loading cdata...");
    let config_file = get_cdata_file(instance).ok_or("Unable to load cdata dir")?;

    if !config_file.exists() {
        create_cdata(instance)?;
    }

    debug!("opening {}", config_file.as_path().display());
    let config_str = fs::read_to_string(config_file)?;
    debug!("parsing cdata file");
    let config: TFClientData = toml::from_str(&config_str)?;
    info!("Loaded cdata successfully");
    Ok(config)
}

pub fn save_cdata(instance: &str, data: TFClientData) -> Result<(), Box<dyn Error>> {
    info!("Saving cdata...");
    let config_file = get_cdata_file(instance).ok_or("Unable to load cdata dir")?;

    if !config_file.exists() {
        create_cdata(instance)?;
    }

    debug!("serializing cdata file");
    let config: String = toml::to_string(&data)?;
    debug!("writing to {}", config_file.as_path().display());
    fs::write(config_file, config)?;
    info!("Saved cdata successfully");
    Ok(())
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfig {
    pub pki: NebulaConfigPki,
    #[serde(default = "empty_hashmap")]
    #[serde(skip_serializing_if = "is_empty_hashmap")]
    pub static_host_map: HashMap<Ipv4Addr, Vec<SocketAddrV4>>,
    #[serde(skip_serializing_if = "is_none")]
    pub lighthouse: Option<NebulaConfigLighthouse>,
    #[serde(skip_serializing_if = "is_none")]
    pub listen: Option<NebulaConfigListen>,
    #[serde(skip_serializing_if = "is_none")]
    pub punchy: Option<NebulaConfigPunchy>,
    #[serde(default = "cipher_aes")]
    #[serde(skip_serializing_if = "is_cipher_aes")]
    pub cipher: NebulaConfigCipher,
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub preferred_ranges: Vec<IpNet>,
    #[serde(skip_serializing_if = "is_none")]
    pub relay: Option<NebulaConfigRelay>,
    #[serde(skip_serializing_if = "is_none")]
    pub tun: Option<NebulaConfigTun>,
    #[serde(skip_serializing_if = "is_none")]
    pub logging: Option<NebulaConfigLogging>,
    #[serde(skip_serializing_if = "is_none")]
    pub sshd: Option<NebulaConfigSshd>,

    #[serde(skip_serializing_if = "is_none")]
    pub firewall: Option<NebulaConfigFirewall>,

    #[serde(default = "u64_1")]
    #[serde(skip_serializing_if = "is_u64_1")]
    pub routines: u64,

    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub stats: Option<NebulaConfigStats>,

    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub local_range: Option<Ipv4Net>
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigPki {
    pub ca: String,
    pub cert: String,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub key: Option<String>,
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub blocklist: Vec<String>,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub disconnect_invalid: bool
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigLighthouse {
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub am_lighthouse: bool,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub serve_dns: bool,
    #[serde(skip_serializing_if = "is_none")]
    pub dns: Option<NebulaConfigLighthouseDns>,
    #[serde(default = "u32_10")]
    #[serde(skip_serializing_if = "is_u32_10")]
    pub interval: u32,
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub hosts: Vec<Ipv4Addr>,
    #[serde(default = "empty_hashmap")]
    #[serde(skip_serializing_if = "is_empty_hashmap")]
    pub remote_allow_list: HashMap<Ipv4Net, bool>,
    #[serde(default = "empty_hashmap")]
    #[serde(skip_serializing_if = "is_empty_hashmap")]
    pub local_allow_list: HashMap<Ipv4Net, bool>, // `interfaces` is not supported
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigLighthouseDns {
    #[serde(default = "string_empty")]
    #[serde(skip_serializing_if = "is_string_empty")]
    pub host: String,
    #[serde(default = "u16_53")]
    #[serde(skip_serializing_if = "is_u16_53")]
    pub port: u16
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigListen {
    #[serde(default = "string_empty")]
    #[serde(skip_serializing_if = "is_string_empty")]
    pub host: String,
    #[serde(default = "u16_0")]
    #[serde(skip_serializing_if = "is_u16_0")]
    pub port: u16,
    #[serde(default = "u32_64")]
    #[serde(skip_serializing_if = "is_u32_64")]
    pub batch: u32,
    #[serde(skip_serializing_if = "is_none")]
    pub read_buffer: Option<u32>,
    #[serde(skip_serializing_if = "is_none")]
    pub write_buffer: Option<u32>
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigPunchy {
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub punch: bool,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub respond: bool,
    #[serde(default = "string_1s")]
    #[serde(skip_serializing_if = "is_string_1s")]
    pub delay: String
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigCipher {
    #[serde(rename = "aes")]
    Aes,
    #[serde(rename = "chachapoly")]
    ChaChaPoly
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigRelay {
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub relays: Vec<Ipv4Addr>,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub am_relay: bool,
    #[serde(default = "bool_true")]
    #[serde(skip_serializing_if = "is_bool_true")]
    pub use_relays: bool
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigTun {
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub disabled: bool,
    #[serde(skip_serializing_if = "is_none")]
    pub dev: Option<String>,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub drop_local_broadcast: bool,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub drop_multicast: bool,
    #[serde(default = "u64_500")]
    #[serde(skip_serializing_if = "is_u64_500")]
    pub tx_queue: u64,
    #[serde(default = "u64_1300")]
    #[serde(skip_serializing_if = "is_u64_1300")]
    pub mtu: u64,
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub routes: Vec<NebulaConfigTunRouteOverride>,
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub unsafe_routes: Vec<NebulaConfigTunUnsafeRoute>
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigTunRouteOverride {
    pub mtu: u64,
    pub route: Ipv4Net
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigTunUnsafeRoute {
    pub route: Ipv4Net,
    pub via: Ipv4Addr,
    #[serde(default = "u64_1300")]
    #[serde(skip_serializing_if = "is_u64_1300")]
    pub mtu: u64,
    #[serde(default = "i64_100")]
    #[serde(skip_serializing_if = "is_i64_100")]
    pub metric: i64
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigLogging {
    #[serde(default = "loglevel_info")]
    #[serde(skip_serializing_if = "is_loglevel_info")]
    pub level: NebulaConfigLoggingLevel,
    #[serde(default = "format_text")]
    #[serde(skip_serializing_if = "is_format_text")]
    pub format: NebulaConfigLoggingFormat,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub disable_timestamp: bool,
    #[serde(default = "timestamp")]
    #[serde(skip_serializing_if = "is_timestamp")]
    pub timestamp_format: String
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigLoggingLevel {
    #[serde(rename = "panic")]
    Panic,
    #[serde(rename = "fatal")]
    Fatal,
    #[serde(rename = "error")]
    Error,
    #[serde(rename = "warning")]
    Warning,
    #[serde(rename = "info")]
    Info,
    #[serde(rename = "debug")]
    Debug
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigLoggingFormat {
    #[serde(rename = "json")]
    Json,
    #[serde(rename = "text")]
    Text
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigSshd {
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub enabled: bool,
    pub listen: SocketAddrV4,
    pub host_key: String,
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub authorized_users: Vec<NebulaConfigSshdAuthorizedUser>
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigSshdAuthorizedUser {
    pub user: String,
    #[serde(default = "empty_vec")]
    #[serde(skip_serializing_if = "is_empty_vec")]
    pub keys: Vec<String>
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type")]
pub enum NebulaConfigStats {
    #[serde(rename = "graphite")]
    Graphite(NebulaConfigStatsGraphite),
    #[serde(rename = "prometheus")]
    Prometheus(NebulaConfigStatsPrometheus)
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigStatsGraphite {
    #[serde(default = "string_nebula")]
    #[serde(skip_serializing_if = "is_string_nebula")]
    pub prefix: String,
    #[serde(default = "protocol_tcp")]
    #[serde(skip_serializing_if = "is_protocol_tcp")]
    pub protocol: NebulaConfigStatsGraphiteProtocol,
    pub host: SocketAddrV4,
    pub interval: String,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub message_metrics: bool,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub lighthouse_metrics: bool
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigStatsGraphiteProtocol {
    #[serde(rename = "tcp")]
    Tcp,
    #[serde(rename = "udp")]
    Udp
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigStatsPrometheus {
    pub listen: String,
    pub path: String,
    #[serde(default = "string_nebula")]
    #[serde(skip_serializing_if = "is_string_nebula")]
    pub namespace: String,
    #[serde(default = "string_nebula")]
    #[serde(skip_serializing_if = "is_string_nebula")]
    pub subsystem: String,
    pub interval: String,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub message_metrics: bool,
    #[serde(default = "bool_false")]
    #[serde(skip_serializing_if = "is_bool_false")]
    pub lighthouse_metrics: bool
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigFirewall {
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub conntrack: Option<NebulaConfigFirewallConntrack>,

    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub inbound: Option<Vec<NebulaConfigFirewallRule>>,

    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub outbound: Option<Vec<NebulaConfigFirewallRule>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigFirewallConntrack {
    #[serde(default = "string_12m")]
    #[serde(skip_serializing_if = "is_string_12m")]
    pub tcp_timeout: String,
    #[serde(default = "string_3m")]
    #[serde(skip_serializing_if = "is_string_3m")]
    pub udp_timeout: String,
    #[serde(default = "string_10m")]
    #[serde(skip_serializing_if = "is_string_10m")]
    pub default_timeout: String
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigFirewallRule {
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub port: Option<String>,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub proto: Option<String>,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub ca_name: Option<String>,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub ca_sha: Option<String>,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub host: Option<String>,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub group: Option<String>,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub groups: Option<Vec<String>>,
    #[serde(default = "none")]
    #[serde(skip_serializing_if = "is_none")]
    pub cidr: Option<String>
}

// Default values for serde

fn string_12m() -> String { "12m".to_string() }
fn is_string_12m(s: &str) -> bool { s == "12m" }

fn string_3m() -> String { "3m".to_string() }
fn is_string_3m(s: &str) -> bool { s == "3m" }

fn string_10m() -> String { "10m".to_string() }
fn is_string_10m(s: &str) -> bool { s == "10m" }

fn empty_vec<T>() -> Vec<T> { vec![] }
fn is_empty_vec<T>(v: &Vec<T>) -> bool { v.is_empty() }

fn empty_hashmap<A, B>() -> HashMap<A, B> { HashMap::new() }
fn is_empty_hashmap<A, B>(h: &HashMap<A, B>) -> bool { h.is_empty() }

fn bool_false() -> bool { false }
fn is_bool_false(b: &bool) -> bool { !*b }

fn bool_true() -> bool { true }
fn is_bool_true(b: &bool) -> bool { *b }

fn u16_53() -> u16 { 53 }
fn is_u16_53(u: &u16) -> bool { *u == 53 }

fn u32_10() -> u32 { 10 }
fn is_u32_10(u: &u32) -> bool { *u == 10 }

fn ipv4_0000() -> Ipv4Addr { Ipv4Addr::new(0, 0, 0, 0) }
fn is_ipv4_0000(i: &Ipv4Addr) -> bool { *i == ipv4_0000() }

fn u16_0() -> u16 { 0 }
fn is_u16_0(u: &u16) -> bool { *u == 0 }

fn u32_64() -> u32 { 64 }
fn is_u32_64(u: &u32) -> bool { *u == 64 }

fn string_1s() -> String { "1s".to_string() }
fn is_string_1s(s: &str) -> bool { s == "1s" }

fn cipher_aes() -> NebulaConfigCipher { NebulaConfigCipher::Aes }
fn is_cipher_aes(c: &NebulaConfigCipher) -> bool { matches!(c, NebulaConfigCipher::Aes) }

fn u64_500() -> u64 { 500 }
fn is_u64_500(u: &u64) -> bool { *u == 500 }

fn u64_1300() -> u64 { 1300 }
fn is_u64_1300(u: &u64) -> bool { *u == 1300 }

fn i64_100() -> i64 { 100 }
fn is_i64_100(i: &i64) -> bool { *i == 100 }

fn loglevel_info() -> NebulaConfigLoggingLevel { NebulaConfigLoggingLevel::Info }
fn is_loglevel_info(l: &NebulaConfigLoggingLevel) -> bool { matches!(l, NebulaConfigLoggingLevel::Info) }

fn format_text() -> NebulaConfigLoggingFormat { NebulaConfigLoggingFormat::Text }
fn is_format_text(f: &NebulaConfigLoggingFormat) -> bool { matches!(f, NebulaConfigLoggingFormat::Text) }

fn timestamp() -> String { "2006-01-02T15:04:05Z07:00".to_string() }
fn is_timestamp(s: &str) -> bool { s == "2006-01-02T15:04:05Z07:00" }

fn u64_1() -> u64 { 1 }
fn is_u64_1(u: &u64) -> bool { *u == 1 }

fn string_nebula() -> String { "nebula".to_string() }
fn is_string_nebula(s: &str) -> bool { s == "nebula" }

fn string_empty() -> String { String::new() }
fn is_string_empty(s: &str) -> bool { s == "" }

fn protocol_tcp() -> NebulaConfigStatsGraphiteProtocol { NebulaConfigStatsGraphiteProtocol::Tcp }
fn is_protocol_tcp(p: &NebulaConfigStatsGraphiteProtocol) -> bool { matches!(p, NebulaConfigStatsGraphiteProtocol::Tcp) }

fn none<T>() -> Option<T> { None }
fn is_none<T>(o: &Option<T>) -> bool { o.is_none() }