use std::path::PathBuf;

use clap::{command, value_parser, ArgAction, Command, Parser};
use libduralumin::key_gen::{HashDesc, KeygenDesc};
use zeroize::Zeroizing;

fn args() -> clap::ArgMatches {
    let sha256 = Command::new("sha256")
        .about("Use sha256 as the hash function")
        .arg(
            clap::arg!(-i --iterations <ITERATIONS> "Number of iterations")
                .required(true)
                .value_parser(value_parser!(u32))
                .default_value("4"),
        );
    let argon2 = Command::new("argon2")
        .about("Use argon2 as the hash function")
        .arg_required_else_help(true)
        .arg(
            clap::arg!(-v --variant <VARIANT> "Variant of argon2 to use")
                .required(false)
                .value_parser(["argon2d", "argon2i", "argon2id"])
                .default_value("argon2id"),
        )
        .arg(
            clap::arg!(-m --memory <MEMORY> "Memory cost in mibibytes (MiB)")
                .required(false)
                .value_parser(value_parser!(u32))
                .default_value("64"),
        )
        .arg(
            clap::arg!(-t --time <TIME> "Time cost (number of iterations)")
                .required(false)
                .value_parser(value_parser!(u32))
                .default_value("4"),
        )
        .arg(
            clap::arg!(-p --parallelism <PARALLELISM> "Degree of parallelism (lanes)")
                .required(false)
                .value_parser(value_parser!(u32))
                .default_value("1"),
        );

    let declare = Command::new("declare")
        .about("Declare key definition via the command line instead of interactively")
        .arg(
            clap::arg!(-t --tag <TAG> "Tag to identify the keypair")
                .required(false)
                .value_parser(value_parser!(String)),
        )
        .arg(
            clap::arg!(--encrypt "Whether to encrypt the private key on disk")
                .required(false)
                .action(ArgAction::SetTrue),
        )
        .arg(
            clap::arg!(--passphrase <PASSPHRASE> "Passphrase to seed the keypair generation")
                .required(true)
                .value_parser(value_parser!(String)),
        )
        .subcommands([sha256, argon2]);

    let interactive = Command::new("interactive").about("Interactively prompt for key definition");

    let matches = command!()
        .about("Duralumin SSH Keypair Generator")
        .long_about("A tool to deterministically generate ed25519 SSH keypairs with secure passphrase-based encryption and hashing options.")
        .author("Janis <janis@nirgendwo.xyz>")
        .arg(
            clap::arg!(-f --file <FILE> "Path to save the keypair to.")
                .required(false)
                .value_parser(value_parser!(PathBuf)),
        )
        .subcommands([declare, interactive])
        .get_matches();

    matches
}

fn desc_from_args(args: &clap::ArgMatches) -> KeygenDesc {
    match args.subcommand() {
        Some(("declare", matches)) => {
            let tag = matches
                .get_one::<String>("tag")
                .cloned()
                .map(Zeroizing::new);
            let passphrase = matches
                .get_one::<String>("passphrase")
                .cloned()
                .map(Zeroizing::new)
                .expect("Passphrase is required");
            let encrypt = matches.get_flag("encrypt");

            match matches.subcommand() {
                Some(("sha256", matches)) => {
                    let iterations = matches.get_one::<u32>("iterations").copied().unwrap_or(4);
                    KeygenDesc {
                        hash: libduralumin::key_gen::HashDesc::Sha256 { iterations },
                        salt: KeygenDesc::SALT.to_vec(),
                        tag,
                        passphrase,
                        encrypt,
                    }
                }
                Some(("argon2", matches)) => {
                    let variant = matches
                        .get_one::<String>("variant")
                        .map(String::as_str)
                        .unwrap_or("argon2id");
                    let memory = matches.get_one::<u32>("memory").copied().unwrap_or(64) * 1024;
                    let time = matches.get_one::<u32>("time").copied().unwrap_or(4);
                    let lanes = matches.get_one::<u32>("parallelism").copied().unwrap_or(1);
                    let variant = match variant {
                        "argon2d" => argon2::Algorithm::Argon2d,
                        "argon2i" => argon2::Algorithm::Argon2i,
                        "argon2id" => argon2::Algorithm::Argon2id,
                        _ => unreachable!("clap should ensure this"),
                    };
                    let hash = HashDesc::Argon {
                        variant,
                        memory,
                        time,
                        lanes,
                        version: argon2::Version::V0x13,
                        hash_length: 32,
                    };

                    KeygenDesc {
                        hash,
                        salt: KeygenDesc::SALT.to_vec(),
                        tag,
                        passphrase,
                        encrypt,
                    }
                }
                _ => panic!("A subcommand is required"),
            }
        }
        Some(("interactive", _)) => libduralumin::key_gen::cli::keygen_desc_from_stdin()
            .expect("Failed to read keygen description from stdin"),
        _ => panic!("A subcommand is required"),
    }
}

fn main() -> anyhow::Result<()> {
    let args = args();
    println!("Generating ed25519 ssh keypair:");

    let desc = desc_from_args(&args);

    let file = args.get_one::<PathBuf>("file").cloned();
    let base_path = file.unwrap_or_else(|| {
        PathBuf::from(if let Some(tag) = desc.tag.as_ref() {
            format!("duralumin_{}", tag.as_str())
        } else {
            "duralumin".to_owned()
        })
    });

    let keypair = libduralumin::key_gen::generate_key(desc)?;

    println!(
        "Your key fingerprint is: Sha256:{}",
        keypair.fingerprint_base64()
    );

    println!(
        "RandomArt:\n{}",
        keypair.randomart().render("ED25519 256", "SHA256")?
    );

    let private_path = base_path.clone();
    let public_path = base_path.with_extension("pub");

    let (private_key, public_key) = keypair.encode_keys()?;
    std::fs::write(&private_path, private_key)?;

    std::fs::write(&public_path, public_key)?;

    #[cfg(target_family = "unix")]
    {
        use std::fs::Permissions;
        use std::os::unix::fs::PermissionsExt;

        std::fs::set_permissions(private_path, Permissions::from_mode(0o0600))?;
        std::fs::set_permissions(public_path, Permissions::from_mode(0o0600))?;
    }

    Ok(())
}
