//! # Transition Blocks
//!
//! Use this tool to apply a `SignedBeaconBlock` to a `BeaconState`. Useful for benchmarking or
//! troubleshooting consensus failures.
//!
//! It can load states and blocks from file or pull them from a beaconAPI. Objects pulled from a
//! beaconAPI can be saved to disk to reduce future calls to that server.
//!
//! Logging output is controlled via the `RUST_LOG` environment variable. For example, `export
//! RUST_LOG=debug`.
//!
//! ## Examples
//!
//! ### Run using a block from a beaconAPI
//!
//! Download the 0x6c69 block and its pre-state (the state from its parent block) from the
//! beaconAPI. Advance the pre-state to the slot of the 0x6c69 block and apply that block to the
//! pre-state.
//!
//! ```ignore
//! lcli transition-blocks \
//!     --beacon-url http://localhost:5052 \
//!     --block-id 0x6c69cf50a451f1ec905e954bf1fa22970f371a72a5aa9f8e3a43a18fdd980bec \
//!     --runs 10
//! ```
//!
//! ### Download a block and pre-state from a beaconAPI to the filesystem
//!
//! Download a block and pre-state to the filesystem, without performing any transitions:
//!
//! ```ignore
//! lcli transition-blocks \
//!     --beacon-url http://localhost:5052 \
//!     --block-id 0x6c69cf50a451f1ec905e954bf1fa22970f371a72a5aa9f8e3a43a18fdd980bec \
//!     --runs 0 \
//!     --block-output-path /tmp/block-0x6c69.ssz \
//!     --pre-state-output-path /tmp/pre-state-0x6c69.ssz
//! ```
//!
//! ### Use a block and pre-state from the filesystem
//!
//! Do one run over the block and pre-state downloaded in the previous example and save the post
//! state to file:
//!
//! ```ignore
//! lcli transition-blocks \
//!     --block-path /tmp/block-0x6c69.ssz \
//!     --pre-state-path /tmp/pre-state-0x6c69.ssz
//!     --post-state-output-path /tmp/post-state-0x6c69.ssz
//! ```
//!
//! ### Isolate block processing for benchmarking
//!
//! Try to isolate block processing as much as possible for benchmarking:
//!
//! ```ignore
//! lcli transition-blocks \
//!     --block-path /tmp/block-0x6c69.ssz \
//!     --pre-state-path /tmp/pre-state-0x6c69.ssz \
//!     --runs 10 \
//!     --exclude-cache-builds \
//!     --exclude-post-block-thc
//! ```
use beacon_chain::{
    test_utils::EphemeralHarnessType, validator_pubkey_cache::ValidatorPubkeyCache,
};
use clap::ArgMatches;
use clap_utils::{parse_optional, parse_required};
use environment::Environment;
use eth2::{
    BeaconNodeHttpClient, SensitiveUrl, Timeouts,
    types::{BlockId, StateId},
};
use eth2_network_config::Eth2NetworkConfig;
use ssz::Encode;
use state_processing::state_advance::complete_state_advance;
use state_processing::{
    AllCaches, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot,
    block_signature_verifier::BlockSignatureVerifier, per_block_processing,
};
use std::borrow::Cow;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use store::HotColdDB;
use tracing::{debug, info};
use types::{BeaconState, ChainSpec, EthSpec, Hash256, SignedBeaconBlock};

const HTTP_TIMEOUT: Duration = Duration::from_secs(10);

#[derive(Debug)]
struct Config {
    no_signature_verification: bool,
    exclude_cache_builds: bool,
    exclude_post_block_thc: bool,
}

pub fn run<E: EthSpec>(
    env: Environment<E>,
    network_config: Eth2NetworkConfig,
    matches: &ArgMatches,
) -> Result<(), String> {
    let spec = Arc::new(network_config.chain_spec::<E>()?);
    let executor = env.core_context().executor;

    /*
     * Parse (most) CLI arguments.
     */

    let pre_state_path: Option<PathBuf> = parse_optional(matches, "pre-state-path")?;
    let block_path: Option<PathBuf> = parse_optional(matches, "block-path")?;
    let post_state_output_path: Option<PathBuf> =
        parse_optional(matches, "post-state-output-path")?;
    let pre_state_output_path: Option<PathBuf> = parse_optional(matches, "pre-state-output-path")?;
    let block_output_path: Option<PathBuf> = parse_optional(matches, "block-output-path")?;
    let beacon_url: Option<SensitiveUrl> = parse_optional(matches, "beacon-url")?;
    let runs: usize = parse_required(matches, "runs")?;
    let config = Config {
        no_signature_verification: matches.get_flag("no-signature-verification"),
        exclude_cache_builds: matches.get_flag("exclude-cache-builds"),
        exclude_post_block_thc: matches.get_flag("exclude-post-block-thc"),
    };

    info!("Using {} spec", E::spec_name());
    info!("Doing {} runs", runs);
    info!("{:?}", &config);

    /*
     * Load the block and pre-state from disk or beaconAPI URL.
     */

    let (mut pre_state, mut state_root_opt, block) = match (pre_state_path, block_path, beacon_url)
    {
        (Some(pre_state_path), Some(block_path), None) => {
            info!("Block path: {:?}", block_path);
            info!("Pre-state path: {:?}", pre_state_path);
            let pre_state =
                load_from_ssz_with(&pre_state_path, &spec, BeaconState::from_ssz_bytes)?;
            let block = load_from_ssz_with(&block_path, &spec, SignedBeaconBlock::from_ssz_bytes)?;
            (pre_state, None, block)
        }
        (None, None, Some(beacon_url)) => {
            let block_id: BlockId = parse_required(matches, "block-id")?;
            let client = BeaconNodeHttpClient::new(beacon_url, Timeouts::set_all(HTTP_TIMEOUT));
            let inner_spec = spec.clone();
            executor
                .handle()
                .ok_or("shutdown in progress")?
                .block_on(async move {
                    let block = client
                        .get_beacon_blocks(block_id)
                        .await
                        .map_err(|e| format!("Failed to download block: {:?}", e))?
                        .ok_or_else(|| format!("Unable to locate block at {:?}", block_id))?
                        .into_data();

                    if block.slot() == inner_spec.genesis_slot {
                        return Err("Cannot run on the genesis block".to_string());
                    }

                    let parent_block: SignedBeaconBlock<E> = client
                        .get_beacon_blocks(BlockId::Root(block.parent_root()))
                        .await
                        .map_err(|e| format!("Failed to download parent block: {:?}", e))?
                        .ok_or_else(|| format!("Unable to locate parent block at {:?}", block_id))?
                        .into_data();

                    let state_root = parent_block.state_root();
                    let state_id = StateId::Root(state_root);
                    let pre_state = client
                        .get_debug_beacon_states::<E>(state_id)
                        .await
                        .map_err(|e| format!("Failed to download state: {:?}", e))?
                        .ok_or_else(|| format!("Unable to locate state at {:?}", state_id))?
                        .into_data();

                    Ok((pre_state, Some(state_root), block))
                })
                .map_err(|e| format!("Failed to complete task: {:?}", e))?
        }
        _ => {
            return Err(
                "must supply *both* --pre-state-path and --block-path *or* only --beacon-url"
                    .into(),
            );
        }
    };

    // Compute the block root.
    let block_root = block.canonical_root();

    /*
     * Create a `BeaconStore` and `ValidatorPubkeyCache` for block signature verification.
     */

    let store = HotColdDB::open_ephemeral(<_>::default(), spec.clone())
        .map_err(|e| format!("Failed to create ephemeral store: {:?}", e))?;
    let store = Arc::new(store);

    debug!("Building pubkey cache (might take some time)");
    let validator_pubkey_cache = ValidatorPubkeyCache::new(&pre_state, store)
        .map_err(|e| format!("Failed to create pubkey cache: {:?}", e))?;

    /*
     * If cache builds are excluded from the timings, build them early so they are available for
     * each run.
     */

    if config.exclude_cache_builds {
        pre_state
            .build_all_caches(&spec)
            .map_err(|e| format!("Unable to build caches: {:?}", e))?;
        let state_root = pre_state
            .update_tree_hash_cache()
            .map_err(|e| format!("Unable to build THC: {:?}", e))?;

        if state_root_opt.is_some_and(|expected| expected != state_root) {
            return Err(format!(
                "State root mismatch! Expected {}, computed {}",
                state_root_opt.unwrap(),
                state_root
            ));
        }
        state_root_opt = Some(state_root);
    }

    /*
     * Perform the core "runs".
     */

    let mut output_post_state = None;
    let mut saved_ctxt = None;
    for i in 0..runs {
        let pre_state = pre_state.clone();
        let block = block.clone();

        let start = Instant::now();

        let post_state = do_transition(
            pre_state,
            block_root,
            block,
            state_root_opt,
            &config,
            &validator_pubkey_cache,
            &mut saved_ctxt,
            &spec,
        )?;

        let duration = Instant::now().duration_since(start);
        info!("Run {}: {:?}", i, duration);

        if output_post_state.is_none() {
            output_post_state = Some(post_state)
        }
    }

    /*
     * Write artifacts to disk, if required.
     */

    if let Some(path) = post_state_output_path {
        let output_post_state = output_post_state.ok_or_else(|| {
            format!(
                "Post state was not computed, cannot save to disk (runs = {})",
                runs
            )
        })?;

        let mut output_file =
            File::create(path).map_err(|e| format!("Unable to create output file: {:?}", e))?;

        output_file
            .write_all(&output_post_state.as_ssz_bytes())
            .map_err(|e| format!("Unable to write to output file: {:?}", e))?;
    }

    if let Some(path) = pre_state_output_path {
        let mut output_file =
            File::create(path).map_err(|e| format!("Unable to create output file: {:?}", e))?;

        output_file
            .write_all(&pre_state.as_ssz_bytes())
            .map_err(|e| format!("Unable to write to output file: {:?}", e))?;
    }

    if let Some(path) = block_output_path {
        let mut output_file =
            File::create(path).map_err(|e| format!("Unable to create output file: {:?}", e))?;

        output_file
            .write_all(&block.as_ssz_bytes())
            .map_err(|e| format!("Unable to write to output file: {:?}", e))?;
    }

    drop(pre_state);

    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn do_transition<E: EthSpec>(
    mut pre_state: BeaconState<E>,
    block_root: Hash256,
    block: SignedBeaconBlock<E>,
    mut state_root_opt: Option<Hash256>,
    config: &Config,
    validator_pubkey_cache: &ValidatorPubkeyCache<EphemeralHarnessType<E>>,
    saved_ctxt: &mut Option<ConsensusContext<E>>,
    spec: &ChainSpec,
) -> Result<BeaconState<E>, String> {
    if !config.exclude_cache_builds {
        let t = Instant::now();
        pre_state
            .build_all_caches(spec)
            .map_err(|e| format!("Unable to build caches: {:?}", e))?;
        debug!("Build caches: {:?}", t.elapsed());

        let t = Instant::now();
        let state_root = pre_state
            .update_tree_hash_cache()
            .map_err(|e| format!("Unable to build tree hash cache: {:?}", e))?;
        debug!("Initial tree hash: {:?}", t.elapsed());

        if state_root_opt.is_some_and(|expected| expected != state_root) {
            return Err(format!(
                "State root mismatch! Expected {}, computed {}",
                state_root_opt.unwrap(),
                state_root
            ));
        }
        state_root_opt = Some(state_root);
    }

    let state_root = state_root_opt.ok_or("Failed to compute state root, internal error")?;

    // Transition the parent state to the block slot.
    let t = Instant::now();
    complete_state_advance(&mut pre_state, Some(state_root), block.slot(), spec)
        .map_err(|e| format!("Unable to perform complete advance: {e:?}"))?;
    debug!("Slot processing: {:?}", t.elapsed());

    // Slot and epoch processing should keep the caches fully primed.
    assert!(pre_state.all_caches_built());

    let t = Instant::now();
    pre_state
        .build_all_caches(spec)
        .map_err(|e| format!("Unable to build caches: {:?}", e))?;
    debug!("Build all caches (again): {:?}", t.elapsed());

    let mut ctxt = if let Some(ctxt) = saved_ctxt {
        ctxt.clone()
    } else {
        ConsensusContext::new(pre_state.slot())
            .set_current_block_root(block_root)
            .set_proposer_index(block.message().proposer_index())
    };

    if !config.no_signature_verification {
        let get_pubkey = move |validator_index| {
            validator_pubkey_cache
                .get(validator_index)
                .map(Cow::Borrowed)
        };

        let decompressor = move |pk_bytes| {
            // Map compressed pubkey to validator index.
            let validator_index = validator_pubkey_cache.get_index(pk_bytes)?;
            // Map validator index to pubkey (respecting guard on unknown validators).
            get_pubkey(validator_index)
        };

        let t = Instant::now();
        BlockSignatureVerifier::verify_entire_block(
            &pre_state,
            get_pubkey,
            decompressor,
            &block,
            &mut ctxt,
            spec,
        )
        .map_err(|e| format!("Invalid block signature: {:?}", e))?;
        debug!("Batch verify block signatures: {:?}", t.elapsed());

        // Signature verification should prime the indexed attestation cache.
        assert_eq!(
            ctxt.num_cached_indexed_attestations(),
            block.message().body().attestations_len()
        );
    }

    let t = Instant::now();
    per_block_processing(
        &mut pre_state,
        &block,
        BlockSignatureStrategy::NoVerification,
        VerifyBlockRoot::True,
        &mut ctxt,
        spec,
    )
    .map_err(|e| format!("State transition failed: {:?}", e))?;
    debug!("Process block: {:?}", t.elapsed());

    if !config.exclude_post_block_thc {
        let t = Instant::now();
        pre_state
            .update_tree_hash_cache()
            .map_err(|e| format!("Unable to build tree hash cache: {:?}", e))?;
        debug!("Post-block tree hash: {:?}", t.elapsed());
    }

    Ok(pre_state)
}

pub fn load_from_ssz_with<T>(
    path: &Path,
    spec: &ChainSpec,
    decoder: impl FnOnce(&[u8], &ChainSpec) -> Result<T, ssz::DecodeError>,
) -> Result<T, String> {
    let mut file =
        File::open(path).map_err(|e| format!("Unable to open file {:?}: {:?}", path, e))?;
    let mut bytes = vec![];
    file.read_to_end(&mut bytes)
        .map_err(|e| format!("Unable to read from file {:?}: {:?}", path, e))?;
    let t = Instant::now();
    let result = decoder(&bytes, spec).map_err(|e| format!("Ssz decode failed: {:?}", e));
    debug!("SSZ decoding {}: {:?}", path.display(), t.elapsed());
    result
}
