// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Cumulus.

// Cumulus is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Cumulus is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Cumulus.  If not, see <http://www.gnu.org/licenses/>.

use crate::{
	chain_spec,
	chain_spec::GenericChainSpec,
	cli::{Cli, RelayChainCli, Subcommand},
	fake_runtime_api::{
		asset_hub_polkadot_aura::RuntimeApi as AssetHubPolkadotRuntimeApi, aura::RuntimeApi,
	},
	service::{new_partial, Block, Hash},
};
use cumulus_client_service::storage_proof_size::HostFunctions as ReclaimHostFunctions;
use cumulus_primitives_core::ParaId;
use frame_benchmarking_cli::{BenchmarkCmd, SUBSTRATE_REFERENCE_HARDWARE};
use log::info;
use parachains_common::{AssetHubPolkadotAuraId, AuraId};
use sc_cli::{
	ChainSpec, CliConfiguration, DefaultConfigurationValues, ImportParams, KeystoreParams,
	NetworkParams, Result, SharedParams, SubstrateCli,
};
use sc_service::config::{BasePath, PrometheusConfig};
use sp_runtime::traits::AccountIdConversion;
use std::{net::SocketAddr, path::PathBuf};

/// Helper enum that is used for better distinction of different parachain/runtime configuration
/// (it is based/calculated on ChainSpec's ID attribute)
#[derive(Debug, PartialEq, Default)]
enum Runtime {
	/// This is the default runtime (actually based on rococo)
	#[default]
	Default,
	Shell,
	Seedling,
	AssetHubPolkadot,
	AssetHubKusama,
	AssetHubRococo,
	AssetHubWestend,
	Penpal(ParaId),
	ContractsRococo,
	CollectivesPolkadot,
	CollectivesWestend,
	Glutton,
	GluttonWestend,
	BridgeHub(chain_spec::bridge_hubs::BridgeHubRuntimeType),
	Coretime(chain_spec::coretime::CoretimeRuntimeType),
	People(chain_spec::people::PeopleRuntimeType),
}

trait RuntimeResolver {
	fn runtime(&self) -> Result<Runtime>;
}

impl RuntimeResolver for dyn ChainSpec {
	fn runtime(&self) -> Result<Runtime> {
		Ok(runtime(self.id()))
	}
}

/// Implementation, that can resolve [`Runtime`] from any json configuration file
impl RuntimeResolver for PathBuf {
	fn runtime(&self) -> Result<Runtime> {
		#[derive(Debug, serde::Deserialize)]
		struct EmptyChainSpecWithId {
			id: String,
		}

		let file = std::fs::File::open(self)?;
		let reader = std::io::BufReader::new(file);
		let chain_spec: EmptyChainSpecWithId =
			serde_json::from_reader(reader).map_err(|e| sc_cli::Error::Application(Box::new(e)))?;

		Ok(runtime(&chain_spec.id))
	}
}

fn runtime(id: &str) -> Runtime {
	let id = id.replace('_', "-");
	let (_, id, para_id) = extract_parachain_id(&id);

	if id.starts_with("shell") {
		Runtime::Shell
	} else if id.starts_with("seedling") {
		Runtime::Seedling
	} else if id.starts_with("asset-hub-polkadot") | id.starts_with("statemint") {
		Runtime::AssetHubPolkadot
	} else if id.starts_with("asset-hub-kusama") | id.starts_with("statemine") {
		Runtime::AssetHubKusama
	} else if id.starts_with("asset-hub-rococo") {
		Runtime::AssetHubRococo
	} else if id.starts_with("asset-hub-westend") | id.starts_with("westmint") {
		Runtime::AssetHubWestend
	} else if id.starts_with("penpal") {
		Runtime::Penpal(para_id.unwrap_or(ParaId::new(0)))
	} else if id.starts_with("contracts-rococo") {
		Runtime::ContractsRococo
	} else if id.starts_with("collectives-polkadot") {
		Runtime::CollectivesPolkadot
	} else if id.starts_with("collectives-westend") {
		Runtime::CollectivesWestend
	} else if id.starts_with(chain_spec::bridge_hubs::BridgeHubRuntimeType::ID_PREFIX) {
		Runtime::BridgeHub(
			id.parse::<chain_spec::bridge_hubs::BridgeHubRuntimeType>()
				.expect("Invalid value"),
		)
	} else if id.starts_with(chain_spec::coretime::CoretimeRuntimeType::ID_PREFIX) {
		Runtime::Coretime(
			id.parse::<chain_spec::coretime::CoretimeRuntimeType>().expect("Invalid value"),
		)
	} else if id.starts_with("glutton-westend") {
		Runtime::GluttonWestend
	} else if id.starts_with("glutton") {
		Runtime::Glutton
	} else if id.starts_with(chain_spec::people::PeopleRuntimeType::ID_PREFIX) {
		Runtime::People(id.parse::<chain_spec::people::PeopleRuntimeType>().expect("Invalid value"))
	} else {
		log::warn!("No specific runtime was recognized for ChainSpec's id: '{}', so Runtime::default() will be used", id);
		Runtime::default()
	}
}

fn load_spec(id: &str) -> std::result::Result<Box<dyn ChainSpec>, String> {
	let (id, _, para_id) = extract_parachain_id(id);
	Ok(match id {
		// - Default-like
		"staging" =>
			Box::new(chain_spec::rococo_parachain::staging_rococo_parachain_local_config()),
		"tick" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/tick.json")[..],
		)?),
		"trick" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/trick.json")[..],
		)?),
		"track" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/track.json")[..],
		)?),

		// -- Starters
		"shell" => Box::new(chain_spec::shell::get_shell_chain_spec()),
		"seedling" => Box::new(chain_spec::seedling::get_seedling_chain_spec()),

		// -- Asset Hub Polkadot
		"asset-hub-polkadot" | "statemint" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/asset-hub-polkadot.json")[..],
		)?),

		// -- Asset Hub Kusama
		"asset-hub-kusama" | "statemine" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/asset-hub-kusama.json")[..],
		)?),

		// -- Asset Hub Rococo
		"asset-hub-rococo-dev" =>
			Box::new(chain_spec::asset_hubs::asset_hub_rococo_development_config()),
		"asset-hub-rococo-local" =>
			Box::new(chain_spec::asset_hubs::asset_hub_rococo_local_config()),
		// the chain spec as used for generating the upgrade genesis values
		"asset-hub-rococo-genesis" =>
			Box::new(chain_spec::asset_hubs::asset_hub_rococo_genesis_config()),
		"asset-hub-rococo" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/asset-hub-rococo.json")[..],
		)?),

		// -- Asset Hub Westend
		"asset-hub-westend-dev" | "westmint-dev" =>
			Box::new(chain_spec::asset_hubs::asset_hub_westend_development_config()),
		"asset-hub-westend-local" | "westmint-local" =>
			Box::new(chain_spec::asset_hubs::asset_hub_westend_local_config()),
		// the chain spec as used for generating the upgrade genesis values
		"asset-hub-westend-genesis" | "westmint-genesis" =>
			Box::new(chain_spec::asset_hubs::asset_hub_westend_config()),
		// the shell-based chain spec as used for syncing
		"asset-hub-westend" | "westmint" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/asset-hub-westend.json")[..],
		)?),

		// -- Polkadot Collectives
		"collectives-polkadot" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/collectives-polkadot.json")[..],
		)?),

		// -- Westend Collectives
		"collectives-westend-dev" =>
			Box::new(chain_spec::collectives::collectives_westend_development_config()),
		"collectives-westend-local" =>
			Box::new(chain_spec::collectives::collectives_westend_local_config()),
		"collectives-westend" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/collectives-westend.json")[..],
		)?),

		// -- Contracts on Rococo
		"contracts-rococo-dev" =>
			Box::new(chain_spec::contracts::contracts_rococo_development_config()),
		"contracts-rococo-local" =>
			Box::new(chain_spec::contracts::contracts_rococo_local_config()),
		"contracts-rococo-genesis" => Box::new(chain_spec::contracts::contracts_rococo_config()),
		"contracts-rococo" => Box::new(GenericChainSpec::from_json_bytes(
			&include_bytes!("../chain-specs/contracts-rococo.json")[..],
		)?),

		// -- BridgeHub
		bridge_like_id
			if bridge_like_id
				.starts_with(chain_spec::bridge_hubs::BridgeHubRuntimeType::ID_PREFIX) =>
			bridge_like_id
				.parse::<chain_spec::bridge_hubs::BridgeHubRuntimeType>()
				.expect("invalid value")
				.load_config()?,

		// -- Coretime
		coretime_like_id
			if coretime_like_id
				.starts_with(chain_spec::coretime::CoretimeRuntimeType::ID_PREFIX) =>
			coretime_like_id
				.parse::<chain_spec::coretime::CoretimeRuntimeType>()
				.expect("invalid value")
				.load_config()?,

		// -- Penpal
		"penpal-rococo" => Box::new(chain_spec::penpal::get_penpal_chain_spec(
			para_id.expect("Must specify parachain id"),
			"rococo-local",
		)),
		"penpal-westend" => Box::new(chain_spec::penpal::get_penpal_chain_spec(
			para_id.expect("Must specify parachain id"),
			"westend-local",
		)),

		// -- Glutton Westend
		"glutton-westend-dev" => Box::new(chain_spec::glutton::glutton_westend_development_config(
			para_id.expect("Must specify parachain id"),
		)),
		"glutton-westend-local" => Box::new(chain_spec::glutton::glutton_westend_local_config(
			para_id.expect("Must specify parachain id"),
		)),
		// the chain spec as used for generating the upgrade genesis values
		"glutton-westend-genesis" => Box::new(chain_spec::glutton::glutton_westend_config(
			para_id.expect("Must specify parachain id"),
		)),

		// -- People
		people_like_id
			if people_like_id.starts_with(chain_spec::people::PeopleRuntimeType::ID_PREFIX) =>
			people_like_id
				.parse::<chain_spec::people::PeopleRuntimeType>()
				.expect("invalid value")
				.load_config()?,

		// -- Fallback (generic chainspec)
		"" => {
			log::warn!("No ChainSpec.id specified, so using default one, based on rococo-parachain runtime");
			Box::new(chain_spec::rococo_parachain::rococo_parachain_local_config())
		},

		// -- Loading a specific spec from disk
		path => Box::new(GenericChainSpec::from_json_file(path.into())?),
	})
}

/// Extracts the normalized chain id and parachain id from the input chain id.
/// (H/T to Phala for the idea)
/// E.g. "penpal-kusama-2004" yields ("penpal-kusama", Some(2004))
fn extract_parachain_id(id: &str) -> (&str, &str, Option<ParaId>) {
	const ROCOCO_TEST_PARA_PREFIX: &str = "penpal-rococo-";
	const KUSAMA_TEST_PARA_PREFIX: &str = "penpal-kusama-";
	const POLKADOT_TEST_PARA_PREFIX: &str = "penpal-polkadot-";

	const GLUTTON_PARA_DEV_PREFIX: &str = "glutton-kusama-dev-";
	const GLUTTON_PARA_LOCAL_PREFIX: &str = "glutton-kusama-local-";
	const GLUTTON_PARA_GENESIS_PREFIX: &str = "glutton-kusama-genesis-";

	const GLUTTON_WESTEND_PARA_DEV_PREFIX: &str = "glutton-westend-dev-";
	const GLUTTON_WESTEND_PARA_LOCAL_PREFIX: &str = "glutton-westend-local-";
	const GLUTTON_WESTEND_PARA_GENESIS_PREFIX: &str = "glutton-westend-genesis-";

	let (norm_id, orig_id, para) = if let Some(suffix) = id.strip_prefix(ROCOCO_TEST_PARA_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..ROCOCO_TEST_PARA_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(KUSAMA_TEST_PARA_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..KUSAMA_TEST_PARA_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(POLKADOT_TEST_PARA_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..POLKADOT_TEST_PARA_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(GLUTTON_PARA_DEV_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..GLUTTON_PARA_DEV_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(GLUTTON_PARA_LOCAL_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..GLUTTON_PARA_LOCAL_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(GLUTTON_PARA_GENESIS_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..GLUTTON_PARA_GENESIS_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(GLUTTON_WESTEND_PARA_DEV_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..GLUTTON_WESTEND_PARA_DEV_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(GLUTTON_WESTEND_PARA_LOCAL_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..GLUTTON_WESTEND_PARA_LOCAL_PREFIX.len() - 1], id, Some(para_id))
	} else if let Some(suffix) = id.strip_prefix(GLUTTON_WESTEND_PARA_GENESIS_PREFIX) {
		let para_id: u32 = suffix.parse().expect("Invalid parachain-id suffix");
		(&id[..GLUTTON_WESTEND_PARA_GENESIS_PREFIX.len() - 1], id, Some(para_id))
	} else {
		(id, id, None)
	};

	(norm_id, orig_id, para.map(Into::into))
}

impl SubstrateCli for Cli {
	fn impl_name() -> String {
		"Polkadot parachain".into()
	}

	fn impl_version() -> String {
		env!("SUBSTRATE_CLI_IMPL_VERSION").into()
	}

	fn description() -> String {
		format!(
			"Polkadot parachain\n\nThe command-line arguments provided first will be \
		passed to the parachain node, while the arguments provided after -- will be passed \
		to the relaychain node.\n\n\
		{} [parachain-args] -- [relaychain-args]",
			Self::executable_name()
		)
	}

	fn author() -> String {
		env!("CARGO_PKG_AUTHORS").into()
	}

	fn support_url() -> String {
		"https://github.com/paritytech/polkadot-sdk/issues/new".into()
	}

	fn copyright_start_year() -> i32 {
		2017
	}

	fn load_spec(&self, id: &str) -> std::result::Result<Box<dyn ChainSpec>, String> {
		load_spec(id)
	}
}

impl SubstrateCli for RelayChainCli {
	fn impl_name() -> String {
		"Polkadot parachain".into()
	}

	fn impl_version() -> String {
		env!("SUBSTRATE_CLI_IMPL_VERSION").into()
	}

	fn description() -> String {
		format!(
			"Polkadot parachain\n\nThe command-line arguments provided first will be \
		passed to the parachain node, while the arguments provided after -- will be passed \
		to the relay chain node.\n\n\
		{} [parachain-args] -- [relay_chain-args]",
			Self::executable_name()
		)
	}

	fn author() -> String {
		env!("CARGO_PKG_AUTHORS").into()
	}

	fn support_url() -> String {
		"https://github.com/paritytech/polkadot-sdk/issues/new".into()
	}

	fn copyright_start_year() -> i32 {
		2017
	}

	fn load_spec(&self, id: &str) -> std::result::Result<Box<dyn ChainSpec>, String> {
		polkadot_cli::Cli::from_iter([RelayChainCli::executable_name()].iter()).load_spec(id)
	}
}

/// Creates partial components for the runtimes that are supported by the benchmarks.
macro_rules! construct_partials {
	($config:expr, |$partials:ident| $code:expr) => {
		match $config.chain_spec.runtime()? {
			Runtime::AssetHubPolkadot => {
				let $partials = new_partial::<AssetHubPolkadotRuntimeApi, _>(
					&$config,
					crate::service::build_relay_to_aura_import_queue::<_, AssetHubPolkadotAuraId>,
				)?;
				$code
			},
			Runtime::AssetHubKusama |
			Runtime::AssetHubRococo |
			Runtime::AssetHubWestend |
			Runtime::BridgeHub(_) |
			Runtime::CollectivesPolkadot |
			Runtime::CollectivesWestend |
			Runtime::Coretime(_) |
			Runtime::People(_) => {
				let $partials = new_partial::<RuntimeApi, _>(
					&$config,
					crate::service::build_relay_to_aura_import_queue::<_, AuraId>,
				)?;
				$code
			},
			Runtime::GluttonWestend | Runtime::Glutton | Runtime::Shell | Runtime::Seedling => {
				let $partials = new_partial::<RuntimeApi, _>(
					&$config,
					crate::service::build_shell_import_queue,
				)?;
				$code
			},
			Runtime::ContractsRococo | Runtime::Penpal(_) | Runtime::Default => {
				let $partials = new_partial::<RuntimeApi, _>(
					&$config,
					crate::service::build_aura_import_queue,
				)?;
				$code
			},
		}
	};
}

macro_rules! construct_async_run {
	(|$components:ident, $cli:ident, $cmd:ident, $config:ident| $( $code:tt )* ) => {{
		let runner = $cli.create_runner($cmd)?;
		match runner.config().chain_spec.runtime()? {
			Runtime::AssetHubPolkadot => {
				runner.async_run(|$config| {
					let $components = new_partial::<AssetHubPolkadotRuntimeApi, _>(
						&$config,
						crate::service::build_relay_to_aura_import_queue::<_, AssetHubPolkadotAuraId>,
					)?;
					let task_manager = $components.task_manager;
					{ $( $code )* }.map(|v| (v, task_manager))
				})
			},
			Runtime::AssetHubKusama |
			Runtime::AssetHubRococo |
			Runtime::AssetHubWestend |
			Runtime::BridgeHub(_) |
			Runtime::CollectivesPolkadot |
			Runtime::CollectivesWestend |
			Runtime::Coretime(_) |
			Runtime::People(_) => {
				runner.async_run(|$config| {
					let $components = new_partial::<RuntimeApi, _>(
						&$config,
						crate::service::build_relay_to_aura_import_queue::<_, AuraId>,
					)?;
					let task_manager = $components.task_manager;
					{ $( $code )* }.map(|v| (v, task_manager))
				})
			},
			Runtime::Shell |
			Runtime::Seedling |
			Runtime::GluttonWestend |
			Runtime::Glutton => {
				runner.async_run(|$config| {
					let $components = new_partial::<RuntimeApi, _>(
						&$config,
						crate::service::build_shell_import_queue,
					)?;
					let task_manager = $components.task_manager;
					{ $( $code )* }.map(|v| (v, task_manager))
				})
			}
			Runtime::ContractsRococo | Runtime::Penpal(_) | Runtime::Default => {
				runner.async_run(|$config| {
					let $components = new_partial::<
						RuntimeApi,
						_,
					>(
						&$config,
						crate::service::build_aura_import_queue,
					)?;
					let task_manager = $components.task_manager;
					{ $( $code )* }.map(|v| (v, task_manager))
				})
			},
		}
	}}
}

/// Parse command line arguments into service configuration.
pub fn run() -> Result<()> {
	let cli = Cli::from_args();

	match &cli.subcommand {
		Some(Subcommand::BuildSpec(cmd)) => {
			let runner = cli.create_runner(cmd)?;
			runner.sync_run(|config| cmd.run(config.chain_spec, config.network))
		},
		Some(Subcommand::CheckBlock(cmd)) => {
			construct_async_run!(|components, cli, cmd, config| {
				Ok(cmd.run(components.client, components.import_queue))
			})
		},
		Some(Subcommand::ExportBlocks(cmd)) => {
			construct_async_run!(|components, cli, cmd, config| {
				Ok(cmd.run(components.client, config.database))
			})
		},
		Some(Subcommand::ExportState(cmd)) => {
			construct_async_run!(|components, cli, cmd, config| {
				Ok(cmd.run(components.client, config.chain_spec))
			})
		},
		Some(Subcommand::ImportBlocks(cmd)) => {
			construct_async_run!(|components, cli, cmd, config| {
				Ok(cmd.run(components.client, components.import_queue))
			})
		},
		Some(Subcommand::Revert(cmd)) => construct_async_run!(|components, cli, cmd, config| {
			Ok(cmd.run(components.client, components.backend, None))
		}),
		Some(Subcommand::PurgeChain(cmd)) => {
			let runner = cli.create_runner(cmd)?;

			runner.sync_run(|config| {
				let polkadot_cli = RelayChainCli::new(
					&config,
					[RelayChainCli::executable_name()].iter().chain(cli.relaychain_args.iter()),
				);

				let polkadot_config = SubstrateCli::create_configuration(
					&polkadot_cli,
					&polkadot_cli,
					config.tokio_handle.clone(),
				)
				.map_err(|err| format!("Relay chain argument error: {}", err))?;

				cmd.run(config, polkadot_config)
			})
		},
		Some(Subcommand::ExportGenesisHead(cmd)) => {
			let runner = cli.create_runner(cmd)?;
			runner.sync_run(|config| {
				construct_partials!(config, |partials| cmd.run(partials.client))
			})
		},
		Some(Subcommand::ExportGenesisWasm(cmd)) => {
			let runner = cli.create_runner(cmd)?;
			runner.sync_run(|_config| {
				let spec = cli.load_spec(&cmd.shared_params.chain.clone().unwrap_or_default())?;
				cmd.run(&*spec)
			})
		},
		Some(Subcommand::Benchmark(cmd)) => {
			let runner = cli.create_runner(cmd)?;

			// Switch on the concrete benchmark sub-command-
			match cmd {
				BenchmarkCmd::Pallet(cmd) =>
					if cfg!(feature = "runtime-benchmarks") {
						runner.sync_run(|config| cmd.run_with_spec::<sp_runtime::traits::HashingFor<Block>, ReclaimHostFunctions>(Some(config.chain_spec)))
					} else {
						Err("Benchmarking wasn't enabled when building the node. \
				You can enable it with `--features runtime-benchmarks`."
							.into())
					},
				BenchmarkCmd::Block(cmd) => runner.sync_run(|config| {
					construct_partials!(config, |partials| cmd.run(partials.client))
				}),
				#[cfg(not(feature = "runtime-benchmarks"))]
				BenchmarkCmd::Storage(_) =>
					return Err(sc_cli::Error::Input(
						"Compile with --features=runtime-benchmarks \
						to enable storage benchmarks."
							.into(),
					)
					.into()),
				#[cfg(feature = "runtime-benchmarks")]
				BenchmarkCmd::Storage(cmd) => runner.sync_run(|config| {
					construct_partials!(config, |partials| {
						let db = partials.backend.expose_db();
						let storage = partials.backend.expose_storage();

						cmd.run(config, partials.client.clone(), db, storage)
					})
				}),
				BenchmarkCmd::Machine(cmd) =>
					runner.sync_run(|config| cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone())),
				// NOTE: this allows the Client to leniently implement
				// new benchmark commands without requiring a companion MR.
				#[allow(unreachable_patterns)]
				_ => Err("Benchmarking sub-command unsupported".into()),
			}
		},
		Some(Subcommand::TryRuntime) => Err("The `try-runtime` subcommand has been migrated to a standalone CLI (https://github.com/paritytech/try-runtime-cli). It is no longer being maintained here and will be removed entirely some time after January 2024. Please remove this subcommand from your runtime and use the standalone CLI.".into()),
		Some(Subcommand::Key(cmd)) => Ok(cmd.run(&cli)?),
		None => {
			let runner = cli.create_runner(&cli.run.normalize())?;
			let collator_options = cli.run.collator_options();

			runner.run_node_until_exit(|config| async move {
				// If Statemint (Statemine, Westmint, Rockmine) DB exists and we're using the
				// asset-hub chain spec, then rename the base path to the new chain ID. In the case
				// that both file paths exist, the node will exit, as the user must decide (by
				// deleting one path) the information that they want to use as their DB.
				let old_name = match config.chain_spec.id() {
					"asset-hub-polkadot" => Some("statemint"),
					"asset-hub-kusama" => Some("statemine"),
					"asset-hub-westend" => Some("westmint"),
					"asset-hub-rococo" => Some("rockmine"),
					_ => None,
				};

				if let Some(old_name) = old_name {
					let new_path = config.base_path.config_dir(config.chain_spec.id());
					let old_path = config.base_path.config_dir(old_name);

					if old_path.exists() && new_path.exists() {
						return Err(format!(
							"Found legacy {} path {} and new asset-hub path {}. Delete one path such that only one exists.",
							old_name, old_path.display(), new_path.display()
						).into())
					}

					if old_path.exists() {
						std::fs::rename(old_path.clone(), new_path.clone())?;
						info!(
							"Statemint renamed to Asset Hub. The filepath with associated data on disk has been renamed from {} to {}.",
							old_path.display(), new_path.display()
						);
					}
				}

				let hwbench = (!cli.no_hardware_benchmarks).then_some(
					config.database.path().map(|database_path| {
						let _ = std::fs::create_dir_all(database_path);
						sc_sysinfo::gather_hwbench(Some(database_path))
					})).flatten();

				let para_id = chain_spec::Extensions::try_get(&*config.chain_spec)
					.map(|e| e.para_id)
					.ok_or("Could not find parachain extension in chain-spec.")?;

				let polkadot_cli = RelayChainCli::new(
					&config,
					[RelayChainCli::executable_name()].iter().chain(cli.relaychain_args.iter()),
				);

				let id = ParaId::from(para_id);

				let parachain_account =
					AccountIdConversion::<polkadot_primitives::AccountId>::into_account_truncating(&id);

				let tokio_handle = config.tokio_handle.clone();
				let polkadot_config =
					SubstrateCli::create_configuration(&polkadot_cli, &polkadot_cli, tokio_handle)
						.map_err(|err| format!("Relay chain argument error: {}", err))?;

				info!("Parachain id: {:?}", id);
				info!("Parachain Account: {}", parachain_account);
				info!("Is collating: {}", if config.role.is_authority() { "yes" } else { "no" });

				match polkadot_config.network.network_backend {
					sc_network::config::NetworkBackendType::Libp2p =>
						start_node::<sc_network::NetworkWorker<_, _>>(
							config,
							polkadot_config,
							collator_options,
							id,
							hwbench,
						)
						.await,
					sc_network::config::NetworkBackendType::Litep2p =>
						start_node::<sc_network::Litep2pNetworkBackend>(
							config,
							polkadot_config,
							collator_options,
							id,
							hwbench,
						)
						.await,
				}
			})
		},
	}
}

async fn start_node<Network: sc_network::NetworkBackend<Block, Hash>>(
	config: sc_service::Configuration,
	polkadot_config: sc_service::Configuration,
	collator_options: cumulus_client_cli::CollatorOptions,
	id: ParaId,
	hwbench: Option<sc_sysinfo::HwBench>,
) -> Result<sc_service::TaskManager> {
	match config.chain_spec.runtime()? {
		Runtime::AssetHubPolkadot => crate::service::start_asset_hub_node::<
			AssetHubPolkadotRuntimeApi,
			AssetHubPolkadotAuraId,
			Network,
		>(config, polkadot_config, collator_options, id, hwbench)
		.await
		.map(|r| r.0)
		.map_err(Into::into),

		Runtime::AssetHubKusama => crate::service::start_asset_hub_node::<
			RuntimeApi,
			AuraId,
			Network,
		>(config, polkadot_config, collator_options, id, hwbench)
		.await
		.map(|r| r.0)
		.map_err(Into::into),

		Runtime::AssetHubRococo | Runtime::AssetHubWestend =>
			crate::service::start_asset_hub_lookahead_node::<RuntimeApi, AuraId, Network>(
				config,
				polkadot_config,
				collator_options,
				id,
				hwbench,
			)
			.await
			.map(|r| r.0)
			.map_err(Into::into),

		Runtime::CollectivesPolkadot => crate::service::start_generic_aura_node::<Network>(
			config,
			polkadot_config,
			collator_options,
			id,
			hwbench,
		)
		.await
		.map(|r| r.0)
		.map_err(Into::into),

		Runtime::CollectivesWestend =>
			crate::service::start_generic_aura_lookahead_node::<Network>(
				config,
				polkadot_config,
				collator_options,
				id,
				hwbench,
			)
			.await
			.map(|r| r.0)
			.map_err(Into::into),

		Runtime::Seedling | Runtime::Shell => crate::service::start_shell_node::<Network>(
			config,
			polkadot_config,
			collator_options,
			id,
			hwbench,
		)
		.await
		.map(|r| r.0)
		.map_err(Into::into),

		Runtime::ContractsRococo => crate::service::start_contracts_rococo_node::<Network>(
			config,
			polkadot_config,
			collator_options,
			id,
			hwbench,
		)
		.await
		.map(|r| r.0)
		.map_err(Into::into),

		Runtime::BridgeHub(bridge_hub_runtime_type) => match bridge_hub_runtime_type {
			chain_spec::bridge_hubs::BridgeHubRuntimeType::Polkadot |
			chain_spec::bridge_hubs::BridgeHubRuntimeType::PolkadotLocal =>
				crate::service::start_generic_aura_node::<Network>(
					config,
					polkadot_config,
					collator_options,
					id,
					hwbench,
				)
				.await
				.map(|r| r.0),
			chain_spec::bridge_hubs::BridgeHubRuntimeType::Kusama |
			chain_spec::bridge_hubs::BridgeHubRuntimeType::KusamaLocal =>
				crate::service::start_generic_aura_node::<Network>(
					config,
					polkadot_config,
					collator_options,
					id,
					hwbench,
				)
				.await
				.map(|r| r.0),
			chain_spec::bridge_hubs::BridgeHubRuntimeType::Westend |
			chain_spec::bridge_hubs::BridgeHubRuntimeType::WestendLocal |
			chain_spec::bridge_hubs::BridgeHubRuntimeType::WestendDevelopment =>
				crate::service::start_generic_aura_lookahead_node::<Network>(
					config,
					polkadot_config,
					collator_options,
					id,
					hwbench,
				)
				.await
				.map(|r| r.0),
			chain_spec::bridge_hubs::BridgeHubRuntimeType::Rococo |
			chain_spec::bridge_hubs::BridgeHubRuntimeType::RococoLocal |
			chain_spec::bridge_hubs::BridgeHubRuntimeType::RococoDevelopment =>
				crate::service::start_generic_aura_lookahead_node::<Network>(
					config,
					polkadot_config,
					collator_options,
					id,
					hwbench,
				)
				.await
				.map(|r| r.0),
		}
		.map_err(Into::into),

		Runtime::Coretime(coretime_runtime_type) => match coretime_runtime_type {
			chain_spec::coretime::CoretimeRuntimeType::Kusama |
			chain_spec::coretime::CoretimeRuntimeType::KusamaLocal |
			chain_spec::coretime::CoretimeRuntimeType::Polkadot |
			chain_spec::coretime::CoretimeRuntimeType::PolkadotLocal |
			chain_spec::coretime::CoretimeRuntimeType::Rococo |
			chain_spec::coretime::CoretimeRuntimeType::RococoLocal |
			chain_spec::coretime::CoretimeRuntimeType::RococoDevelopment |
			chain_spec::coretime::CoretimeRuntimeType::Westend |
			chain_spec::coretime::CoretimeRuntimeType::WestendLocal |
			chain_spec::coretime::CoretimeRuntimeType::WestendDevelopment =>
				crate::service::start_generic_aura_lookahead_node::<Network>(
					config,
					polkadot_config,
					collator_options,
					id,
					hwbench,
				)
				.await
				.map(|r| r.0),
		}
		.map_err(Into::into),

		Runtime::Penpal(_) | Runtime::Default =>
			crate::service::start_rococo_parachain_node::<Network>(
				config,
				polkadot_config,
				collator_options,
				id,
				hwbench,
			)
			.await
			.map(|r| r.0)
			.map_err(Into::into),

		Runtime::Glutton | Runtime::GluttonWestend =>
			crate::service::start_basic_lookahead_node::<Network>(
				config,
				polkadot_config,
				collator_options,
				id,
				hwbench,
			)
			.await
			.map(|r| r.0)
			.map_err(Into::into),

		Runtime::People(people_runtime_type) => match people_runtime_type {
			chain_spec::people::PeopleRuntimeType::Kusama |
			chain_spec::people::PeopleRuntimeType::KusamaLocal |
			chain_spec::people::PeopleRuntimeType::Polkadot |
			chain_spec::people::PeopleRuntimeType::PolkadotLocal |
			chain_spec::people::PeopleRuntimeType::Rococo |
			chain_spec::people::PeopleRuntimeType::RococoLocal |
			chain_spec::people::PeopleRuntimeType::RococoDevelopment |
			chain_spec::people::PeopleRuntimeType::Westend |
			chain_spec::people::PeopleRuntimeType::WestendLocal |
			chain_spec::people::PeopleRuntimeType::WestendDevelopment =>
				crate::service::start_generic_aura_lookahead_node::<Network>(
					config,
					polkadot_config,
					collator_options,
					id,
					hwbench,
				)
				.await
				.map(|r| r.0),
		}
		.map_err(Into::into),
	}
}

impl DefaultConfigurationValues for RelayChainCli {
	fn p2p_listen_port() -> u16 {
		30334
	}

	fn rpc_listen_port() -> u16 {
		9945
	}

	fn prometheus_listen_port() -> u16 {
		9616
	}
}

impl CliConfiguration<Self> for RelayChainCli {
	fn shared_params(&self) -> &SharedParams {
		self.base.base.shared_params()
	}

	fn import_params(&self) -> Option<&ImportParams> {
		self.base.base.import_params()
	}

	fn network_params(&self) -> Option<&NetworkParams> {
		self.base.base.network_params()
	}

	fn keystore_params(&self) -> Option<&KeystoreParams> {
		self.base.base.keystore_params()
	}

	fn base_path(&self) -> Result<Option<BasePath>> {
		Ok(self
			.shared_params()
			.base_path()?
			.or_else(|| self.base_path.clone().map(Into::into)))
	}

	fn rpc_addr(&self, default_listen_port: u16) -> Result<Option<SocketAddr>> {
		self.base.base.rpc_addr(default_listen_port)
	}

	fn prometheus_config(
		&self,
		default_listen_port: u16,
		chain_spec: &Box<dyn ChainSpec>,
	) -> Result<Option<PrometheusConfig>> {
		self.base.base.prometheus_config(default_listen_port, chain_spec)
	}

	fn init<F>(
		&self,
		_support_url: &String,
		_impl_version: &String,
		_logger_hook: F,
		_config: &sc_service::Configuration,
	) -> Result<()>
	where
		F: FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration),
	{
		unreachable!("PolkadotCli is never initialized; qed");
	}

	fn chain_id(&self, is_dev: bool) -> Result<String> {
		let chain_id = self.base.base.chain_id(is_dev)?;

		Ok(if chain_id.is_empty() { self.chain_id.clone().unwrap_or_default() } else { chain_id })
	}

	fn role(&self, is_dev: bool) -> Result<sc_service::Role> {
		self.base.base.role(is_dev)
	}

	fn transaction_pool(&self, is_dev: bool) -> Result<sc_service::config::TransactionPoolOptions> {
		self.base.base.transaction_pool(is_dev)
	}

	fn trie_cache_maximum_size(&self) -> Result<Option<usize>> {
		self.base.base.trie_cache_maximum_size()
	}

	fn rpc_methods(&self) -> Result<sc_service::config::RpcMethods> {
		self.base.base.rpc_methods()
	}

	fn rpc_max_connections(&self) -> Result<u32> {
		self.base.base.rpc_max_connections()
	}

	fn rpc_cors(&self, is_dev: bool) -> Result<Option<Vec<String>>> {
		self.base.base.rpc_cors(is_dev)
	}

	fn default_heap_pages(&self) -> Result<Option<u64>> {
		self.base.base.default_heap_pages()
	}

	fn force_authoring(&self) -> Result<bool> {
		self.base.base.force_authoring()
	}

	fn disable_grandpa(&self) -> Result<bool> {
		self.base.base.disable_grandpa()
	}

	fn max_runtime_instances(&self) -> Result<Option<usize>> {
		self.base.base.max_runtime_instances()
	}

	fn announce_block(&self) -> Result<bool> {
		self.base.base.announce_block()
	}

	fn telemetry_endpoints(
		&self,
		chain_spec: &Box<dyn ChainSpec>,
	) -> Result<Option<sc_telemetry::TelemetryEndpoints>> {
		self.base.base.telemetry_endpoints(chain_spec)
	}

	fn node_name(&self) -> Result<String> {
		self.base.base.node_name()
	}
}

#[cfg(test)]
mod tests {
	use crate::{
		chain_spec::{get_account_id_from_seed, get_from_seed},
		command::{Runtime, RuntimeResolver},
	};
	use sc_chain_spec::{ChainSpec, ChainSpecExtension, ChainSpecGroup, ChainType, Extension};
	use serde::{Deserialize, Serialize};
	use sp_core::sr25519;
	use std::path::PathBuf;
	use tempfile::TempDir;

	#[derive(
		Debug, Clone, PartialEq, Serialize, Deserialize, ChainSpecGroup, ChainSpecExtension, Default,
	)]
	#[serde(deny_unknown_fields)]
	pub struct Extensions1 {
		pub attribute1: String,
		pub attribute2: u32,
	}

	#[derive(
		Debug, Clone, PartialEq, Serialize, Deserialize, ChainSpecGroup, ChainSpecExtension, Default,
	)]
	#[serde(deny_unknown_fields)]
	pub struct Extensions2 {
		pub attribute_x: String,
		pub attribute_y: String,
		pub attribute_z: u32,
	}

	fn store_configuration(dir: &TempDir, spec: Box<dyn ChainSpec>) -> PathBuf {
		let raw_output = true;
		let json = sc_service::chain_ops::build_spec(&*spec, raw_output)
			.expect("Failed to build json string");
		let mut cfg_file_path = dir.path().to_path_buf();
		cfg_file_path.push(spec.id());
		cfg_file_path.set_extension("json");
		std::fs::write(&cfg_file_path, json).expect("Failed to write to json file");
		cfg_file_path
	}

	pub type DummyChainSpec<E> = sc_service::GenericChainSpec<(), E>;

	pub fn create_default_with_extensions<E: Extension>(
		id: &str,
		extension: E,
	) -> DummyChainSpec<E> {
		DummyChainSpec::builder(
			rococo_parachain_runtime::WASM_BINARY
				.expect("WASM binary was not built, please build it!"),
			extension,
		)
		.with_name("Dummy local testnet")
		.with_id(id)
		.with_chain_type(ChainType::Local)
		.with_genesis_config_patch(crate::chain_spec::rococo_parachain::testnet_genesis(
			get_account_id_from_seed::<sr25519::Public>("Alice"),
			vec![
				get_from_seed::<rococo_parachain_runtime::AuraId>("Alice"),
				get_from_seed::<rococo_parachain_runtime::AuraId>("Bob"),
			],
			vec![get_account_id_from_seed::<sr25519::Public>("Alice")],
			1000.into(),
		))
		.build()
	}

	#[test]
	fn test_resolve_runtime_for_different_configuration_files() {
		let temp_dir = tempfile::tempdir().expect("Failed to access tempdir");

		let path = store_configuration(
			&temp_dir,
			Box::new(create_default_with_extensions("shell-1", Extensions1::default())),
		);
		assert_eq!(Runtime::Shell, path.runtime().unwrap());

		let path = store_configuration(
			&temp_dir,
			Box::new(create_default_with_extensions("shell-2", Extensions2::default())),
		);
		assert_eq!(Runtime::Shell, path.runtime().unwrap());

		let path = store_configuration(
			&temp_dir,
			Box::new(create_default_with_extensions("seedling", Extensions2::default())),
		);
		assert_eq!(Runtime::Seedling, path.runtime().unwrap());

		let path = store_configuration(
			&temp_dir,
			Box::new(crate::chain_spec::rococo_parachain::rococo_parachain_local_config()),
		);
		assert_eq!(Runtime::Default, path.runtime().unwrap());

		let path = store_configuration(
			&temp_dir,
			Box::new(crate::chain_spec::contracts::contracts_rococo_local_config()),
		);
		assert_eq!(Runtime::ContractsRococo, path.runtime().unwrap());
	}
}
