//! Provides methods for obtaining validator signatures, including:
//!
//! - Via a local `Keypair`.
//! - Via a remote signer (Web3Signer)

use bls::{Keypair, PublicKey, Signature};
use eth2_keystore::Keystore;
use lockfile::Lockfile;
use parking_lot::Mutex;
use reqwest::{Client, header::ACCEPT};
use std::path::PathBuf;
use std::sync::Arc;
use task_executor::{RayonPoolType, TaskExecutor};
use tracing::instrument;
use types::*;
use url::Url;
use web3signer::{ForkInfo, MessageType, SigningRequest, SigningResponse};

pub use web3signer::Web3SignerObject;

mod web3signer;

#[derive(Debug, PartialEq)]
pub enum Error {
    InconsistentDomains {
        message_type_domain: Domain,
        domain: Domain,
    },
    Web3SignerRequestFailed(String),
    Web3SignerJsonParsingFailed(String),
    ShuttingDown,
    TokioJoin(String),
    MergeForkNotSupported,
    GenesisForkVersionRequired,
}

/// Enumerates all messages that can be signed by a validator.
pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload<E> = FullPayload<E>> {
    RandaoReveal(Epoch),
    BeaconBlock(&'a BeaconBlock<E, Payload>),
    AttestationData(&'a AttestationData),
    SignedAggregateAndProof(AggregateAndProofRef<'a, E>),
    SelectionProof(Slot),
    SyncSelectionProof(&'a SyncAggregatorSelectionData),
    SyncCommitteeSignature {
        beacon_block_root: Hash256,
        slot: Slot,
    },
    SignedContributionAndProof(&'a ContributionAndProof<E>),
    ValidatorRegistration(&'a ValidatorRegistrationData),
    VoluntaryExit(&'a VoluntaryExit),
    ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope<E>),
    PayloadAttestationData(&'a PayloadAttestationData),
    ProposerPreferences(&'a ProposerPreferences),
}

impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignableMessage<'_, E, Payload> {
    /// Returns the `SignedRoot` for the contained message.
    ///
    /// The actual `SignedRoot` trait is not used since it also requires a `TreeHash` impl, which is
    /// not required here.
    pub fn signing_root(&self, domain: Hash256) -> Hash256 {
        match self {
            SignableMessage::RandaoReveal(epoch) => epoch.signing_root(domain),
            SignableMessage::BeaconBlock(b) => b.signing_root(domain),
            SignableMessage::AttestationData(a) => a.signing_root(domain),
            SignableMessage::SignedAggregateAndProof(a) => a.signing_root(domain),
            SignableMessage::SelectionProof(slot) => slot.signing_root(domain),
            SignableMessage::SyncSelectionProof(s) => s.signing_root(domain),
            SignableMessage::SyncCommitteeSignature {
                beacon_block_root, ..
            } => beacon_block_root.signing_root(domain),
            SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain),
            SignableMessage::ValidatorRegistration(v) => v.signing_root(domain),
            SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain),
            SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain),
            SignableMessage::PayloadAttestationData(d) => d.signing_root(domain),
            SignableMessage::ProposerPreferences(p) => p.signing_root(domain),
        }
    }
}

/// A method used by a validator to sign messages.
///
/// Presently there is only a single variant, however we expect more variants to arise (e.g.,
/// remote signing).
pub enum SigningMethod {
    /// A validator that is defined by an EIP-2335 keystore on the local filesystem.
    LocalKeystore {
        voting_keystore_path: PathBuf,
        voting_keystore_lockfile: Mutex<Option<Lockfile>>,
        voting_keystore: Keystore,
        voting_keypair: Arc<Keypair>,
    },
    /// A validator that defers to a Web3Signer server for signing.
    ///
    /// See: https://docs.web3signer.consensys.net/en/latest/
    Web3Signer {
        signing_url: Url,
        http_client: Client,
        voting_public_key: PublicKey,
    },
}

/// The additional information used to construct a signature. Mostly used for protection from replay
/// attacks.
pub struct SigningContext {
    pub domain: Domain,
    pub epoch: Epoch,
    pub fork: Fork,
    pub genesis_validators_root: Hash256,
}

impl SigningContext {
    /// Returns the `Hash256` to be mixed-in with the signature.
    pub fn domain_hash(&self, spec: &ChainSpec) -> Hash256 {
        spec.get_domain(
            self.epoch,
            self.domain,
            &self.fork,
            self.genesis_validators_root,
        )
    }
}

impl SigningMethod {
    /// Return whether this signing method requires local slashing protection.
    pub fn requires_local_slashing_protection(
        &self,
        enable_web3signer_slashing_protection: bool,
    ) -> bool {
        match self {
            // Slashing protection is ALWAYS required for local keys. DO NOT TURN THIS OFF.
            SigningMethod::LocalKeystore { .. } => true,
            // Slashing protection is only required for remote signer keys when the configuration
            // dictates that it is desired.
            SigningMethod::Web3Signer { .. } => enable_web3signer_slashing_protection,
        }
    }

    /// Return the signature of `signable_message`, with respect to the `signing_context`.
    #[instrument(skip_all, level = "debug")]
    pub async fn get_signature<E: EthSpec, Payload: AbstractExecPayload<E>>(
        &self,
        signable_message: SignableMessage<'_, E, Payload>,
        signing_context: SigningContext,
        spec: &ChainSpec,
        executor: &TaskExecutor,
    ) -> Result<Signature, Error> {
        let domain_hash = signing_context.domain_hash(spec);
        let SigningContext {
            fork,
            genesis_validators_root,
            ..
        } = signing_context;

        let signing_root = signable_message.signing_root(domain_hash);

        let fork_info = Some(ForkInfo {
            fork,
            genesis_validators_root,
        });

        self.get_signature_from_root::<E, Payload>(
            signable_message,
            signing_root,
            executor,
            fork_info,
        )
        .await
    }

    pub async fn get_signature_from_root<E: EthSpec, Payload: AbstractExecPayload<E>>(
        &self,
        signable_message: SignableMessage<'_, E, Payload>,
        signing_root: Hash256,
        executor: &TaskExecutor,
        fork_info: Option<ForkInfo>,
    ) -> Result<Signature, Error> {
        match self {
            SigningMethod::LocalKeystore { voting_keypair, .. } => {
                let _timer = validator_metrics::start_timer_vec(
                    &validator_metrics::SIGNING_TIMES,
                    &[validator_metrics::LOCAL_KEYSTORE],
                );

                let voting_keypair = voting_keypair.clone();
                // Spawn a blocking task to produce the signature. This avoids blocking the core
                // tokio executor.
                //
                // We are using the Rayon high-priority pool which uses up to 80% of available
                // threads. In future we could consider using 90-100% in the VC, seeing as we have
                // very little other work to do aside from signing.
                let signature = executor
                    .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || {
                        voting_keypair.sk.sign(signing_root)
                    })
                    .await
                    .map_err(|_| Error::ShuttingDown)?;
                Ok(signature)
            }
            SigningMethod::Web3Signer {
                signing_url,
                http_client,
                ..
            } => {
                let _timer = validator_metrics::start_timer_vec(
                    &validator_metrics::SIGNING_TIMES,
                    &[validator_metrics::WEB3SIGNER],
                );

                // Map the message into a Web3Signer type.
                let object = match signable_message {
                    SignableMessage::RandaoReveal(epoch) => {
                        Web3SignerObject::RandaoReveal { epoch }
                    }
                    SignableMessage::BeaconBlock(block) => Web3SignerObject::beacon_block(block)?,
                    SignableMessage::AttestationData(a) => Web3SignerObject::Attestation(a),
                    SignableMessage::SignedAggregateAndProof(a) => {
                        Web3SignerObject::AggregateAndProof(a)
                    }
                    SignableMessage::SelectionProof(slot) => {
                        Web3SignerObject::AggregationSlot { slot }
                    }
                    SignableMessage::SyncSelectionProof(s) => {
                        Web3SignerObject::SyncAggregatorSelectionData(s)
                    }
                    SignableMessage::SyncCommitteeSignature {
                        beacon_block_root,
                        slot,
                    } => Web3SignerObject::SyncCommitteeMessage {
                        beacon_block_root,
                        slot,
                    },
                    SignableMessage::SignedContributionAndProof(c) => {
                        Web3SignerObject::ContributionAndProof(c)
                    }
                    SignableMessage::ValidatorRegistration(v) => {
                        Web3SignerObject::ValidatorRegistration(v)
                    }
                    SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e),
                    SignableMessage::ExecutionPayloadEnvelope(e) => {
                        Web3SignerObject::ExecutionPayloadEnvelope(e)
                    }
                    SignableMessage::PayloadAttestationData(d) => {
                        Web3SignerObject::PayloadAttestationData(d)
                    }
                    SignableMessage::ProposerPreferences(p) => {
                        Web3SignerObject::ProposerPreferences(p)
                    }
                };

                // Determine the Web3Signer message type.
                let message_type = object.message_type();
                if matches!(message_type, MessageType::ValidatorRegistration) && fork_info.is_some()
                {
                    return Err(Error::GenesisForkVersionRequired);
                }

                let request = SigningRequest {
                    message_type,
                    fork_info,
                    signing_root,
                    object,
                };

                // Request a signature from the Web3Signer instance via HTTP(S).
                let response: SigningResponse = http_client
                    .post(signing_url.clone())
                    .header(ACCEPT, "application/json")
                    .json(&request)
                    .send()
                    .await
                    .map_err(|e| Error::Web3SignerRequestFailed(e.to_string()))?
                    .error_for_status()
                    .map_err(|e| Error::Web3SignerRequestFailed(e.to_string()))?
                    .json()
                    .await
                    .map_err(|e| Error::Web3SignerJsonParsingFailed(e.to_string()))?;

                Ok(response.signature)
            }
        }
    }
}
