use crate::BeaconChain;
use crate::BeaconChainTypes;
use eth2::types::GraffitiPolicy;
use execution_layer::{CommitPrefix, ExecutionLayer, http::ENGINE_GET_CLIENT_VERSION_V1};
use logging::crit;
use serde::{Deserialize, Serialize};
use slot_clock::SlotClock;
use std::{fmt::Debug, time::Duration};
use task_executor::TaskExecutor;
use tracing::{debug, error, warn};
use types::{EthSpec, GRAFFITI_BYTES_LEN, Graffiti};

const ENGINE_VERSION_AGE_LIMIT_EPOCH_MULTIPLE: u32 = 6; // 6 epochs
const ENGINE_VERSION_CACHE_REFRESH_EPOCH_MULTIPLE: u32 = 2; // 2 epochs
const ENGINE_VERSION_CACHE_PRELOAD_STARTUP_DELAY: Duration = Duration::from_secs(60);

/// Represents the source and content of graffiti for block production, excluding
/// inputs from the validator client and execution engine. Graffiti is categorized
/// as either user-specified or calculated to facilitate decisions on graffiti
/// selection.
#[derive(Clone, Copy, Serialize, Deserialize)]
pub enum GraffitiOrigin {
    UserSpecified(Graffiti),
    Calculated(Graffiti),
}

impl GraffitiOrigin {
    pub fn graffiti(&self) -> Graffiti {
        match self {
            GraffitiOrigin::UserSpecified(graffiti) => *graffiti,
            GraffitiOrigin::Calculated(graffiti) => *graffiti,
        }
    }
}

impl Default for GraffitiOrigin {
    fn default() -> Self {
        let version_bytes = lighthouse_version::VERSION.as_bytes();
        let trimmed_len = std::cmp::min(version_bytes.len(), GRAFFITI_BYTES_LEN);
        let mut bytes = [0u8; GRAFFITI_BYTES_LEN];
        bytes[..trimmed_len].copy_from_slice(&version_bytes[..trimmed_len]);
        Self::Calculated(Graffiti::from(bytes))
    }
}

impl Debug for GraffitiOrigin {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.graffiti().fmt(f)
    }
}

pub enum GraffitiSettings {
    Unspecified,
    Specified {
        graffiti: Graffiti,
        policy: GraffitiPolicy,
    },
}

impl GraffitiSettings {
    pub fn new(validator_graffiti: Option<Graffiti>, policy: Option<GraffitiPolicy>) -> Self {
        validator_graffiti
            .map(|graffiti| Self::Specified {
                graffiti,
                policy: policy.unwrap_or_default(),
            })
            .unwrap_or(Self::Unspecified)
    }
}

pub struct GraffitiCalculator<T: BeaconChainTypes> {
    pub beacon_graffiti: GraffitiOrigin,
    execution_layer: Option<ExecutionLayer<T::EthSpec>>,
    pub epoch_duration: Duration,
}

impl<T: BeaconChainTypes> GraffitiCalculator<T> {
    pub fn new(
        beacon_graffiti: GraffitiOrigin,
        execution_layer: Option<ExecutionLayer<T::EthSpec>>,
        epoch_duration: Duration,
    ) -> Self {
        Self {
            beacon_graffiti,
            execution_layer,
            epoch_duration,
        }
    }

    /// Returns the appropriate graffiti to use for block production, prioritizing
    /// sources in the following order:
    /// 1. Graffiti specified by the validator client.
    /// 2. Graffiti specified by the user via beacon node CLI options.
    /// 3. The EL & CL client version string, applicable when the EL supports version specification.
    /// 4. The default lighthouse version string, used if the EL lacks version specification support.
    pub async fn get_graffiti(&self, graffiti_settings: GraffitiSettings) -> Graffiti {
        match graffiti_settings {
            GraffitiSettings::Specified { graffiti, policy } => match policy {
                GraffitiPolicy::PreserveUserGraffiti => graffiti,
                GraffitiPolicy::AppendClientVersions => {
                    self.calculate_combined_graffiti(Some(graffiti)).await
                }
            },
            GraffitiSettings::Unspecified => self.calculate_combined_graffiti(None).await,
        }
    }

    async fn calculate_combined_graffiti(&self, validator_graffiti: Option<Graffiti>) -> Graffiti {
        match self.beacon_graffiti {
            GraffitiOrigin::UserSpecified(graffiti) => graffiti,
            GraffitiOrigin::Calculated(default_graffiti) => {
                let Some(execution_layer) = self.execution_layer.as_ref() else {
                    // Return default graffiti if there is no execution layer. This
                    // shouldn't occur if we're actually producing blocks.
                    crit!(
                        "No execution layer available for graffiti calculation during block production!"
                    );
                    return default_graffiti;
                };

                // The engine version cache refresh service ensures this will almost always retrieve this data from the
                // cache instead of making a request to the execution engine. A cache miss would only occur if lighthouse
                // has recently started or the EL recently went offline.
                let engine_versions = match execution_layer
                    .get_engine_version(Some(
                        self.epoch_duration * ENGINE_VERSION_AGE_LIMIT_EPOCH_MULTIPLE,
                    ))
                    .await
                {
                    Ok(engine_versions) => engine_versions,
                    Err(el_error) => {
                        warn!(error = ?el_error, "Failed to determine execution engine version for graffiti");
                        return default_graffiti;
                    }
                };

                let Some(engine_version) = engine_versions.first() else {
                    // Got an empty array which indicates the EL doesn't support the method
                    debug!(
                        "Using default lighthouse graffiti: EL does not support {} method",
                        ENGINE_GET_CLIENT_VERSION_V1
                    );
                    return default_graffiti;
                };
                if engine_versions.len() != 1 {
                    // More than one version implies lighthouse is connected to
                    // an EL multiplexer. We don't support modifying the graffiti
                    // with these configurations.
                    warn!("Execution Engine multiplexer detected, using default graffiti");
                    return default_graffiti;
                }

                let lighthouse_commit_prefix =
                    CommitPrefix::try_from(lighthouse_version::COMMIT_PREFIX.to_string())
                        .unwrap_or_else(|error_message| {
                            // This really shouldn't happen but we want to definitly log if it does
                            crit!(
                                error = error_message,
                                "Failed to parse lighthouse commit prefix"
                            );
                            CommitPrefix("00000000".to_string())
                        });

                engine_version.calculate_graffiti(lighthouse_commit_prefix, validator_graffiti)
            }
        }
    }
}

pub fn start_engine_version_cache_refresh_service<T: BeaconChainTypes>(
    chain: &BeaconChain<T>,
    executor: TaskExecutor,
) {
    let Some(el_ref) = chain.execution_layer.as_ref() else {
        debug!("No execution layer configured, not starting engine version cache refresh service");
        return;
    };
    if matches!(
        chain.graffiti_calculator.beacon_graffiti,
        GraffitiOrigin::UserSpecified(_)
    ) {
        debug!("Graffiti is user-specified, not starting engine version cache refresh service");
        return;
    }

    let execution_layer = el_ref.clone();
    let slot_clock = chain.slot_clock.clone();
    let epoch_duration = chain.graffiti_calculator.epoch_duration;
    executor.spawn(
        async move {
            engine_version_cache_refresh_service::<T>(execution_layer, slot_clock, epoch_duration)
                .await
        },
        "engine_version_cache_refresh_service",
    );
}

async fn engine_version_cache_refresh_service<T: BeaconChainTypes>(
    execution_layer: ExecutionLayer<T::EthSpec>,
    slot_clock: T::SlotClock,
    epoch_duration: Duration,
) {
    // Preload the engine version cache after a brief delay to allow for EL initialization.
    // This initial priming ensures cache readiness before the service's regular update cycle begins.
    tokio::time::sleep(ENGINE_VERSION_CACHE_PRELOAD_STARTUP_DELAY).await;
    if let Err(e) = execution_layer.get_engine_version(None).await {
        debug!(
            error = ?e,
            "Failed to preload engine version cache"
        );
    }

    // this service should run 3/8 of the way through the epoch
    let epoch_delay = (epoch_duration * 3) / 8;
    // the duration of 1 epoch less than the total duration between firing of this service
    let partial_firing_delay =
        epoch_duration * ENGINE_VERSION_CACHE_REFRESH_EPOCH_MULTIPLE.saturating_sub(1);
    loop {
        match slot_clock.duration_to_next_epoch(T::EthSpec::slots_per_epoch()) {
            Some(duration_to_next_epoch) => {
                let firing_delay = partial_firing_delay + duration_to_next_epoch + epoch_delay;
                tokio::time::sleep(firing_delay).await;

                debug!("Engine version cache refresh service firing");

                match execution_layer.get_engine_version(None).await {
                    Err(e) => warn!( error = ?e, "Failed to populate engine version cache"),
                    Ok(versions) => {
                        if versions.is_empty() {
                            // Empty array indicates the EL doesn't support the method
                            debug!(
                                "EL does not support {} method. Sleeping twice as long before retry",
                                ENGINE_GET_CLIENT_VERSION_V1
                            );
                            tokio::time::sleep(
                                epoch_duration * ENGINE_VERSION_CACHE_REFRESH_EPOCH_MULTIPLE,
                            )
                            .await;
                        }
                    }
                }
            }
            None => {
                error!("Failed to read slot clock");
                // If we can't read the slot clock, just wait another slot.
                tokio::time::sleep(slot_clock.slot_duration()).await;
            }
        };
    }
}

#[cfg(test)]
mod tests {
    use crate::ChainConfig;
    use crate::graffiti_calculator::GraffitiSettings;
    use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec};
    use bls::Keypair;
    use eth2::types::GraffitiPolicy;
    use execution_layer::EngineCapabilities;
    use execution_layer::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_ENGINE_CAPABILITIES};
    use std::sync::Arc;
    use std::sync::LazyLock;
    use std::time::Duration;
    use tracing::info;
    use types::{ChainSpec, GRAFFITI_BYTES_LEN, Graffiti, MinimalEthSpec};

    const VALIDATOR_COUNT: usize = 48;
    /// A cached set of keys.
    static KEYPAIRS: LazyLock<Vec<Keypair>> =
        LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT));

    fn get_harness(
        validator_count: usize,
        spec: Arc<ChainSpec>,
        chain_config: Option<ChainConfig>,
    ) -> BeaconChainHarness<EphemeralHarnessType<MinimalEthSpec>> {
        let harness = BeaconChainHarness::builder(MinimalEthSpec)
            .spec(spec)
            .chain_config(chain_config.unwrap_or_default())
            .keypairs(KEYPAIRS[0..validator_count].to_vec())
            .fresh_ephemeral_store()
            .mock_execution_layer()
            .build();

        harness.advance_slot();

        harness
    }

    #[tokio::test]
    async fn check_graffiti_without_el_version_support() {
        let spec = Arc::new(test_spec::<MinimalEthSpec>());
        let harness = get_harness(VALIDATOR_COUNT, spec, None);
        // modify execution engine so it doesn't support engine_getClientVersionV1 method
        let mock_execution_layer = harness.mock_execution_layer.as_ref().unwrap();
        mock_execution_layer
            .server
            .set_engine_capabilities(EngineCapabilities {
                get_client_version_v1: false,
                ..DEFAULT_ENGINE_CAPABILITIES
            });
        // refresh capabilities cache
        harness
            .chain
            .execution_layer
            .as_ref()
            .unwrap()
            .get_engine_capabilities(Some(Duration::ZERO))
            .await
            .unwrap();

        let version_bytes = std::cmp::min(lighthouse_version::VERSION.len(), GRAFFITI_BYTES_LEN);
        // grab the slice of the graffiti that corresponds to the lighthouse version
        let graffiti_slice = &harness
            .chain
            .graffiti_calculator
            .get_graffiti(GraffitiSettings::Unspecified)
            .await
            .0[..version_bytes];

        // convert graffiti bytes slice to ascii for easy debugging if this test should fail
        let graffiti_str =
            std::str::from_utf8(graffiti_slice).expect("bytes should convert nicely to ascii");

        info!(
            lighthouse_version = lighthouse_version::VERSION,
            graffiti_str, "results"
        );
        println!("lighthouse_version: '{}'", lighthouse_version::VERSION);
        println!("graffiti_str:       '{}'", graffiti_str);

        assert!(lighthouse_version::VERSION.starts_with(graffiti_str));
    }

    #[tokio::test]
    async fn check_graffiti_with_el_version_support() {
        let spec = Arc::new(test_spec::<MinimalEthSpec>());
        let harness = get_harness(VALIDATOR_COUNT, spec, None);

        let found_graffiti_bytes = harness
            .chain
            .graffiti_calculator
            .get_graffiti(GraffitiSettings::Unspecified)
            .await
            .0;

        let mock_commit = DEFAULT_CLIENT_VERSION.commit.clone();
        let expected_graffiti_string = format!(
            "{}{}{}{}",
            DEFAULT_CLIENT_VERSION.code,
            mock_commit
                .strip_prefix("0x")
                .unwrap_or(&mock_commit)
                .get(0..4)
                .expect("should get first 2 bytes in hex"),
            "LH",
            lighthouse_version::COMMIT_PREFIX
                .get(0..4)
                .expect("should get first 2 bytes in hex")
        );

        let expected_graffiti_prefix_bytes = expected_graffiti_string.as_bytes();
        let expected_graffiti_prefix_len =
            std::cmp::min(expected_graffiti_prefix_bytes.len(), GRAFFITI_BYTES_LEN);

        let found_graffiti_string =
            std::str::from_utf8(&found_graffiti_bytes[..expected_graffiti_prefix_len])
                .expect("bytes should convert nicely to ascii");

        info!(expected_graffiti_string, found_graffiti_string, "results");
        println!("expected_graffiti_string: '{}'", expected_graffiti_string);
        println!("found_graffiti_string:    '{}'", found_graffiti_string);

        assert_eq!(expected_graffiti_string, found_graffiti_string);

        let mut expected_graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN];
        expected_graffiti_bytes[..expected_graffiti_prefix_len]
            .copy_from_slice(expected_graffiti_string.as_bytes());
        assert_eq!(found_graffiti_bytes, expected_graffiti_bytes);
    }

    #[tokio::test]
    async fn check_graffiti_with_validator_specified_value() {
        let spec = Arc::new(test_spec::<MinimalEthSpec>());
        let harness = get_harness(VALIDATOR_COUNT, spec, None);

        let graffiti_str = "nice graffiti bro";
        let mut graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN];
        graffiti_bytes[..graffiti_str.len()].copy_from_slice(graffiti_str.as_bytes());

        let found_graffiti = harness
            .chain
            .graffiti_calculator
            .get_graffiti(GraffitiSettings::new(
                Some(Graffiti::from(graffiti_bytes)),
                Some(GraffitiPolicy::PreserveUserGraffiti),
            ))
            .await;

        assert_eq!(
            found_graffiti.to_string(),
            "0x6e6963652067726166666974692062726f000000000000000000000000000000"
        );
    }

    #[tokio::test]
    async fn check_append_el_version_graffiti_various_length() {
        let spec = Arc::new(test_spec::<MinimalEthSpec>());
        let harness = get_harness(VALIDATOR_COUNT, spec, None);

        let graffiti_vec = vec![
            // less than 20 characters, example below is 19 characters
            "This is my graffiti",
            // 20-23 characters, example below is 22 characters
            "This is my graffiti yo",
            // 24-27 characters, example below is 26 characters
            "This is my graffiti string",
            // 28-29 characters, example below is 29 characters
            "This is my graffiti string yo",
            // 30-32 characters, example below is 32 characters
            "This is my graffiti string yo yo",
        ];

        for graffiti in graffiti_vec {
            let mut graffiti_bytes = [0; GRAFFITI_BYTES_LEN];
            graffiti_bytes[..graffiti.len()].copy_from_slice(graffiti.as_bytes());

            // To test appending client version info with user specified graffiti
            let policy = GraffitiPolicy::AppendClientVersions;
            let found_graffiti_bytes = harness
                .chain
                .graffiti_calculator
                .get_graffiti(GraffitiSettings::Specified {
                    graffiti: Graffiti::from(graffiti_bytes),
                    policy,
                })
                .await
                .0;

            let mock_commit = DEFAULT_CLIENT_VERSION.commit.clone();

            let graffiti_length = graffiti.len();
            let append_graffiti_string = match graffiti_length {
                0..=19 => format!(
                    "{}{}{}{}",
                    DEFAULT_CLIENT_VERSION.code,
                    mock_commit
                        .strip_prefix("0x")
                        .unwrap_or(&mock_commit)
                        .get(0..4)
                        .expect("should get first 2 bytes in hex"),
                    "LH",
                    lighthouse_version::COMMIT_PREFIX
                        .get(0..4)
                        .expect("should get first 2 bytes in hex")
                ),
                20..=23 => format!(
                    "{}{}{}{}",
                    DEFAULT_CLIENT_VERSION.code,
                    mock_commit
                        .strip_prefix("0x")
                        .unwrap_or(&mock_commit)
                        .get(0..2)
                        .expect("should get first 2 bytes in hex"),
                    "LH",
                    lighthouse_version::COMMIT_PREFIX
                        .get(0..2)
                        .expect("should get first 2 bytes in hex")
                ),
                24..=27 => format!("{}{}", DEFAULT_CLIENT_VERSION.code, "LH",),
                28..=29 => DEFAULT_CLIENT_VERSION.code.to_string(),
                // when user graffiti length is 30-32 characters, append nothing
                30..=32 => String::new(),
                _ => panic!(
                    "graffiti length should be less than or equal to GRAFFITI_BYTES_LEN (32 characters)"
                ),
            };

            let expected_graffiti_string = if append_graffiti_string.is_empty() {
                // for the case of empty append_graffiti_string, i.e., user-specified graffiti is 30-32 characters
                graffiti.to_string()
            } else {
                // There is a space between the user graffiti and client version info
                // as defined in calculate_graffiti function in engine_api.rs
                format!("{} {}", graffiti, append_graffiti_string)
            };

            let expected_graffiti_prefix_bytes = expected_graffiti_string.as_bytes();
            let expected_graffiti_prefix_len =
                std::cmp::min(expected_graffiti_prefix_bytes.len(), GRAFFITI_BYTES_LEN);

            let found_graffiti_string =
                std::str::from_utf8(&found_graffiti_bytes[..expected_graffiti_prefix_len])
                    .expect("bytes should convert nicely to ascii");

            assert_eq!(expected_graffiti_string, found_graffiti_string);
        }
    }
}
