// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
// SPDX-FileCopyrightText: 2023 David Runge <dave@sleepmap.de>
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::path::PathBuf;

use anyhow::{Result, anyhow};
use clap::{Parser, ValueEnum};
use openpgp_card::{
    Card,
    ocard::{KeyType, algorithm::AlgoSimple, crypto::CardUploadableKey},
    state::{Admin, Transaction},
};
use openpgp_card_rpgp::{UploadableKey, public_key_material_to_key};
use pgp::{
    packet::PublicKey,
    types::{Fingerprint, KeyDetails, KeyVersion, Timestamp},
};
use rpgpie::{
    certificate::{Certificate, Checked},
    tsk::Tsk,
};
use secrecy::SecretString;

use crate::{
    ENTER_ADMIN_PIN,
    ENTER_USER_PIN,
    output,
    util,
    versioned_output::{OutputBuilder, OutputFormat, OutputVersion},
};

#[derive(Parser, Debug)]
pub struct AdminCommand {
    #[arg(
        name = "card ident",
        short = 'c',
        long = "card",
        help = "Identifier of the card to use"
    )]
    pub ident: String,

    #[arg(
        name = "Admin PIN file",
        short = 'P',
        long = "admin-pin",
        help = "Optionally, get Admin PIN from a file"
    )]
    pub admin_pin: Option<PathBuf>,

    #[command(subcommand)]
    pub cmd: AdminSubCommand,
}

#[derive(Parser, Debug)]
pub enum AdminSubCommand {
    /// Set cardholder name
    Name {
        #[arg(help = "cardholder name to set on the card")]
        name: String,
    },

    /// Set certificate URL
    Url {
        #[arg(help = "URL that provides the certificate for the key material on this card")]
        url: String,
    },

    /// Configure if user PIN presentation is valid for just one signing operation, or for unlimited
    /// signing operations
    ///
    /// Configure the "only valid for one PSO:CDS" setting in PW Status Bytes.
    SigningPinValidity {
        #[arg(help = "Validity of user PIN presentation for signing operations")]
        state: PinValidity,
    },

    /// Import a Key onto the card.
    ///
    /// Most keys can be imported without specifying subkey fingerprints. However, if the key
    /// contains more than one signing, decryption or authentication capable subkey, subkeys must be
    /// explicitly selected.
    ///
    /// If any of the options is given, only the selected subkeys are imported into the selected
    /// slots.
    ///
    /// Subkey capabilities must match the slot the key is imported into. The DEC slot can
    /// only be used for encryption capable subkeys. The SIG and AUT slots can be used for signing,
    /// certification and authentication capable subkeys.
    Import {
        #[arg(help = "File that contains the PGP private key")]
        keyfile: PathBuf,

        /// Optionally, select the subkey to import in the SIG slot
        #[arg(name = "SIG subkey fingerprint", short = 's', long = "sig-fp")]
        sig_fp: Option<String>,

        /// Optionally, select the subkey to import in the DEC slot
        #[arg(name = "DEC subkey fingerprint", short = 'd', long = "dec-fp")]
        dec_fp: Option<String>,

        /// Optionally, select the subkey to import in the AUT slot
        #[arg(name = "AUT subkey fingerprint", short = 'a', long = "aut-fp")]
        aut_fp: Option<String>,

        /// Optionally, provide a passphrase for encrypted secret key material in a file
        #[arg(long = "key-passphrase")]
        key_passphrase: Vec<PathBuf>,
    },

    /// Generate a Key on the card.
    ///
    /// A signing key is always created, decryption and authentication keys
    /// are optional.
    Generate(AdminGenerateCommand),

    /// Set the card's touch policy (if supported)
    ///
    /// A touch policy defines if cryptographic operations on the card require user interaction
    /// with the card, for example by touching a button on the card.
    ///
    /// Only some cards support this feature at all, not all cards support all policies.
    ///
    /// Caution: Setting the ATT slot to Fixed or Cached-Fixed is permanent. Even a factory reset
    /// does not undo this setting.
    Touch {
        /// Key slot to set the touch policy for
        #[arg(name = "Key slot", short = 'k', long = "key", value_enum)]
        key: BasePlusAttKeySlot,

        /// Touch policy to set on this key slot
        #[arg(
            name = "Policy",
            short = 'p',
            long = "policy",
            value_enum,
            long_help = "Touch policy to set on this key slot

Off: No touch confirmation required.
On: Touch confirmation required for each operation.
Fixed: Like 'On', but the policy can only be changed by a reset.
Cached: Like 'On', but touch confirmation is valid for 15 seconds.
Cached-Fixed: Combines 'Cached' and 'Fixed'."
        )]
        policy: TouchPolicy,
    },
}

#[derive(Parser, Debug)]
pub struct AdminGenerateCommand {
    /// Output file
    #[arg(name = "output", long = "output", short = 'o')]
    output_file: PathBuf,

    /// Do not create a key in the DEC slot
    #[arg(long = "no-dec", action = clap::ArgAction::SetFalse)]
    decrypt: bool,

    /// Do not create a key in the AUT slot
    #[arg(long = "no-aut", action = clap::ArgAction::SetFalse)]
    auth: bool,

    #[arg(
        name = "algorithm",
        value_enum,
        help = "Choose the algorithm for the key material to generate on the card.",
        long_help = format!("Choose the algorithm for the key material to generate on the card.

    If the parameter is not given, use the algorithm currently set on the card.

    Specific cards support a set of algorithms that can differ between models. On modern cards,
    use '{} info' to see the list of supported algorithms.", env!("CARGO_BIN_NAME"))
    )]
    algo: Option<Algo>,

    /// User ID to add to the exported certificate representation
    #[arg(name = "User ID", short = 'u', long = "userid", required = true)]
    user_ids: Vec<String>,

    #[arg(
        name = "User PIN file",
        short = 'p',
        long = "user-pin",
        help = "Optionally, get User PIN from a file"
    )]
    user_pin: Option<PathBuf>,
}

#[derive(ValueEnum, Debug, Clone)]
pub enum PinValidity {
    Once,
    Unlimited,
}

impl From<PinValidity> for bool {
    fn from(state: PinValidity) -> Self {
        match state {
            PinValidity::Unlimited => false,
            PinValidity::Once => true,
        }
    }
}

#[derive(ValueEnum, Debug, Clone)]
#[value(rename_all = "UPPER")]
pub enum BasePlusAttKeySlot {
    Sig,
    Dec,
    Aut,
    Att,
}

impl From<BasePlusAttKeySlot> for KeyType {
    fn from(ks: BasePlusAttKeySlot) -> Self {
        match ks {
            BasePlusAttKeySlot::Sig => KeyType::Signing,
            BasePlusAttKeySlot::Dec => KeyType::Decryption,
            BasePlusAttKeySlot::Aut => KeyType::Authentication,
            BasePlusAttKeySlot::Att => KeyType::Attestation,
        }
    }
}

#[derive(ValueEnum, Debug, Clone)]
pub enum TouchPolicy {
    #[value(name = "Off")]
    Off,
    #[value(name = "On")]
    On,
    #[value(name = "Fixed")]
    Fixed,
    #[value(name = "Cached")]
    Cached,
    #[value(name = "Cached-Fixed")]
    CachedFixed,
}

impl From<TouchPolicy> for openpgp_card::ocard::data::TouchPolicy {
    fn from(tp: TouchPolicy) -> Self {
        use openpgp_card::ocard::data::TouchPolicy as OCTouchPolicy;
        match tp {
            TouchPolicy::On => OCTouchPolicy::On,
            TouchPolicy::Off => OCTouchPolicy::Off,
            TouchPolicy::Fixed => OCTouchPolicy::Fixed,
            TouchPolicy::Cached => OCTouchPolicy::Cached,
            TouchPolicy::CachedFixed => OCTouchPolicy::CachedFixed,
        }
    }
}

#[derive(ValueEnum, Debug, Clone)]
#[value(rename_all = "lower")]
pub enum Algo {
    Rsa2048,
    Rsa3072,
    Rsa4096,
    Nistp256,
    Nistp384,
    Nistp521,
    Curve25519,
}

impl From<Algo> for AlgoSimple {
    fn from(a: Algo) -> Self {
        match a {
            Algo::Rsa2048 => AlgoSimple::RSA2k,
            Algo::Rsa3072 => AlgoSimple::RSA3k,
            Algo::Rsa4096 => AlgoSimple::RSA4k,
            Algo::Nistp256 => AlgoSimple::NIST256,
            Algo::Nistp384 => AlgoSimple::NIST384,
            Algo::Nistp521 => AlgoSimple::NIST521,
            Algo::Curve25519 => AlgoSimple::Curve25519,
        }
    }
}

pub fn admin(
    output_format: OutputFormat,
    output_version: OutputVersion,
    command: AdminCommand,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut open = util::open_card(&command.ident)?;
    let mut card = open.transaction()?;

    let admin_pin = util::get_pin(&mut card, command.admin_pin, ENTER_ADMIN_PIN)?;

    match command.cmd {
        AdminSubCommand::Name { name } => {
            name_command(&name, card, admin_pin)?;
        }
        AdminSubCommand::Url { url } => {
            url_command(&url, card, admin_pin)?;
        }
        AdminSubCommand::SigningPinValidity { state } => {
            signing_pin_validity(state, card, admin_pin)?;
        }
        AdminSubCommand::Import {
            keyfile,
            sig_fp,
            dec_fp,
            aut_fp,
            key_passphrase,
        } => {
            import_command(
                keyfile,
                sig_fp,
                dec_fp,
                aut_fp,
                key_passphrase,
                card,
                admin_pin,
            )?;
        }
        AdminSubCommand::Generate(cmd) => {
            generate_command(output_format, output_version, card, admin_pin, cmd)?;
        }
        AdminSubCommand::Touch { key, policy } => {
            touch_command(card, admin_pin, key, policy)?;
        }
    }
    Ok(())
}

fn keys_pick_yolo(tsk: &Tsk) -> Result<[Option<UploadableKey>; 3]> {
    let cert = Certificate::from(tsk.clone());
    let ccert = Checked::from(cert);

    let now = Timestamp::now();

    let sig = ccert.valid_signing_capable_component_keys_at(now);
    let sig_fp = match sig.len() {
        0 => None,
        1 => Some(sig[0].as_componentkey().fingerprint()),
        _ => {
            return Err(anyhow::anyhow!(
                "More than one valid signing component key found"
            ));
        }
    };

    let dec = ccert.valid_encryption_capable_component_keys();
    let dec_fp = match dec.len() {
        0 => None,
        1 => Some(dec[0].fingerprint()),
        _ => {
            return Err(anyhow::anyhow!(
                "More than one valid encryption component key found"
            ));
        }
    };

    let aut = ccert.valid_authentication_capable_component_keys(now);
    let aut_fp = match aut.len() {
        0 => None,
        1 => Some(aut[0].fingerprint()),
        _ => {
            return Err(anyhow::anyhow!(
                "More than one valid authentication component key found"
            ));
        }
    };

    keys_pick_explicit(tsk, sig_fp, dec_fp, aut_fp)
}

fn keys_pick_explicit(
    tsk: &Tsk,
    sig_fp: Option<Fingerprint>,
    dec_fp: Option<Fingerprint>,
    aut_fp: Option<Fingerprint>,
) -> Result<[Option<UploadableKey>; 3]> {
    let key_by_fp = |fp: Option<Fingerprint>| match fp {
        Some(fp) => component_key_by_fingerprint(tsk, fp),
        None => Ok(None),
    };

    Ok([key_by_fp(sig_fp)?, key_by_fp(dec_fp)?, key_by_fp(aut_fp)?])
}

fn component_key_by_fingerprint(tsk: &Tsk, search: Fingerprint) -> Result<Option<UploadableKey>> {
    let ssk = tsk.key();
    let pri = &ssk.primary_key;
    if pri.fingerprint() == search {
        return Ok(Some(UploadableKey::from(pri.clone())));
    }

    for sk in &ssk.secret_subkeys {
        if sk.fingerprint() == search {
            return Ok(Some(UploadableKey::from(sk.key.clone())));
        }
    }

    Ok(None)
}

fn gen_subkeys(
    admin: &mut Card<Admin>,
    decrypt: bool,
    auth: bool,
    algo: Option<AlgoSimple>,
) -> Result<(PublicKey, Option<PublicKey>, Option<PublicKey>)> {
    // We begin by generating the signing subkey, which is mandatory.
    eprintln!(" Generate subkey for Signing");
    algo.map(|a| admin.set_algorithm(KeyType::Signing, a));
    let (pkm, ts) =
        admin.generate_key(openpgp_card_rpgp::public_to_fingerprint, KeyType::Signing)?;
    let key_sig = public_key_material_to_key(&pkm, KeyType::Signing, &ts, None, None)?;

    // make decryption subkey (unless disabled), with the same algorithm as
    // the sig key
    let key_dec = if decrypt {
        eprintln!(" Generate subkey for Decryption");
        algo.map(|a| admin.set_algorithm(KeyType::Decryption, a));
        let (pkm, ts) = admin.generate_key(
            openpgp_card_rpgp::public_to_fingerprint,
            KeyType::Decryption,
        )?;
        Some(public_key_material_to_key(
            &pkm,
            KeyType::Decryption,
            &ts,
            None,
            None,
        )?)
    } else {
        None
    };

    // make authentication subkey (unless disabled), with the same
    // algorithm as the sig key
    let key_aut = if auth {
        eprintln!(" Generate subkey for Authentication");
        algo.map(|a| admin.set_algorithm(KeyType::Authentication, a));
        let (pkm, ts) = admin.generate_key(
            openpgp_card_rpgp::public_to_fingerprint,
            KeyType::Authentication,
        )?;

        Some(public_key_material_to_key(
            &pkm,
            KeyType::Authentication,
            &ts,
            None,
            None,
        )?)
    } else {
        None
    };

    Ok((key_sig, key_dec, key_aut))
}

fn name_command(
    name: &str,
    mut card: Card<Transaction>,
    admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut admin = util::verify_to_admin(&mut card, admin_pin)?;

    admin.set_cardholder_name(name)?;
    Ok(())
}

fn url_command(
    url: &str,
    mut card: Card<Transaction>,
    admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut admin = util::verify_to_admin(&mut card, admin_pin)?;

    admin.set_url(url)?;
    Ok(())
}

fn signing_pin_validity(
    state: PinValidity,
    mut card: Card<Transaction>,
    admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
    eprintln!("User PIN validity duration for signing: {state:?}");
    admin.set_user_pin_signing_validity(bool::from(state))?;
    Ok(())
}

fn import_command(
    keyfile: PathBuf,
    sig_fp: Option<String>,
    dec_fp: Option<String>,
    aut_fp: Option<String>,
    key_passphrase: Vec<PathBuf>,
    mut card: Card<Transaction>,
    admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut file = std::fs::File::open(keyfile)?;
    let mut key = Tsk::load(&mut file)?;

    if key.len() != 1 {
        return Err(anyhow!("Expected one OpenPGP key, found {}", key.len()).into());
    }

    let key: Tsk = key.remove(0);

    fn to_fp(fp: Option<String>) -> Result<Option<Fingerprint>> {
        let fp = fp.map(hex::decode).transpose()?;
        Ok(fp
            .map(|v| Fingerprint::new(KeyVersion::V4, &v))
            .transpose()?)
    }

    // select the (sub)keys to upload
    let [mut sig, mut dec, mut auth] = match (sig_fp, dec_fp, aut_fp) {
        // No fingerprint has been provided, we try to autoselect keys
        // (this fails if there is more than one (sub)key for any keytype).
        (None, None, None) => keys_pick_yolo(&key)?,

        (sig_fp, dec_fp, aut_fp) => {
            keys_pick_explicit(&key, to_fp(sig_fp)?, to_fp(dec_fp)?, to_fp(aut_fp)?)?
        }
    };

    // Verify card to Admin
    // (And fail early if the PIN is wrong, before potentially asking for Tsk passwords)
    let mut admin = util::verify_to_admin(&mut card, admin_pin)?;

    // Handle Tsk passwords
    {
        let mut pws: Vec<String> = vec![];

        for pw in key_passphrase {
            let pw = std::fs::read_to_string(pw)?;
            pws.push(pw);
        }

        // helper:
        // Check if we have the right password for `key` in `pws`, if so unlock it.
        // Otherwise, ask the user for a password, try it. if it unlocks the key, add it to `pws`.
        let mut unlock_key = |key: &mut Option<UploadableKey>, key_type: &str| -> Result<()> {
            if let Some(k) = key {
                if !k.is_locked() {
                    // key is unencrypted, we need no password
                    return Ok(());
                }

                // key is encrypted, we need the password

                // do we already have the right password?
                for pw in &pws {
                    if k.try_unlock(pw).is_ok() {
                        return Ok(());
                    }
                }

                // no, we need to get the password from user
                let pw = rpassword::prompt_password(format!(
                    "Enter password for {} (sub)key {}:",
                    key_type,
                    k.fingerprint()?.to_hex()
                ))?;

                if k.try_unlock(&pw).is_ok() {
                    // remember pw for next subkeys
                    pws.push(pw.clone());

                    Ok(())
                } else {
                    // this password doesn't work, error out
                    Err(anyhow!(
                        "Password not valid for (Sub)Key {}",
                        k.fingerprint()?.to_hex()
                    ))
                }
            } else {
                // we have no key for this slot, so we don't need a password
                Ok(())
            }
        };

        // unlock, if encrypted (try previous pw before asking for user input)
        unlock_key(&mut sig, "signing")?;
        unlock_key(&mut dec, "decryption")?;
        unlock_key(&mut auth, "authentication")?;
    }

    // upload keys to card
    if let Some(sig) = sig {
        eprintln!("Uploading {} as signing key", sig.fingerprint()?);
        admin.import_key(&sig, KeyType::Signing)?;
    }
    if let Some(dec) = dec {
        eprintln!("Uploading {} as decryption key", dec.fingerprint()?);
        admin.import_key(&dec, KeyType::Decryption)?;
    }
    if let Some(auth) = auth {
        eprintln!("Uploading {} as authentication key", auth.fingerprint()?);
        admin.import_key(&auth, KeyType::Authentication)?;
    }
    Ok(())
}

fn generate_command(
    output_format: OutputFormat,
    output_version: OutputVersion,
    mut card: Card<Transaction>,

    admin_pin: Option<SecretString>,

    cmd: AdminGenerateCommand,
) -> Result<()> {
    // Validate the Admin PIN (possibly from a pin pad)
    // If the Admin PIN is validated, we can just use the card in admin mode, below.
    let _ = util::verify_to_admin(&mut card, admin_pin)
        .map_err(|e| anyhow!("Failed to open card in admin mode ({}).", e))?;

    // Handle User PIN: query the user for user PIN entry, if we haven't received a PIN via a file
    // parameter, and if no pinpad is available for this card.
    // (`user_pin` is `None` if `util::get_pin` has deduced that the user will use a pinpad.)
    let user_pin = util::get_pin(&mut card, cmd.user_pin, ENTER_USER_PIN)?;

    // Validate the User PIN (unless we're expecting to use a pin pad).
    //
    // Note that the User PIN is checked again, below, via the call to `crate::get_cert`.
    // This is in part redundant, but the call below will additionally cause validation via the
    // pinpad, if that is appropriate.
    //
    // This `verify_user_signing_pin` call only validates PINs that are available in the host
    // computer, to fail early.
    if let Some(user_pin) = user_pin.clone() {
        card.verify_user_signing_pin(user_pin)
            .map_err(|e| anyhow!("Failed to open card in user mode ({}).", e))?;
    }

    let mut output = output::AdminGenerate::default();
    output.ident(card.application_identifier()?.ident());

    // 1) Interpret the user's choice of algorithm.
    //
    // Unset (None) means that the algorithm that is specified on the card
    // should remain unchanged.
    //
    // For RSA, different cards use different exact algorithm
    // specifications. In particular, the length of the value `e` differs
    // between cards. Some devices use 32 bit length for e, others use 17 bit.
    // In some cases, it's possible to get this information from the card,
    // but I believe this information is not obtainable in all cases.
    // Because of this, for generation of RSA keys, here we take the approach
    // of first trying one variant, and then if that fails, try the other.

    let algo = cmd.algo.map(AlgoSimple::from);
    log::info!(" Key generation will be attempted with algo: {algo:?}");
    output.algorithm(format!("{algo:?}"));

    // 2) Then, generate keys on the card.
    // We need "admin" access to the card for this.
    let (key_sig, key_dec, key_aut) = {
        // we've presented the Admin PIN above and can just use the card in admin mode now
        let mut admin = card.as_admin_card(None)?;

        gen_subkeys(&mut admin, cmd.decrypt, cmd.auth, algo)?
    };

    // 3) Generate a Cert from the generated keys. For this, we
    // need "signing" access to the card (to make binding signatures within
    // the Cert).
    let cert = crate::get_cert(
        &mut card,
        key_sig,
        key_dec,
        key_aut,
        user_pin,
        &cmd.user_ids,
        &|| eprintln!("Enter User PIN on card reader pinpad."),
    )?;

    let mut buf = Vec::new();
    cert.save(true, &mut buf)?;
    let armored = std::str::from_utf8(buf.as_slice())?.to_string();

    output.public_key(armored);

    // Write armored certificate to the output file
    let mut handle = util::open_or_stdout(Some(&cmd.output_file))?;
    handle.write_all(output.print(output_format, output_version)?.as_bytes())?;
    let _ = handle.write(b"\n")?;

    Ok(())
}

fn touch_command(
    mut card: Card<Transaction>,
    admin_pin: Option<SecretString>,
    key: BasePlusAttKeySlot,
    policy: TouchPolicy,
) -> Result<(), Box<dyn std::error::Error>> {
    let kt = KeyType::from(key);

    let pol = openpgp_card::ocard::data::TouchPolicy::from(policy);

    let mut admin = util::verify_to_admin(&mut card, admin_pin)?;

    admin.set_touch_policy(kt, pol)?;
    Ok(())
}
