// tfclient, an open source client for the Defined Networking nebula management protocol.
//     Copyright (C) 2023 c0repwn3r
//
//     This program is free software: you can redistribute it and/or modify
//     it under the terms of the GNU General Public License as published by
//     the Free Software Foundation, either version 3 of the License, or
//     (at your option) any later version.
//
//     This program is distributed in the hope that it will be useful,
//     but WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//     GNU General Public License for more details.
//
//     You should have received a copy of the GNU General Public License
//     along with this program.  If not, see <https://www.gnu.org/licenses/>.

pub mod embedded_nebula;
pub mod dirs;
pub mod util;
pub mod nebulaworker;
pub mod daemon;
pub mod config;
pub mod service;
pub mod apiworker;
pub mod socketworker;
pub mod socketclient;
pub mod timerworker;

pub mod nebula_bin {
    include!(concat!(env!("OUT_DIR"), "/nebula.bin.rs"));
}
pub mod nebula_cert_bin {
    include!(concat!(env!("OUT_DIR"), "/nebula_cert.bin.rs"));
}


use std::fs;

use clap::{Parser, ArgAction, Subcommand};
use log::{error, info};
use simple_logger::SimpleLogger;
use crate::config::load_config;
use crate::dirs::get_data_dir;
use crate::embedded_nebula::{run_embedded_nebula, run_embedded_nebula_cert};
use crate::service::entry::{cli_install, cli_start, cli_stop, cli_uninstall};

#[derive(Parser)]
#[command(author = "c0repwn3r", version, about, long_about = None)]
#[clap(disable_version_flag = true)]
struct Cli {
    #[arg(short = 'v', long = "version", action = ArgAction::SetTrue)]
    #[clap(global = true)]
    /// Print the tfclient version, as well as the trifid-pki version and the version of the embedded nebula and nebula-cert binaries
    version: bool,

    #[command(subcommand)]
    subcommand: Commands
}

#[derive(Subcommand)]
enum Commands {
    /// Run the `nebula` binary. This is useful if you want to do debugging with tfclient's internal nebula.
    RunNebula {
        /// Arguments to pass to the `nebula` binary
        #[clap(trailing_var_arg=true, allow_hyphen_values=true)]
        args: Vec<String>
    },
    /// Run the `nebula-cert` binary. This is useful if you want to mess with certificates. Note: tfclient does not actually use nebula-cert for certificate operations, and instead uses trifid-pki internally
    RunNebulaCert {
        /// Arguments to pass to the `nebula-cert` binary
        #[clap(trailing_var_arg=true, allow_hyphen_values=true)]
        args: Vec<String>
    },
    /// Clear any cached data that tfclient may have added
    ClearCache {},

    /// Install the tfclient system service
    Install {
        #[clap(short, long, default_value = "tfclient")]
        /// Optional service name used to run multiple tfclient instances. Specify the same name on all other cli sub-commands (start, uninstall, etc.) to refer to this installed instance
        name: String,
        #[clap(short, long)]
        /// Server to use for API calls.
        server: String
    },

    /// Uninstall the tfclient system service
    Uninstall {
        #[clap(short, long, default_value = "tfclient")]
        /// Service name specified on install
        name: String
    },

    /// Start the tfclient system service
    Start {
        #[clap(short, long, default_value = "tfclient")]
        /// Service name specified on install
        name: String
    },

    /// Stop the tfclient system service
    Stop {
        #[clap(short, long, default_value = "tfclient")]
        /// Service name specified on start
        name: String
    },

    /// Run the tfclient daemon in the foreground
    Run {
        #[clap(short, long, default_value = "tfclient")]
        /// Service name specified on install
        name: String,
        #[clap(short, long)]
        /// Server to use for API calls.
        server: String
    },

    /// Enroll this host using a trifid-api enrollment code
    Enroll {
        #[clap(short, long, default_value = "tfclient")]
        /// Service name specified on install
        name: String,
        #[clap(short, long)]
        /// Enrollment code used to enroll this node
        code: String,
    },

    /// Manually trigger a config update. It's useful to use this in combination with the `disable_automatic_config_updates` option.
    Update {
        #[clap(short, long, default_value = "tfclient")]
        /// Service name specified on install
        name: String,
    }
}

fn main() {
    SimpleLogger::new().init().unwrap();

    let args = Cli::parse();

    if args.version {
        print_version();
    }

    match args.subcommand {
        Commands::RunNebula { args } => {
            match run_embedded_nebula(&args) {
                Ok(mut c) => {
                    match c.wait() {
                        Ok(stat) => {
                            match stat.code() {
                                Some(code) => {
                                    if code != 0 {
                                        error!("Nebula process exited with nonzero status code {}", code);
                                    }
                                    std::process::exit(code);
                                },
                                None => {
                                    info!("Nebula process terminated by signal");
                                    std::process::exit(0);
                                }
                            }
                        },
                        Err(e) => {
                            error!("Unable to wait for child to exit: {}", e);
                            std::process::exit(1);
                        }
                    }
                },
                Err(e) => {
                    error!("Unable to start nebula binary: {}", e);
                    std::process::exit(1);
                }
            }
        },
        Commands::ClearCache { .. } => {
            let data_dir = match get_data_dir() {
                Some(dir) => dir,
                None => {
                    error!("Unable to get platform-specific data dir");
                    std::process::exit(1);
                }
            };
            match fs::remove_dir_all(&data_dir) {
                Ok(_) => (),
                Err(e) => {
                    error!("Unable to delete data dir: {}", e);
                    std::process::exit(0);
                }
            }
            info!("Removed data dir {}", data_dir.as_path().display());

            info!("Removed all cached data.");
            std::process::exit(0);
        },
        Commands::RunNebulaCert { args } => {
            match run_embedded_nebula_cert(&args) {
                Ok(mut c) => {
                    match c.wait() {
                        Ok(stat) => {
                            match stat.code() {
                                Some(code) => {
                                    if code != 0 {
                                        error!("nebula-cert process exited with nonzero status code {}", code);
                                    }
                                    std::process::exit(code);
                                },
                                None => {
                                    info!("nebula-cert process terminated by signal");
                                    std::process::exit(0);
                                }
                            }
                        },
                        Err(e) => {
                            error!("Unable to wait for child to exit: {}", e);
                            std::process::exit(1);
                        }
                    }
                },
                Err(e) => {
                    error!("Unable to start nebula-cert binary: {}", e);
                    std::process::exit(1);
                }
            }
        }
        Commands::Install { server, name } => {
            cli_install(&name, &server);
        }
        Commands::Uninstall { name } => {
            cli_uninstall(&name);
        }
        Commands::Start { name } => {
            cli_start(&name);
        }
        Commands::Stop { name } => {
            cli_stop(&name);
        }
        Commands::Run { name, server } => {
            daemon::daemon_main(name, server);
        }
        Commands::Enroll { name, code } => {
            info!("Loading config...");
            let config = match load_config(&name) {
                Ok(cfg) => cfg,
                Err(e) => {
                    error!("Error loading configuration: {}", e);
                    std::process::exit(1);
                }
            };
            match socketclient::enroll(&code, &config) {
                Ok(_) => (),
                Err(e) => {
                    error!("Error sending enrollment request: {}", e);
                    std::process::exit(1);
                }
            };
        },
        Commands::Update { name } => {
            info!("Loading config...");
            let config = match load_config(&name) {
                Ok(cfg) => cfg,
                Err(e) => {
                    error!("Error loading configuration: {}", e);
                    std::process::exit(1);
                }
            };
            match socketclient::update(&config) {
                Ok(_) => (),
                Err(e) => {
                    error!("Error sending update request: {}", e);
                    std::process::exit(1);
                }
            };
        }
    }
}

fn print_version() {
    println!("tfclient v{} linked to trifid-pki v{}, embedding nebula v{} and nebula-cert v{}", env!("CARGO_PKG_VERSION"), trifid_pki::TRIFID_PKI_VERSION, crate::nebula_bin::NEBULA_VERSION, crate::nebula_cert_bin::NEBULA_CERT_VERSION);
}