//! Command line interface for interacting with the "File Hierarchy for the Verification of OS
//! Artifacts" (VOA).

use std::{
    collections::{BTreeMap, HashSet},
    fmt::Display,
    path::PathBuf,
    process::ExitCode,
};

use clap::Parser;
use log::LevelFilter;
use serde::Serialize;
use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
use voa::{
    Error,
    cli::{Cli, Command, ConfigCommand, OutputFormat},
    commands::{
        PurposeAndContext,
        get_technology_settings,
        get_voa_config,
        get_writable_load_path,
        load_verifier,
        openpgp_verify,
        read_openpgp_signatures,
        read_openpgp_verifiers,
        search_verifiers,
        write_verifier_to_hierarchy,
    },
};
use voa_config::{ConfigOrigin, VoaConfig};
use voa_core::identifiers::{Context, Os, Purpose, Technology};
use voa_openpgp::{ModelBasedVerifier, OpenpgpSignatureCheck};

/// Output representation of load path information.
#[derive(Debug, Serialize)]
struct LoadOutput {
    /// The load path of the verifier.
    pub load_path: PathBuf,
    /// Whether the verifier is located in a writable load path.
    pub writable: bool,
    /// Whether the verifier is located in an ephemeral load path.
    pub ephemeral: bool,
}

/// Output representation of a verifier.
#[derive(Debug, Serialize)]
struct VerifierOutput {
    /// Load path information.
    pub load_path: LoadOutput,
    /// The full file path of the verifier.
    pub verifier_path: PathBuf,
    /// The associated OS information.
    pub os: Os,
    /// The associated purpose.
    pub purpose: Purpose,
    /// The associated context.
    pub context: Context,
    /// The used technology.
    pub technology: Technology,
}

/// The verification result of an OpenPGP signature.
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OpenpgpVerificationResult {
    /// A signature was successfully validated.
    Valid {
        /// The signature creation time.
        signature_creation_time: Option<u64>,
        /// The primary key fingerprint.
        primary_key_fingerprint: String,
        /// The verifying component key fingerprint.
        verifying_component_key_fingerprint: String,
    },
    /// A signature could not be validated.
    Invalid,
}

/// The result of a verification.
#[derive(Debug, Serialize)]
pub struct OpenpgpVerificationOutput {
    /// The optional path of the signature.
    pub signature: Option<PathBuf>,
    /// The verification result for the `signature`.
    pub result: OpenpgpVerificationResult,
}

impl Display for OpenpgpVerificationOutput {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{} ",
            match &self.result {
                OpenpgpVerificationResult::Valid { .. } => "✅",
                OpenpgpVerificationResult::Invalid => "❌",
            }
        )?;

        if let Some(signature_path) = self.signature.as_ref() {
            write!(f, "{} ", signature_path.as_os_str().to_string_lossy())?;
        } else {
            write!(f, "- ")?;
        }

        match &self.result {
            OpenpgpVerificationResult::Valid {
                signature_creation_time,
                primary_key_fingerprint,
                verifying_component_key_fingerprint,
            } => {
                if let Some(signature_creation_time) = signature_creation_time {
                    write!(f, "{signature_creation_time} ")?;
                } else {
                    write!(f, "- ")?;
                }

                write!(
                    f,
                    "{primary_key_fingerprint} {verifying_component_key_fingerprint}"
                )?;
            }
            OpenpgpVerificationResult::Invalid => {
                write!(f, "- - -")?;
            }
        }

        Ok(())
    }
}

impl<'a> From<&OpenpgpSignatureCheck<'a>> for OpenpgpVerificationOutput {
    fn from(value: &OpenpgpSignatureCheck<'a>) -> Self {
        let result = if let Some(signer_info) = value.signer_info() {
            OpenpgpVerificationResult::Valid {
                signature_creation_time: value
                    .signature()
                    .creation_time()
                    .map(|time| time.as_secs().into()),
                primary_key_fingerprint: signer_info
                    .certificate()
                    .certificate
                    .fingerprint()
                    .to_string(),
                verifying_component_key_fingerprint: signer_info
                    .component_fingerprint()
                    .to_string(),
            }
        } else {
            OpenpgpVerificationResult::Invalid
        };

        Self {
            signature: value.signature().source().map(|path| path.to_path_buf()),
            result,
        }
    }
}

/// Output representation of the ['ConfigOrigin']s of all configs.
#[derive(Debug, Serialize)]
struct ConfigOriginsOutput<'a> {
    os: BTreeMap<String, &'a [ConfigOrigin]>,
    os_purpose_context: BTreeMap<String, &'a [ConfigOrigin]>,
}

impl<'a> Display for ConfigOriginsOutput<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for (os, origins) in &self.os {
            writeln!(f, "🖥️ {os}")?;
            for origin in origins.iter() {
                writeln!(f, "⤷ {origin}")?;
            }
        }

        for (os_purpose_context, origins) in &self.os_purpose_context {
            writeln!(f, "➕️ {os_purpose_context}")?;
            for origin in origins.iter() {
                writeln!(f, "⤷ {origin}")?;
            }
        }

        Ok(())
    }
}

impl<'a> From<&'a VoaConfig> for ConfigOriginsOutput<'a> {
    fn from(value: &'a VoaConfig) -> Self {
        let os = value
            .oses()
            .iter()
            .map(|(os, settings)| (os.to_string(), settings.origins()))
            .collect::<BTreeMap<String, &[ConfigOrigin]>>();
        let os_purpose_context = value
            .contexts()
            .iter()
            .map(|(os_purpose_context, settings)| {
                (os_purpose_context.to_string(), settings.origins())
            })
            .collect::<BTreeMap<String, &[ConfigOrigin]>>();
        Self {
            os,
            os_purpose_context,
        }
    }
}

/// Initializes a logger globally based on a log level.
///
/// Emits a warning on stderr, if the logger could not be initialized.
fn init_logger(log_level: LevelFilter) {
    if TermLogger::init(
        log_level,
        Config::default(),
        TerminalMode::Stderr,
        ColorChoice::Auto,
    )
    .is_err()
    {
        eprintln!("Failed initializing a logger. Log messages may not be available!");
    }
}

fn run_voa() -> Result<(), Error> {
    let cli = Cli::parse();
    init_logger(cli.verbosity.into());

    match cli.command {
        Command::Config(command) => {
            let config = get_voa_config();
            match command {
                ConfigCommand::List(command) => {
                    let config_paths = ConfigOriginsOutput::from(&config);
                    match command.output_format {
                        OutputFormat::Text => {
                            println!("{config_paths}");
                        }
                        OutputFormat::Json => {
                            let output = serde_json::to_string(&config_paths).map_err(
                            |source| Error::JsonSerialization {
                                context: "processing the output of the voa config list subcommand",
                                source,
                            },
                        )?;
                            println!("{output}");
                        }
                    }
                }
                ConfigCommand::Show(command) => {
                    let technology_settings = get_technology_settings(
                        &config,
                        &command.os,
                        PurposeAndContext::new(command.purpose, command.context).as_ref(),
                    );

                    match command.output_format {
                        OutputFormat::Text => {
                            println!("{technology_settings}");
                        }
                        OutputFormat::Json => {
                            let output = serde_json::to_string(&technology_settings).map_err(
                            |source| Error::JsonSerialization {
                                context: "processing the output of the voa config show subcommand",
                                source,
                            },
                        )?;
                            println!("{output}");
                        }
                    }
                }
            }
        }
        Command::Import(command) => {
            let base_path = if let Some(base_path) = command.base_path {
                base_path
            } else {
                get_writable_load_path(command.runtime)?
            };

            let verifier = load_verifier(command.input, command.technology)?;

            write_verifier_to_hierarchy(
                verifier,
                base_path,
                command.os,
                command.purpose,
                command.context,
            )?;
        }
        Command::List(command) => {
            let verifiers = search_verifiers(
                command.os,
                command.purpose,
                command.context,
                command.technology,
            )?;
            let verifiers: Vec<VerifierOutput> = verifiers
                .values()
                .flat_map(|vec| vec.iter())
                .map(|verifier| VerifierOutput {
                    load_path: LoadOutput {
                        load_path: verifier.voa_location().load_path().path().into(),
                        writable: verifier.voa_location().load_path().writable(),
                        ephemeral: verifier.voa_location().load_path().ephemeral(),
                    },
                    verifier_path: verifier.canonicalized().into(),
                    os: verifier.voa_location().os().clone(),
                    purpose: verifier.voa_location().purpose().clone(),
                    context: verifier.voa_location().context().clone(),
                    technology: verifier.voa_location().technology().clone(),
                })
                .collect();

            match command.output_format {
                OutputFormat::Text => {
                    for verifier in verifiers {
                        println!("{}", verifier.verifier_path.as_os_str().to_string_lossy())
                    }
                }
                OutputFormat::Json => {
                    let output = serde_json::to_string(&verifiers).map_err(|source| {
                        Error::JsonSerialization {
                            context: "processing the output of the voa list subcommand",
                            source,
                        }
                    })?;
                    println!("{output}");
                }
            }
        }
        Command::Verify(command) => {
            // line up configuration
            let config = get_voa_config();
            let purpose = command.purpose.as_ref();
            let purpose_and_context =
                PurposeAndContext::new(Some(purpose.clone()), Some(command.context.clone()));
            let openpgp_settings =
                get_technology_settings(&config, &command.os, purpose_and_context.as_ref())
                    .openpgp_settings();

            // initialize trust model implementation
            let verifiers = read_openpgp_verifiers(
                command.os.clone(),
                purpose.clone(),
                command.context.clone(),
            );
            let anchors = read_openpgp_verifiers(
                command.os,
                purpose.clone().to_trust_anchor(),
                command.context,
            );
            let model = ModelBasedVerifier::new(openpgp_settings, &verifiers, &anchors);

            // load data signatures
            let signatures = read_openpgp_signatures(&HashSet::from_iter(command.signatures))?;

            // verify data signatures against the trust model
            let verifications = openpgp_verify(&model, &signatures, &command.file)?;

            let verification_output: Vec<OpenpgpVerificationOutput> = verifications
                .iter()
                .map(OpenpgpVerificationOutput::from)
                .collect();

            match command.output_format {
                OutputFormat::Text => {
                    for verification in &verification_output {
                        println!("{verification}");
                    }
                }
                OutputFormat::Json => {
                    let output = serde_json::to_string(&verification_output).map_err(|source| {
                        Error::JsonSerialization {
                            context: "processing the output of the voa verify subcommand",
                            source,
                        }
                    })?;
                    println!("{output}");
                }
            }

            // Return an error, if there are unsuccessful signature verifications.
            let failed_signatures: Vec<String> = verification_output
                .iter()
                .filter_map(|verification| {
                    if matches!(verification.result, OpenpgpVerificationResult::Invalid) {
                        if let Some(path) = verification.signature.as_ref() {
                            Some(path.to_string_lossy().to_string())
                        } else {
                            Some("-".to_string())
                        }
                    } else {
                        None
                    }
                })
                .collect();

            if !failed_signatures.is_empty() {
                return Err(Error::SignatureVerificationFailed {
                    signatures: failed_signatures,
                });
            }
        }
    }
    Ok(())
}

fn main() -> ExitCode {
    if let Err(error) = run_voa() {
        eprintln!("{error}");
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}
