#![cfg_attr(not(test), warn(unused_crate_dependencies))]

use serde::{Serialize, de::DeserializeOwned};
use std::{io::Read, path::Path};
use thiserror::Error;

mod input;
pub use input::{Input, InputItem};

mod reports;
pub use reports::{ProgramExecutionReport, ProgramProvingReport};

mod network;
pub use network::NetworkProverConfig;

#[allow(non_camel_case_types)]
/// Compiler trait for compiling programs into an opaque sequence of bytes.
pub trait Compiler {
    type Error: std::error::Error + Send + Sync + 'static;
    type Program: Clone + Send + Sync + Serialize + DeserializeOwned;

    /// Compiles the program and returns the program
    ///
    /// # Arguments
    /// * `guest_directory` - The path to the guest program directory
    fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error>;
}

/// ResourceType specifies what resource will be used to create the proofs.
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
pub enum ProverResourceType {
    #[default]
    Cpu,
    Gpu,
    /// Use a remote prover network
    Network(NetworkProverConfig),
}

#[cfg(feature = "clap")]
impl ProverResourceType {
    pub fn to_args(&self) -> Vec<&str> {
        match self {
            Self::Cpu => vec!["cpu"],
            Self::Gpu => vec!["gpu"],
            Self::Network(config) => core::iter::once("network")
                .chain(config.to_args())
                .collect(),
        }
    }
}

/// An error that can occur during prove, execute or verification
/// of a zkVM.
///
/// Note: We use a concrete error type here, so that downstream crates
/// can do patterns such as Vec<dyn zkVM>
#[allow(non_camel_case_types)]
#[derive(Debug, Error)]
pub enum zkVMError {
    /// Network-related errors
    #[error("Network error: {0}")]
    Network(String),

    /// Authentication error
    #[error("Authentication failed: {0}")]
    Authentication(String),

    /// Timeout error
    #[error("Operation timed out after {0:?}")]
    Timeout(std::time::Duration),

    /// Service unavailable
    #[error("Prover service unavailable: {0}")]
    ServiceUnavailable(String),

    /// Invalid response from network
    #[error("Invalid response from prover network: {0}")]
    InvalidResponse(String),

    // TODO: We can add more variants as time goes by.
    // TODO: for now, we use this catch-all as a way to prototype faster
    #[error(transparent)]
    Other(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
}

impl zkVMError {
    pub fn other(error: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
        Self::Other(error.into())
    }
}

/// Public values committed/revealed by guest program.
///
/// Use [`zkVM::deserialize_from`] to deserialize object from the bytes.
pub type PublicValues = Vec<u8>;

/// Proof generated by [`zkVM::prove`], that also includes the [`PublicValues`]
/// for [`zkVM::verify`] to work.
pub type Proof = Vec<u8>;

#[allow(non_camel_case_types)]
#[auto_impl::auto_impl(&, Arc, Box)]
/// zkVM trait to abstract away the differences between each zkVM.
///
/// This trait provides a unified interface, the workflow is:
/// 1. Compile a guest program using the corresponding `Compiler`.
/// 2. Create a zkVM instance with the compiled program and prover resource.
/// 3. Execute, prove, and verify using the zkVM instance methods.
///
/// Note that a zkVM instance is created for specific program, each zkVM
/// implementation will have their own construction function.
pub trait zkVM {
    /// Executes the program with the provided inputs.
    fn execute(&self, inputs: &Input) -> Result<(PublicValues, ProgramExecutionReport), zkVMError>;

    /// Creates a proof of the program execution with given inputs.
    fn prove(
        &self,
        inputs: &Input,
    ) -> Result<(PublicValues, Proof, ProgramProvingReport), zkVMError>;

    /// Verifies a proof of the program used to create this zkVM instance, then
    /// returns the public values extracted from the proof.
    #[must_use = "Public values must be used"]
    fn verify(&self, proof: &[u8]) -> Result<PublicValues, zkVMError>;

    /// Returns the name of the zkVM
    fn name(&self) -> &'static str;

    /// Returns the version of the zkVM SDK (e.g. 0.1.0)
    fn sdk_version(&self) -> &'static str;

    /// Deserializes an object from a [`Read`]er.
    ///
    /// If a guest program has multiple objects committed/revealed, one can do
    /// the following to extract them in sequence:
    ///
    /// ```ignore
    /// let public_values = zkvm.verify(&proof)?;
    /// let mut reader = public_values.as_slice();
    /// let v0: T = zkvm.deserialize_from(&mut reader)?;
    /// let v1: U = zkvm.deserialize_from(&mut reader)?;
    /// ```
    fn deserialize_from<R: Read, T: DeserializeOwned>(&self, reader: R) -> Result<T, zkVMError>;
}
