//! Verifier implementation for v1 client puzzles

use crate::v1::challenge::Challenge;
use crate::v1::{Instance, RuntimeError, RuntimeOption, Solution, SolutionError};
use crate::Error;
use equix::{EquiXBuilder, HashError};

/// Checker for potential [`Solution`]s to a particular puzzle [`Instance`]
///
/// Holds information about the puzzle instance, and optional configuration
/// settings.
pub struct Verifier {
    /// The puzzle instance we're verifying
    instance: Instance,
    /// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance
    equix: EquiXBuilder,
}

impl Verifier {
    /// Construct a new [`Verifier`] by wrapping an [`Instance`].
    pub fn new(instance: Instance) -> Self {
        Self {
            instance,
            equix: Default::default(),
        }
    }

    /// Select the HashX runtime to use for this verifier.
    ///
    /// By default, uses [`RuntimeOption::TryCompile`]
    pub fn runtime(&mut self, option: RuntimeOption) -> &mut Self {
        self.equix.runtime(option);
        self
    }

    /// Check whether a solution is valid for this puzzle instance.
    ///
    /// May return a [`SolutionError`] or a [`RuntimeError`]
    pub fn check(&self, solution: &Solution) -> Result<(), Error> {
        match self.check_seed(solution) {
            Err(e) => Err(Error::BadSolution(e.into())),
            Ok(()) => {
                let challenge = Challenge::new(&self.instance, solution.effort(), solution.nonce());
                match challenge.check_effort(&solution.proof_to_bytes()) {
                    Err(e) => Err(Error::BadSolution(e.into())),
                    Ok(()) => match self.equix.verify(challenge.as_ref(), solution.proof()) {
                        Ok(()) => Ok(()),
                        Err(equix::Error::HashSum) => {
                            Err(Error::BadSolution(SolutionError::HashSum.into()))
                        }
                        Err(equix::Error::Hash(HashError::ProgramConstraints)) => Err(
                            Error::BadSolution(SolutionError::ChallengeConstraints.into()),
                        ),
                        Err(e) => Err(Error::VerifyRuntime(RuntimeError::EquiX(e).into())),
                    },
                }
            }
        }
    }

    /// Check the [`super::SeedHead`] of a solution against an [`Instance`].
    ///
    /// This is a very cheap test, this should come first so a service
    /// can verify every [`Solution`] against its last two [`Instance`]s.
    fn check_seed(&self, solution: &Solution) -> Result<(), SolutionError> {
        if solution.seed_head() == self.instance.seed().head() {
            Ok(())
        } else {
            Err(SolutionError::Seed)
        }
    }
}
