use std::{fs, path::Path};

use crate::{
    ASYNC_RUNTIME,
    drivers::opts::{PrivateKeyContents, VerifyType},
};

use super::{
    SigningDriver,
    functions::get_private_key,
    opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
};
use blue_build_utils::{
    constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
    credentials::Credentials,
    retry,
};
use colored::Colorize;
use log::{debug, trace};
use miette::{Context, IntoDiagnostic, bail, miette};
use sigstore::{
    cosign::{
        ClientBuilder, Constraint, CosignCapabilities, SignatureLayer,
        constraint::PrivateKeySigner,
        verification_constraint::{PublicKeyVerifier, VerificationConstraintVec},
    },
    crypto::{SigningScheme, signing_key::SigStoreKeyPair},
    errors::SigstoreVerifyConstraintsError,
    registry::{Auth, OciReference},
};
use zeroize::Zeroizing;

pub struct SigstoreDriver;

impl SigningDriver for SigstoreDriver {
    fn generate_key_pair(opts: GenerateKeyPairOpts) -> miette::Result<()> {
        let path = opts.dir.unwrap_or_else(|| Path::new("."));
        let priv_key_path = path.join(COSIGN_PRIV_PATH);
        let pub_key_path = path.join(COSIGN_PUB_PATH);

        if priv_key_path.exists() {
            bail!("Private key file already exists at {COSIGN_PRIV_PATH}");
        } else if pub_key_path.exists() {
            bail!("Public key file already exists at {COSIGN_PUB_PATH}");
        }

        let signer = SigningScheme::default()
            .create_signer()
            .into_diagnostic()
            .context("Failed to create signer")?;

        let keypair = signer
            .to_sigstore_keypair()
            .into_diagnostic()
            .context("Failed to create key pair")?;

        let priv_key = keypair
            .private_key_to_encrypted_pem(b"")
            .into_diagnostic()
            .context("Failed to create encrypted private key")?;
        let pub_key = keypair.public_key_to_pem().into_diagnostic()?;

        fs::write(priv_key_path, priv_key)
            .into_diagnostic()
            .with_context(|| format!("Failed to write {COSIGN_PRIV_PATH}"))?;
        fs::write(pub_key_path, pub_key)
            .into_diagnostic()
            .with_context(|| format!("Failed to write {COSIGN_PUB_PATH}"))?;

        Ok(())
    }

    fn check_signing_files(opts: CheckKeyPairOpts) -> miette::Result<()> {
        trace!("SigstoreDriver::check_signing_files({opts:?})");

        let path = opts.dir.unwrap_or_else(|| Path::new("."));
        let pub_path = path.join(COSIGN_PUB_PATH);

        let pub_key = fs::read_to_string(&pub_path)
            .into_diagnostic()
            .with_context(|| format!("Failed to open public key file {}", pub_path.display()))?;
        debug!("Retrieved public key from {COSIGN_PUB_PATH}");
        trace!("{pub_key}");

        let key: Zeroizing<String> = get_private_key(path)
            .context("Failed to get private key")?
            .contents()?;
        debug!("Retrieved private key");

        let keypair = SigStoreKeyPair::from_encrypted_pem(key.as_bytes(), b"")
            .into_diagnostic()
            .context("Failed to generate key pair from private key")?;
        let gen_pub = keypair
            .public_key_to_pem()
            .into_diagnostic()
            .context("Failed to generate public key from private key")?;
        debug!("Generated public key from private key");
        trace!("{gen_pub}");

        if pub_key.trim() == gen_pub.trim() {
            debug!("Public and private key matches");
            Ok(())
        } else {
            bail!("Private and public keys do not match.")
        }
    }

    fn sign(opts: SignOpts) -> miette::Result<()> {
        trace!("SigstoreDriver::sign({opts:?})");

        if opts.image.digest().is_none() {
            bail!(
                "Image ref {} is not a digest ref",
                opts.image.to_string().bold().red(),
            );
        }

        let path = opts.dir.unwrap_or_else(|| Path::new("."));
        let mut client = ClientBuilder::default().build().into_diagnostic()?;
        let image_digest: OciReference = opts.image.to_string().parse().into_diagnostic()?;

        let signing_scheme = SigningScheme::default();
        let key: Zeroizing<Vec<u8>> = get_private_key(path)?.contents()?;
        debug!("Retrieved private key");

        let signer = PrivateKeySigner::new_with_signer(
            SigStoreKeyPair::from_encrypted_pem(&key, b"")
                .into_diagnostic()?
                .to_sigstore_signer(&signing_scheme)
                .into_diagnostic()?,
        );
        debug!("Created signer");

        let auth = match Credentials::get(image_digest.registry()) {
            Some(Credentials::Basic { username, password }) => {
                Auth::Basic(username, password.value().into())
            }
            _ => Auth::Anonymous,
        };
        debug!("Credentials retrieved");

        let (cosign_signature_image, source_image_digest) = retry(2, 5, || {
            ASYNC_RUNTIME
                .block_on(client.triangulate(&image_digest, &auth))
                .into_diagnostic()
                .with_context(|| format!("Failed to triangulate image {image_digest}"))
        })?;
        debug!("Triangulating image");
        trace!("{cosign_signature_image}, {source_image_digest}");

        let mut signature_layer =
            SignatureLayer::new_unsigned(&image_digest, &source_image_digest).into_diagnostic()?;
        signer
            .add_constraint(&mut signature_layer)
            .into_diagnostic()?;
        debug!("Created signing layer");

        debug!("Pushing signature");
        retry(2, 5, || {
            ASYNC_RUNTIME
                .block_on(client.push_signature(
                    None,
                    &auth,
                    &cosign_signature_image,
                    vec![signature_layer.clone()],
                ))
                .into_diagnostic()
                .with_context(|| {
                    format!(
                        "Failed to push signature {cosign_signature_image} for image {image_digest}"
                    )
                })
        })?;
        debug!("Successfully pushed signature");

        Ok(())
    }

    fn verify(opts: VerifyOpts) -> miette::Result<()> {
        let mut client = ClientBuilder::default().build().into_diagnostic()?;

        let image_digest: OciReference = opts.image.to_string().parse().into_diagnostic()?;
        trace!("{image_digest:?}");

        let signing_scheme = SigningScheme::default();

        let pub_key = fs::read_to_string(match &opts.verify_type {
            VerifyType::File(path) => path,
            VerifyType::Keyless { .. } => {
                todo!("Keyless currently not supported for sigstore driver")
            }
        })
        .into_diagnostic()
        .with_context(|| format!("Failed to open public key file {COSIGN_PUB_PATH}"))?;
        debug!("Retrieved public key from {COSIGN_PUB_PATH}");
        trace!("{pub_key}");

        let verifier =
            PublicKeyVerifier::new(pub_key.as_bytes(), &signing_scheme).into_diagnostic()?;
        let verification_constraints: VerificationConstraintVec = vec![Box::new(verifier)];

        debug!("Triangulating image");
        let auth = Auth::Anonymous;
        let (cosign_signature_image, source_image_digest) = retry(2, 5, || {
            ASYNC_RUNTIME
                .block_on(client.triangulate(&image_digest, &auth))
                .into_diagnostic()
                .with_context(|| format!("Failed to triangulate image {image_digest}"))
        })?;
        trace!("{cosign_signature_image}, {source_image_digest}");

        let trusted_layers = retry(2, 5, || {
            ASYNC_RUNTIME
                .block_on(client.trusted_signature_layers(
                    &auth,
                    &source_image_digest,
                    &cosign_signature_image,
                ))
                .into_diagnostic()
        })?;

        sigstore::cosign::verify_constraints(&trusted_layers, verification_constraints.iter())
            .map_err(
                |SigstoreVerifyConstraintsError {
                     unsatisfied_constraints,
                 }| {
                    miette!("Failed to verify for constraints: {unsatisfied_constraints:?}")
                },
            )
    }

    fn signing_login(_server: &str) -> miette::Result<()> {
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use std::{fs, path::Path};

    use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
    use tempfile::TempDir;

    use crate::drivers::{
        SigningDriver,
        cosign_driver::CosignDriver,
        opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
    };

    use super::SigstoreDriver;

    #[test]
    fn generate_key_pair() {
        let tempdir = TempDir::new().unwrap();

        SigstoreDriver::generate_key_pair(
            GenerateKeyPairOpts::builder().dir(tempdir.path()).build(),
        )
        .unwrap();

        eprintln!(
            "Private key:\n{}",
            fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
        );
        eprintln!(
            "Public key:\n{}",
            fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
        );

        SigstoreDriver::check_signing_files(
            CheckKeyPairOpts::builder().dir(tempdir.path()).build(),
        )
        .unwrap();
    }

    #[test]
    fn check_key_pairs() {
        let path = Path::new("../test-files/keys");

        SigstoreDriver::check_signing_files(CheckKeyPairOpts::builder().dir(path).build()).unwrap();
    }

    #[test]
    fn compatibility() {
        let tempdir = TempDir::new().unwrap();

        SigstoreDriver::generate_key_pair(
            GenerateKeyPairOpts::builder().dir(tempdir.path()).build(),
        )
        .unwrap();

        eprintln!(
            "Private key:\n{}",
            fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
        );
        eprintln!(
            "Public key:\n{}",
            fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
        );

        CosignDriver::check_signing_files(CheckKeyPairOpts::builder().dir(tempdir.path()).build())
            .unwrap();
    }
}
