// 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::env::var_os;

use anyhow::{Result, anyhow};
use clap::Parser;
use openpgp_card::{
    Card,
    state::{Open, Transaction},
};
use pgp::packet::PublicKey;
use rpgpie::certificate::Certificate;

mod cli;
mod commands;
mod output;
mod util;
mod versioned_output;

use cli::OUTPUT_VERSIONS;
use secrecy::SecretString;
use versioned_output::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};

use crate::cli::DEFAULT_OUTPUT_VERSION;

const ENTER_USER_PIN: &str = "Enter User PIN:";
const ENTER_ADMIN_PIN: &str = "Enter Admin PIN:";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::init();

    if let Some(mandir) = var_os("OCT_MANPAGE_OUTPUT_DIR") {
        match mandir.into_string().as_ref() {
            Ok(mandir) => {
                clap_allgen::render_manpages::<cli::Cli>(mandir)?;
                return Ok(());
            }
            Err(mandir) => {
                return Err(anyhow!("Failed to convert string: {:?}", mandir).into());
            }
        }
    }

    if let Some(output_dir) = var_os("OCT_COMPLETION_OUTPUT_DIR") {
        match output_dir.into_string().as_ref() {
            Ok(output_dir) => {
                clap_allgen::render_shell_completions::<cli::Cli>(output_dir)?;
                return Ok(());
            }
            Err(output_dir) => {
                return Err(anyhow!("Failed to convert string: {:?}", output_dir).into());
            }
        }
    }

    let cli = cli::Cli::parse();

    match cli.cmd {
        cli::Command::OutputVersions {} => {
            output_versions(cli.output_version);
        }
        cli::Command::List(cmd) => {
            commands::list::print_card_list(cli.output_format, cli.output_version, Some(cmd))?;
        }
        cli::Command::Status(cmd) => {
            commands::status::print_status(cli.output_format, cli.output_version, cmd)?;
        }
        cli::Command::Info(cmd) => {
            commands::info::print_info(cli.output_format, cli.output_version, cmd)?;
        }
        cli::Command::Ssh(cmd) => {
            commands::ssh::print_ssh(cli.output_format, cli.output_version, cmd)?;
        }
        cli::Command::Pubkey(cmd) => {
            commands::pubkey::print_pubkey(cli.output_format, cli.output_version, cmd)?;
        }
        cli::Command::Decrypt(cmd) => {
            commands::decrypt::decrypt(cmd)?;
        }
        cli::Command::Sign(cmd) => {
            commands::sign::sign(cmd)?;
        }
        cli::Command::Attestation(cmd) => {
            commands::attestation::attestation(cli.output_format, cli.output_version, cmd)?;
        }
        cli::Command::System(cmd) => {
            commands::system::system(cmd)?;
        }
        cli::Command::Admin(cmd) => {
            commands::admin::admin(cli.output_format, cli.output_version, cmd)?;
        }
        cli::Command::Pin(cmd) => {
            commands::pin::pin(&cmd.ident, cmd.cmd)?;
        }
    }

    Ok(())
}

fn output_versions(chosen: OutputVersion) {
    for v in OUTPUT_VERSIONS.iter() {
        if v == &chosen {
            println!("* {v}");
        } else {
            println!("  {v}");
        }
    }
}

/// Return a card for a read operation. If `ident` is None, and exactly one card
/// is plugged in, that card is returned. (We don't This
fn pick_card_for_reading(ident: Option<String>) -> Result<Card<Open>> {
    if let Some(ident) = ident {
        Ok(util::open_card(&ident)?)
    } else {
        let mut cards = util::cards()?;
        if cards.len() == 1 {
            Ok(cards
                .pop()
                .ok_or_else(|| anyhow!("There are no more cards"))?)
        } else if cards.is_empty() {
            Err(anyhow::anyhow!("No cards found"))
        } else {
            // The output version for OutputFormat::Text doesn't matter (it's ignored).
            commands::list::print_card_list(OutputFormat::Text, DEFAULT_OUTPUT_VERSION, None)?;

            eprintln!("Specify which card to use with '--card <card ident>'\n");

            Err(anyhow::anyhow!("Found more than one card"))
        }
    }
}

/// public key representation
fn get_cert(
    card: &mut Card<Transaction>,
    key_sig: PublicKey,
    key_dec: Option<PublicKey>,
    key_aut: Option<PublicKey>,
    user_pin: Option<SecretString>,
    user_ids: &[String],
    prompt: &dyn Fn(),
) -> Result<Certificate> {
    if user_pin.is_none() && card.feature_pinpad_verify() {
        eprintln!(
            "The public cert will now be generated.\n\n\
             You will need to enter your User PIN multiple times during this process.\n\n"
        );
    }

    Ok(openpgp_card_rpgp::bind_into_certificate(
        card,
        key_sig,
        key_dec,
        key_aut,
        user_ids,
        user_pin,
        prompt,
        &|| eprintln!("Touch confirmation needed for signing"),
    )
    .map(Certificate::from)?)
}
