use crate::auth::AuthConfig;
use crate::errors::*;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

pub const IDLE_DELAY: u64 = 180;
pub const PING_DEADLINE: i64 = PING_INTERVAL as i64 * 9;
pub const PING_INTERVAL: u64 = 60;
pub const WORKER_DELAY: u64 = 3;
pub const API_ERROR_DELAY: u64 = 30;

pub const DEFAULT_RETRY_DELAY_BASE: i64 = 24;

pub fn load<P: AsRef<Path>>(path: Option<P>) -> Result<ConfigFile> {
    let mut config = ConfigFile::default();

    if let Some(c) = load_from("/etc/rebuilderd.conf")? {
        config.update(c);
    }

    if let Ok(path) = config_path() {
        if let Some(c) = load_from(path)? {
            config.update(c);
        }
    }

    if let Some(path) = path {
        let c = load_from(path)?.ok_or_else(|| format_err!("Failed to read config file"))?;
        config.update(c);
    }

    Ok(config)
}

fn config_path() -> Result<PathBuf> {
    let config_dir =
        dirs_next::config_dir().ok_or_else(|| format_err!("Failed to find config dir"))?;
    Ok(config_dir.join("rebuilderd.conf"))
}

fn load_from<P: AsRef<Path>>(path: P) -> Result<Option<ConfigFile>> {
    if let Ok(buf) = fs::read_to_string(path.as_ref()) {
        debug!("loading config file {:?}", path.as_ref());
        let config = toml::from_str(&buf).context("Failed to load config")?;
        Ok(Some(config))
    } else {
        Ok(None)
    }
}

#[derive(Debug, Default, Clone, Deserialize)]
pub struct ConfigFile {
    #[serde(default)]
    pub http: HttpConfig,
    #[serde(default)]
    pub auth: AuthConfig,
    #[serde(default)]
    pub endpoints: HashMap<String, EndpointConfig>,
    #[serde(default)]
    pub worker: WorkerConfig,
    #[serde(default)]
    pub schedule: ScheduleConfig,
}

impl ConfigFile {
    pub fn update(&mut self, c: ConfigFile) {
        self.http.update(c.http);
        self.auth.update(c.auth);
        for (k, v) in c.endpoints {
            if let Some(o) = self.endpoints.get_mut(&k) {
                o.update(v);
            } else {
                self.endpoints.insert(k, v);
            }
        }
        self.worker.update(c.worker);
        self.schedule.update(c.schedule);
    }
}

#[derive(Debug, Default, Clone, Deserialize)]
pub struct HttpConfig {
    pub bind_addr: Option<String>,
    pub real_ip_header: Option<String>,
    pub post_body_size_limit: Option<usize>,
    pub transparently_sign_attestations: Option<bool>,
    pub endpoint: Option<String>,
}

impl HttpConfig {
    pub fn update(&mut self, c: HttpConfig) {
        if c.bind_addr.is_some() {
            self.bind_addr = c.bind_addr;
        }
        if c.real_ip_header.is_some() {
            self.real_ip_header = c.real_ip_header;
        }
        if c.endpoint.is_some() {
            self.endpoint = c.endpoint;
        }
    }
}

#[derive(Debug, Default, Clone, Deserialize)]
pub struct EndpointConfig {
    pub cookie: String,
}

impl EndpointConfig {
    pub fn update(&mut self, c: EndpointConfig) {
        self.cookie = c.cookie;
    }
}

#[derive(Debug, Default, Clone, Deserialize)]
pub struct WorkerConfig {
    #[serde(default)]
    pub authorized_workers: Vec<String>,
    pub signup_secret: Option<String>,
}

impl WorkerConfig {
    pub fn update(&mut self, c: WorkerConfig) {
        if !c.authorized_workers.is_empty() {
            self.authorized_workers = c.authorized_workers;
        }
        if c.signup_secret.is_some() {
            self.signup_secret = c.signup_secret;
        }
    }
}

#[derive(Debug, Default, Clone, Deserialize)]
pub struct ScheduleConfig {
    retry_delay_base: Option<i64>,
}

impl ScheduleConfig {
    pub fn update(&mut self, c: ScheduleConfig) {
        if c.retry_delay_base.is_some() {
            self.retry_delay_base = c.retry_delay_base;
        }
    }

    pub fn retry_delay_base(&self) -> i64 {
        self.retry_delay_base.unwrap_or(DEFAULT_RETRY_DELAY_BASE)
    }
}
