use crate::args;
use crate::errors::*;
use crate::keygen::in_toto::InTotoEmbedded;
use data_encoding::BASE64;
use in_toto::crypto::{self, HashAlgorithm, HashValue, PrivateKey, SignatureScheme};
use in_toto::interchange::Json;
use in_toto::models::{LinkMetadataBuilder, VirtualTargetPath};
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::str::FromStr;

#[derive(Debug)]
pub struct SigningConfig {
    pub name: String,
    pub materials: Vec<VirtualEntry>,
    pub products: Vec<VirtualEntry>,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct VirtualEntry {
    pub name: String,
    pub path: PathBuf,
}

impl FromStr for VirtualEntry {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let (name, path) = s
            .split_once('=')
            .context("Invalid format, expected `name=/path/to/file.txt`")?;
        Ok(VirtualEntry {
            name: name.into(),
            path: path.into(),
        })
    }
}

impl TryFrom<args::SignInToto> for SigningConfig {
    type Error = Error;

    fn try_from(in_toto: args::SignInToto) -> Result<Self> {
        Ok(Self {
            name: in_toto.name,
            materials: in_toto.material,
            products: in_toto.product,
        })
    }
}

pub fn read_artifact(
    name: String,
    path: &Path,
) -> Result<(VirtualTargetPath, HashMap<HashAlgorithm, HashValue>)> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);
    let hash_algorithms = &[HashAlgorithm::Sha256];
    let (_length, hashes) = crypto::calculate_hashes(&mut reader, hash_algorithms)?;
    Ok((VirtualTargetPath::new(name)?, hashes))
}

pub fn sign(signer: &InTotoEmbedded, config: &SigningConfig) -> Result<Vec<u8>> {
    let secret_key = BASE64
        .decode(signer.secret_key.trim().as_bytes())
        .context("Failed to decode signing key")?;
    let secret_key = PrivateKey::from_pkcs8(&secret_key, SignatureScheme::Ed25519)
        .context("Failed to load signing key")?;

    let materials = config
        .materials
        .iter()
        .map(|e| read_artifact(e.name.to_string(), &e.path))
        .collect::<Result<_>>()
        .context("Failed to collect materials")?;

    let products = config
        .products
        .iter()
        .map(|e| read_artifact(e.name.to_string(), &e.path))
        .collect::<Result<_>>()
        .context("Failed to collect materials")?;

    let link_metadata_builder = LinkMetadataBuilder::new()
        .name(config.name.to_string())
        .materials(materials)
        .products(products);

    let attestation = link_metadata_builder.signed::<Json>(&secret_key)?;

    let buf = serde_json::to_vec(&attestation)?;
    Ok(buf)
}
