use clap::{Arg, Command, ValueHint};
use difluoroborane::{config, Difluoroborane};

use std::str::FromStr;
use std::{env, io, io::Write, path::PathBuf};

use nix::NixPath;

fn main() -> miette::Result<()> {
    // Argument parsing
    // values for the name, description and version are pulled from `Cargo.toml`.
    let matches = Command::new(clap::crate_name!())
        .version(clap::crate_version!())
        .long_version(&*format!("{version}\n\
            FabAccess {apiver}\n\
            \t[{build_kind} build built on {build_time}]\n\
            \t  {rustc_version}\n\t  {cargo_version}",
            version=difluoroborane::env::PKG_VERSION,
            apiver="0.3",
            rustc_version=difluoroborane::env::RUST_VERSION,
            cargo_version=difluoroborane::env::CARGO_VERSION,
            build_time=difluoroborane::env::BUILD_TIME_3339,
            build_kind=difluoroborane::env::BUILD_RUST_CHANNEL))
        .about(clap::crate_description!())
        .arg(Arg::new("config")
                .help("Path to the DHALL config file to use")
                .long("config")
                .short('c')
                .takes_value(true))
        .arg(Arg::new("verbosity")
            .help("Increase logging verbosity. Stackable from -v up to -vvv")
            .long("verbose")
            .short('v')
            .multiple_occurrences(true)
            .max_occurrences(3)
            .conflicts_with("quiet"))
        .arg(Arg::new("quiet")
            .help("Decrease logging verbosity")
            .long("quiet")
            .conflicts_with("verbosity"))
        .arg(Arg::new("log format")
            .help("Use an alternative log formatter. Available: Full, Compact, Pretty")
            .long("log-format")
            .takes_value(true)
            .ignore_case(true)
            .possible_values(["Full", "Compact", "Pretty"]))
        .arg(Arg::new("log level")
            .help("Set the desired log levels.")
            .long("log-level")
            .takes_value(true)
            .possible_values(["info", "warn", "error", "debug", "trace"]))
        .arg(
            Arg::new("print default")
                .help("Print a default DHALL config to stdout instead of running")
                .long("print-default"))
        .arg(
            Arg::new("check config")
                .help("Check DHALL config for validity")
                .long("check"))
        .arg(
            Arg::new("dump-db")
                .help("Dump all internal databases (states and users) to the given file as TOML")
                .long("dump-db")
                .alias("dump")
                .conflicts_with("dump-users")
                .conflicts_with("load-users")
                .conflicts_with("load-db")
                .takes_value(true)
                .value_name("FILE")
                .value_hint(ValueHint::AnyPath)
                .default_missing_value("bffh-db.toml")
            )
        .arg(
            Arg::new("dump-users")
                .help("Dump the users db to the given file as TOML")
                .long("dump-users")
                .takes_value(true)
                .value_name("FILE")
                .value_hint(ValueHint::AnyPath)
                .default_missing_value("users.toml")
                .conflicts_with("load-users")
                .conflicts_with("load-db")
                .conflicts_with("dump-db")
                )
        .arg(
            Arg::new("force")
                .help("Force owerwriting existing files")
                .long("force")
        )
        .arg(
            Arg::new("load-users")
                .help("Load users from TOML into the internal databases")
                .long("load-users")
                .alias("load")
                .takes_value(true)
                .value_name("FILE")
                .conflicts_with("dump-db")
                .conflicts_with("load-db")
                .conflicts_with("dump-users")
                )
        .arg(
            Arg::new("load-db")
                .help("Load values from TOML into the internal databases")
                .long("load-db")
                .takes_value(true)
                .value_name("FILE")
                .conflicts_with("dump-db")
                .conflicts_with("load-users")
                .conflicts_with("dump-users"))
        .arg(Arg::new("keylog")
            .help("Log TLS keys into PATH. If no path is specified the value of the envvar SSLKEYLOGFILE is used.")
            .long("tls-key-log")
            .value_name("PATH")
            .takes_value(true)
            .max_values(1)
            .min_values(0)
            .default_missing_value(""))
        .try_get_matches();

    let matches = match matches {
        Ok(m) => m,
        Err(error) => error.exit(),
    };

    let configpath = matches.value_of("config").unwrap_or("/etc/bffh/bffh.dhall");

    // Check for the --print-default option first because we don't need to do anything else in that
    // case.
    if matches.is_present("print default") {
        let config = config::Config::default();
        let encoded = serde_dhall::serialize(&config).to_string().unwrap();

        // Direct writing to fd 1 is faster but also prevents any print-formatting that could
        // invalidate the generated DHALL
        let stdout = io::stdout();
        let mut handle = stdout.lock();
        handle.write_all(encoded.as_bytes()).unwrap();

        // Early return to exit.
        return Ok(());
    } else if matches.is_present("check config") {
        match config::read(&PathBuf::from_str(configpath).unwrap()) {
            Ok(c) => {
                let formatted = format!("{:#?}", c);

                // Direct writing to fd 1 is faster but also prevents any print-formatting that could
                // invalidate the generated TOML
                let stdout = io::stdout();
                let mut handle = stdout.lock();
                handle.write_all(formatted.as_bytes()).unwrap();

                // Early return to exit.
                return Ok(());
            }
            Err(e) => {
                eprintln!("{}", e);
                std::process::exit(-1);
            }
        }
    }

    let mut config = config::read(&PathBuf::from_str(configpath).unwrap())?;

    if matches.is_present("dump-db") {
        let mut bffh = Difluoroborane::new(config)?;
        let fname = matches.value_of("dump-db").unwrap();
        bffh.dump_db(fname)?;
        return Ok(());
    } else if matches.is_present("load-db") {
        let mut bffh = Difluoroborane::new(config)?;
        let fname = matches.value_of("load-db").unwrap();
        bffh.load_db(fname)?;
        return Ok(());
    } else if matches.is_present("dump-users") {
        let bffh = Difluoroborane::new(config)?;

        let number = bffh.users.dump_file(
            matches.value_of("dump-users").unwrap(),
            matches.is_present("force"),
        )?;

        tracing::info!("successfully dumped {} users", number);

        return Ok(());
    } else if matches.is_present("load-users") {
        let bffh = Difluoroborane::new(config)?;

        bffh.users
            .load_file(matches.value_of("load-users").unwrap())?;

        tracing::info!(
            "loaded users from {}",
            matches.value_of("load-users").unwrap()
        );

        return Ok(());
    } else {
        let keylog = matches.value_of("keylog");
        // When passed an empty string (i.e no value) take the value from the env
        let keylog = if let Some("") = keylog {
            let v = env::var_os("SSLKEYLOGFILE").map(PathBuf::from);
            if v.is_none() || v.as_ref().unwrap().is_empty() {
                eprintln!("--tls-key-log set but no path configured!");
                return Ok(());
            }
            v
        } else {
            keylog.map(PathBuf::from)
        };

        config.tlskeylog = keylog;
        config.verbosity = matches.occurrences_of("verbosity") as isize;
        if config.verbosity == 0 && matches.is_present("quiet") {
            config.verbosity = -1;
        }
        config.logging.format = matches.value_of("log format").unwrap_or("full").to_string();

        let mut bffh = Difluoroborane::new(config)?;
        bffh.run()?;
    }

    Ok(())
}