use std::{collections::HashMap, path::PathBuf};

use anyhow::Context;
use provider::{
    constants::{LOCALHOST, NODE_CONFIG_DIR, NODE_DATA_DIR, NODE_RELAY_DATA_DIR, P2P_PORT},
    shared::helpers::running_in_ci,
    types::{SpawnNodeOptions, TransferedFile},
    DynNamespace,
};
use support::{constants::THIS_IS_A_BUG, fs::FileSystem};
use tracing::info;

use crate::{
    generators,
    network::node::NetworkNode,
    network_spec::{node::NodeSpec, parachain::ParachainSpec},
    shared::constants::{PROMETHEUS_PORT, RPC_PORT},
    ScopedFilesystem, ZombieRole,
};

#[derive(Clone)]
pub struct SpawnNodeCtx<'a, T: FileSystem> {
    /// Relaychain id, from the chain-spec (e.g rococo_local_testnet)
    pub(crate) chain_id: &'a str,
    // Parachain id, from the chain-spec (e.g local_testnet)
    pub(crate) parachain_id: Option<&'a str>,
    /// Relaychain chain name (e.g rococo-local)
    pub(crate) chain: &'a str,
    /// Role of the node in the network
    pub(crate) role: ZombieRole,
    /// Ref to the namespace
    pub(crate) ns: &'a DynNamespace,
    /// Ref to an scoped filesystem (encapsulate fs actions inside the ns directory)
    pub(crate) scoped_fs: &'a ScopedFilesystem<'a, T>,
    /// Ref to a parachain (used to spawn collators)
    pub(crate) parachain: Option<&'a ParachainSpec>,
    /// The string representation of the bootnode address to pass to nodes
    pub(crate) bootnodes_addr: &'a Vec<String>,
    /// Flag to wait node is ready or not
    /// Ready state means we can query Prometheus internal server
    pub(crate) wait_ready: bool,
}

pub async fn spawn_node<'a, T>(
    node: &NodeSpec,
    mut files_to_inject: Vec<TransferedFile>,
    ctx: &SpawnNodeCtx<'a, T>,
) -> Result<NetworkNode, anyhow::Error>
where
    T: FileSystem,
{
    let mut created_paths = vec![];
    // Create and inject the keystore IFF
    // - The node is validator in the relaychain
    // - The node is collator (encoded as validator) and the parachain is cumulus_based
    // (parachain_id) should be set then.
    if node.is_validator && (ctx.parachain.is_none() || ctx.parachain_id.is_some()) {
        // Generate keystore for node
        let node_files_path = if let Some(para) = ctx.parachain {
            para.id.to_string()
        } else {
            node.name.clone()
        };
        let asset_hub_polkadot = ctx
            .parachain_id
            .map(|id| id.starts_with("asset-hub-polkadot"))
            .unwrap_or_default();
        let key_filenames = generators::generate_node_keystore(
            &node.accounts,
            &node_files_path,
            ctx.scoped_fs,
            asset_hub_polkadot,
        )
        .await
        .unwrap();

        // Paths returned are relative to the base dir, we need to convert into
        // fullpaths to inject them in the nodes.
        let remote_keystore_chain_id = if let Some(id) = ctx.parachain_id {
            id
        } else {
            ctx.chain_id
        };

        for key_filename in key_filenames {
            let f = TransferedFile::new(
                PathBuf::from(format!(
                    "{}/{}/{}",
                    ctx.ns.base_dir().to_string_lossy(),
                    node_files_path,
                    key_filename.to_string_lossy()
                )),
                PathBuf::from(format!(
                    "/data/chains/{}/keystore/{}",
                    remote_keystore_chain_id,
                    key_filename.to_string_lossy()
                )),
            );
            files_to_inject.push(f);
        }
        created_paths.push(PathBuf::from(format!(
            "/data/chains/{}/keystore",
            remote_keystore_chain_id
        )));
    }

    let base_dir = format!("{}/{}", ctx.ns.base_dir().to_string_lossy(), &node.name);

    let (cfg_path, data_path, relay_data_path) = if !ctx.ns.capabilities().prefix_with_full_path {
        (
            NODE_CONFIG_DIR.into(),
            NODE_DATA_DIR.into(),
            NODE_RELAY_DATA_DIR.into(),
        )
    } else {
        let cfg_path = format!("{}{NODE_CONFIG_DIR}", &base_dir);
        let data_path = format!("{}{NODE_DATA_DIR}", &base_dir);
        let relay_data_path = format!("{}{NODE_RELAY_DATA_DIR}", &base_dir);
        (cfg_path, data_path, relay_data_path)
    };

    let gen_opts = generators::GenCmdOptions {
        relay_chain_name: ctx.chain,
        cfg_path: &cfg_path,               // TODO: get from provider/ns
        data_path: &data_path,             // TODO: get from provider
        relay_data_path: &relay_data_path, // TODO: get from provider
        use_wrapper: false,                // TODO: get from provider
        bootnode_addr: ctx.bootnodes_addr.clone(),
        // IFF the provider require an image (e.g k8s) we should use the default ports in the cmd.
        use_default_ports_in_cmd: ctx.ns.capabilities().use_default_ports_in_cmd,
    };

    let (program, args) = match ctx.role {
        // Collator should be `non-cumulus` one (e.g adder/undying)
        ZombieRole::Node | ZombieRole::Collator => {
            let maybe_para_id = ctx.parachain.map(|para| para.id);

            generators::generate_node_command(node, gen_opts, maybe_para_id)
        },
        ZombieRole::CumulusCollator => {
            let para = ctx.parachain.expect(&format!(
                "parachain must be part of the context {THIS_IS_A_BUG}"
            ));
            let full_p2p = generators::generate_node_port(None)?;
            generators::generate_node_command_cumulus(node, gen_opts, para.id, full_p2p.0)
        },
        _ => unreachable!(), /* TODO: do we need those?
                              * ZombieRole::Bootnode => todo!(),
                              * ZombieRole::Companion => todo!(), */
    };

    info!(
        "🚀 {}, spawning.... with command: {} {}",
        node.name,
        program,
        args.join(" ")
    );

    let ports = if ctx.ns.capabilities().use_default_ports_in_cmd {
        // should use default ports to as internal
        [
            (P2P_PORT, node.p2p_port.0),
            (RPC_PORT, node.rpc_port.0),
            (PROMETHEUS_PORT, node.prometheus_port.0),
        ]
    } else {
        [
            (P2P_PORT, P2P_PORT),
            (RPC_PORT, RPC_PORT),
            (PROMETHEUS_PORT, PROMETHEUS_PORT),
        ]
    };

    let spawn_ops = SpawnNodeOptions::new(node.name.clone(), program)
        .args(args)
        .env(
            node.env
                .iter()
                .map(|var| (var.name.clone(), var.value.clone())),
        )
        .injected_files(files_to_inject)
        .created_paths(created_paths)
        .db_snapshot(node.db_snapshot.clone())
        .port_mapping(HashMap::from(ports));

    let spawn_ops = if let Some(image) = node.image.as_ref() {
        spawn_ops.image(image.as_str())
    } else {
        spawn_ops
    };

    // Drops the port parking listeners before spawn
    node.ws_port.drop_listener();
    node.p2p_port.drop_listener();
    node.rpc_port.drop_listener();
    node.prometheus_port.drop_listener();

    let running_node = ctx.ns.spawn_node(&spawn_ops).await.with_context(|| {
        format!(
            "Failed to spawn node: {} with opts: {:#?}",
            node.name, spawn_ops
        )
    })?;

    let mut ip_to_use = LOCALHOST;

    let (rpc_port_external, prometheus_port_external);

    // Create port-forward iff we are not in CI
    if !running_in_ci() {
        let ports = futures::future::try_join_all(vec![
            running_node.create_port_forward(node.rpc_port.0, RPC_PORT),
            running_node.create_port_forward(node.prometheus_port.0, PROMETHEUS_PORT),
        ])
        .await?;

        (rpc_port_external, prometheus_port_external) = (
            ports[0].unwrap_or(node.rpc_port.0),
            ports[1].unwrap_or(node.prometheus_port.0),
        );
    } else {
        // running in ci require to use ip and default port
        (rpc_port_external, prometheus_port_external) = (RPC_PORT, PROMETHEUS_PORT);
        ip_to_use = running_node.ip().await?;
    }

    let ws_uri = format!("ws://{}:{}", ip_to_use, rpc_port_external);
    let prometheus_uri = format!("http://{}:{}/metrics", ip_to_use, prometheus_port_external);
    info!("🚀 {}, should be running now", node.name);
    info!(
        "💻 {}: direct link https://polkadot.js.org/apps/?rpc={ws_uri}#/explorer",
        node.name
    );
    info!("📊 {}: metrics link {prometheus_uri}", node.name);

    info!("📓 logs cmd: {}", running_node.log_cmd());

    Ok(NetworkNode::new(
        node.name.clone(),
        ws_uri,
        prometheus_uri,
        node.clone(),
        running_node,
    ))
}