//! The main bootnode server execution.

use super::BootNodeConfig;
use crate::config::BootNodeConfigSerialization;
use clap::ArgMatches;
use eth2_network_config::Eth2NetworkConfig;
use lighthouse_network::{
    Eth2Enr,
    discv5::{self, Discv5, enr::NodeId},
};
use network_utils::enr_ext::EnrExt;
use tracing::{info, warn};
use types::EthSpec;

pub async fn run<E: EthSpec>(
    lh_matches: &ArgMatches,
    bn_matches: &ArgMatches,
    eth2_network_config: &Eth2NetworkConfig,
) -> Result<(), String> {
    // parse the CLI args into a useable config
    let config: BootNodeConfig<E> = BootNodeConfig::new(bn_matches, eth2_network_config).await?;

    // Dump configs if `dump-config` or `dump-chain-config` flags are set
    let config_sz = BootNodeConfigSerialization::from_config_ref(&config);
    clap_utils::check_dump_configs::<_, E>(
        lh_matches,
        &config_sz,
        &eth2_network_config.chain_spec::<E>()?,
    )?;

    if lh_matches.get_flag("immediate-shutdown") {
        return Ok(());
    }

    let BootNodeConfig {
        boot_nodes,
        local_enr,
        local_key,
        discv5_config,
        ..
    } = config;

    // Print out useful information about the generated ENR

    let enr_v4_socket = local_enr.udp4_socket();
    let enr_v6_socket = local_enr.udp6_socket();
    let eth2_field = local_enr
        .eth2()
        .map(|fork_id| hex::encode(fork_id.fork_digest))
        .unwrap_or_default();

    let pretty_v4_socket = enr_v4_socket.as_ref().map(|addr| addr.to_string());
    let pretty_v6_socket = enr_v6_socket.as_ref().map(|addr| addr.to_string());
    info!(
        listening_address = ?discv5_config.listen_config,
        advertised_v4_address = ?pretty_v4_socket,
        advertised_v6_address = ?pretty_v6_socket,
        eth2 = eth2_field,
        "Configuration parameters"
    );

    info!(peer_id = %local_enr.peer_id(), node_id = %local_enr.node_id(), "Identity established");

    // build the contactable multiaddr list, adding the p2p protocol
    info!(enr = local_enr.to_base64(), "Contact information");
    info!(enr = ?local_enr, "Enr details");
    info!(multiaddrs = ?local_enr.multiaddr_p2p(), "Contact information");

    // construct the discv5 server
    let mut discv5: Discv5 = Discv5::new(local_enr.clone(), local_key, discv5_config).unwrap();

    // If there are any bootnodes add them to the routing table
    for enr in boot_nodes {
        info!(
            ipv4_address = ?enr.udp4_socket(),
            ipv6_address = ?enr.udp6_socket(),
            peer_id = ?enr.peer_id(),
            node_id = ?enr.node_id(),
            "Adding bootnode"
        );
        if enr != local_enr
            && let Err(e) = discv5.add_enr(enr)
        {
            warn!(error = ?e, "Failed adding ENR");
        }
    }

    // start the server
    if let Err(e) = discv5.start().await {
        return Err(format!("Could not start discv5 server: {e:?}"));
    }

    // if there are peers in the local routing table, establish a session by running a query
    if !discv5.table_entries_id().is_empty() {
        info!("Executing bootstrap query...");
        let _ = discv5.find_node(NodeId::random()).await;
    }

    // respond with metrics every 10 seconds
    let mut metric_interval = tokio::time::interval(tokio::time::Duration::from_secs(10));

    // get an event stream
    let mut event_stream = match discv5.event_stream().await {
        Ok(stream) => stream,
        Err(e) => {
            return Err(format!("Failed to obtain event stream: {e:?}"));
        }
    };

    // listen for events
    loop {
        tokio::select! {
            _ = metric_interval.tick() => {
                // Get some ipv4/ipv6 stats to add in the metrics.
                let mut ipv4_only_reachable: usize = 0;
                let mut ipv6_only_reachable: usize= 0;
                let mut ipv4_ipv6_reachable: usize = 0;
                let mut unreachable_nodes: usize = 0;
                for enr in discv5.kbuckets().iter_ref().filter_map(|entry| entry.status.is_connected().then_some(entry.node.value)) {
                    let declares_ipv4 = enr.udp4_socket().is_some();
                    let declares_ipv6 = enr.udp6_socket().is_some();
                    match (declares_ipv4, declares_ipv6) {
                        (true, true) => ipv4_ipv6_reachable += 1,
                        (true, false) => ipv4_only_reachable += 1,
                        (false, true) => ipv6_only_reachable += 1,
                        (false, false) => unreachable_nodes += 1,
                    }
                }

                // display server metrics
                let metrics = Discv5::metrics();
                info!(
                    connected_peers = discv5.connected_peers(),
                    active_sessions = metrics.active_sessions,
                    "requests/s" = format_args!("{:.2}", metrics.unsolicited_requests_per_second),
                    ipv4_nodes = ipv4_only_reachable,
                    ipv6_only_nodes = ipv6_only_reachable,
                    dual_stack_nodes = ipv4_ipv6_reachable,
                    unreachable_nodes,
                    "Server metrics",
                );

            }
            Some(event) = event_stream.recv() => {
                match event {
                    discv5::Event::Discovered(_enr) => {
                        // An ENR has been obtained by the server
                        // Ignore these events here
                    }
                    discv5::Event::SocketUpdated(socket_addr) => {
                        info!(%socket_addr, "Advertised socket address updated");
                    }
                    _ => {} // Ignore
                }
            }
        }
    }
}
