// Code to handle the nebula worker

use std::error::Error;
use std::fs;
use std::sync::mpsc::{Receiver, TryRecvError};
use std::time::{Duration, SystemTime};
use log::{debug, error, info};
use crate::config::{load_cdata, NebulaConfig, TFClientConfig};
use crate::daemon::ThreadMessageSender;
use crate::dirs::get_nebulaconfig_file;
use crate::embedded_nebula::run_embedded_nebula;

pub enum NebulaWorkerMessage {
    Shutdown,
    ConfigUpdated,
    WakeUp
}

fn insert_private_key(instance: &str) -> Result<(), Box<dyn Error>> {
    if !get_nebulaconfig_file(instance).ok_or("Could not get config file location")?.exists() {
        return Ok(()); // cant insert private key into a file that does not exist - BUT. we can gracefully handle nebula crashing - we cannot gracefully handle this fn failing
    }
    let cdata = load_cdata(instance)?;
    let key = cdata.dh_privkey.ok_or("Missing private key")?;

    let config_str = fs::read_to_string(get_nebulaconfig_file(instance).ok_or("Could not get config file location")?)?;
    let mut config: NebulaConfig = serde_yaml::from_str(&config_str)?;

    config.pki.key = Some(String::from_utf8(key)?);

    debug!("inserted private key into config: {:?}", config);

    let config_str = serde_yaml::to_string(&config)?;
    fs::write(get_nebulaconfig_file(instance).ok_or("Could not get config file location")?, config_str)?;

    Ok(())
}

pub fn nebulaworker_main(_config: TFClientConfig, instance: String, _transmitter: ThreadMessageSender, rx: Receiver<NebulaWorkerMessage>) {
    let _cdata = match load_cdata(&instance) {
        Ok(data) => data,
        Err(e) => {
            error!("unable to load cdata: {}", e);
            error!("nebula thread exiting with error");
            return;
        }
    };

    info!("fixing config...");
    match insert_private_key(&instance) {
        Ok(_) => {
            info!("config fixed (private-key embedded)");
        },
        Err(e) => {
            error!("unable to fix config: {}", e);
            error!("nebula thread exiting with error");
            return;
        }
    }
    info!("starting nebula child...");
    let mut child = match run_embedded_nebula(&["-config".to_string(), get_nebulaconfig_file(&instance).unwrap().to_str().unwrap().to_string()]) {
        Ok(c) => c,
        Err(e) => {
            error!("unable to start embedded nebula binary: {}", e);
            error!("nebula thread exiting with error");
            return;
        }
    };
    info!("nebula process started");

    let mut last_restart_time = SystemTime::now();

    // dont need to save it, because we do not, in any circumstance, write to it
    loop {
        if let Ok(e) = child.try_wait() {
            if e.is_some() && SystemTime::now() > last_restart_time + Duration::from_secs(5) {
                info!("nebula process has exited, restarting");
                child = match run_embedded_nebula(&["-config".to_string(), get_nebulaconfig_file(&instance).unwrap().to_str().unwrap().to_string()]) {
                    Ok(c) => c,
                    Err(e) => {
                        error!("unable to start embedded nebula binary: {}", e);
                        error!("nebula thread exiting with error");
                        return;
                    }
                };
                info!("nebula process started");
                last_restart_time = SystemTime::now();
            }
        }
        match rx.recv() {
            Ok(msg) => {
                match msg {
                    NebulaWorkerMessage::WakeUp => {
                        continue;
                    },
                    NebulaWorkerMessage::Shutdown => {
                        info!("recv on command socket: shutdown, stopping");
                        info!("shutting down nebula binary");
                        match child.kill() {
                            Ok(_) => {
                                debug!("nebula process exited");
                            },
                            Err(e) => {
                                error!("nebula process already exited: {}", e);
                            }
                        }
                        info!("nebula shut down");
                        break;
                    },
                    NebulaWorkerMessage::ConfigUpdated => {
                        info!("our configuration has been updated - restarting");
                        debug!("killing existing process");
                        match child.kill() {
                            Ok(_) => {
                                debug!("nebula process exited");
                            },
                            Err(e) => {
                                error!("nebula process already exited: {}", e);
                            }
                        }
                        debug!("fixing config...");
                        match insert_private_key(&instance) {
                            Ok(_) => {
                                debug!("config fixed (private-key embedded)");
                            },
                            Err(e) => {
                                error!("unable to fix config: {}", e);
                                error!("nebula thread exiting with error");
                                return;
                            }
                        }
                        debug!("restarting nebula process");
                        child = match run_embedded_nebula(&["-config".to_string(), get_nebulaconfig_file(&instance).unwrap().to_str().unwrap().to_string()]) {
                            Ok(c) => c,
                            Err(e) => {
                                error!("unable to start embedded nebula binary: {}", e);
                                error!("nebula thread exiting with error");
                                return;
                            }
                        };
                        last_restart_time = SystemTime::now();
                        debug!("nebula process restarted");
                    }
                }
            },
            Err(e) => {
                error!("nebulaworker command socket errored: {}", e);
                return;
            }
        }
    }
}