use std::{fs, path::PathBuf};

use clap::{Parser, Subcommand};
use sc_chain_spec::{ChainType, GenericChainSpec, GenesisConfigBuilderRuntimeCaller};
use serde_json::Value;

#[derive(Debug, Parser)]
#[command(rename_all = "kebab-case", version, about)]
pub struct ChainSpecBuilder {
    #[command(subcommand)]
    pub command: ChainSpecBuilderCmd,
    /// The path where the chain should be saved.
    #[arg(long, short, default_value = "./chain_spec.json")]
    pub chain_spec_path: PathBuf,
}

#[derive(Debug, Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum ChainSpecBuilderCmd {
    Create(CreateCmd),
    Verify(VerifyCmd),
    UpdateCode(UpdateCodeCmd),
    ConvertToRaw(ConvertToRawCmd),
    ListPresets(ListPresetsCmd),
    DisplayPreset(DisplayPresetCmd),
}

#[derive(Parser, Debug)]
pub struct CreateCmd {
    /// The name of chain.
    #[arg(long, short = 'n', default_value = "Casper")]
    chain_name: String,

    /// The chain id.
    #[arg(long, short = 'i', default_value = "casper")]
    chain_id: String,

    /// The path to runtime wasm blob.
    #[arg(long, short)]
    runtime_wasm_path: PathBuf,

    /// Export chainspec as raw storage.
    #[arg(long, short = 's')]
    raw_storage: bool,

    /// Verify the genesis config. This silently generates the raw storage from
    /// genesis config. Any  errors will be reported.
    #[arg(long, short = 'v')]
    verify: bool,

    #[command(subcommand)]
    action: GenesisBuildAction,
}

#[derive(Subcommand, Debug, Clone)]
enum GenesisBuildAction {
    Patch(PatchCmd),
    Full(FullCmd),
    Default(DefaultCmd),
    NamedPreset(NamedPresetCmd),
}

/// Pathches the runtime's default genesis config with provided patch.
#[derive(Parser, Debug, Clone)]
struct PatchCmd {
    /// The path to the full runtime genesis config json file.
    patch_path: PathBuf,
}

/// Build the genesis config for runtime using provided json file.
/// No defaults will be used.
#[derive(Parser, Debug, Clone)]
struct FullCmd {
    /// The path to the full runtime genesis config json file.
    config_path: PathBuf,
}

/// Gets the default genesis config for the runtime and uses it in ChainSpec.
/// Please note that default genesis config may not be valid. For some runtimes
/// initial values should be added there (e.g. session keys, babe epoch).
#[derive(Parser, Debug, Clone)]
struct DefaultCmd {}

/// Uses named preset provided by runtime to build the chain spec.
#[derive(Parser, Debug, Clone)]
struct NamedPresetCmd {
    preset_name: String,
}

/// Updates the coe on the provided input chain spec.
///
/// The code field of the chain spec will be updated with the runtime provided
/// in the command line. The operation supports both plain and raw formats.
///
/// This command does not update chain-spec file in-place. The result of this
/// command will be stored in a file given as `-c/--chain-spec-path` command
/// line argument.
#[derive(Parser, Debug, Clone)]
pub struct UpdateCodeCmd {
    /// Chain spec to be updated.
    ///
    /// Please note that the file will not be updated in-place.
    pub input_chain_spec: PathBuf,
    /// The path to new runtime wasm blob to be stored into chain-spec.
    pub runtime_wasm_path: PathBuf,
}

/// Converts the given chain spec into raw format.
#[derive(Parser, Debug, Clone)]
pub struct ConvertToRawCmd {
    /// Chain spec to be converted.
    pub input_chain_spec: PathBuf,
}

/// Lists avaiable presets.
#[derive(Parser, Debug, Clone)]
pub struct ListPresetsCmd {
    /// The path to runtime wasm blob.
    #[arg(long, short)]
    pub runtime_wasm_path: PathBuf,
}

/// Displays given preset.
#[derive(Parser, Debug, Clone)]
pub struct DisplayPresetCmd {
    /// The path to runtime wasm blob.
    #[arg(long, short)]
    pub runtime_wasm_path: PathBuf,

    /// Preset to be displayed. If none if given default will be displayed.
    #[arg(long, short)]
    pub preset_name: Option<String>,
}

/// Verifies the provided input chain spec.
///
/// Silently checks if given input chain spec can be converted to raw. It allows
/// to check if all `RuntimeGenesisConfig` fields are properly initialized and
/// if the json does not contain invalid fields.
#[derive(Parser, Debug, Clone)]
pub struct VerifyCmd {
    /// Chain spec to be verified.
    pub input_chain_spec: PathBuf,
}

/// Processes `CreateCmd` and returns JSON version of `ChainSpec`.
pub fn generate_chain_spec_for_runtime(cmd: &CreateCmd) -> Result<String, String> {
    let code = fs::read(cmd.runtime_wasm_path.as_path())
        .map_err(|e| format!("wasm blob shall be readable {e}"))?;

    let builder = GenericChainSpec::<()>::builder(&code[..], Default::default())
        .with_name(&cmd.chain_name[..])
        .with_id(&cmd.chain_id[..])
        .with_chain_type(ChainType::Live);

    let builder = match cmd.action {
        GenesisBuildAction::NamedPreset(NamedPresetCmd { ref preset_name }) => {
            builder.with_genesis_config_preset_name(&preset_name)
        }
        GenesisBuildAction::Patch(PatchCmd { ref patch_path }) => {
            let patch = fs::read(patch_path.as_path())
                .map_err(|e| format!("patch file {patch_path:?} shall be readable: {e}"))?;
            builder.with_genesis_config_patch(serde_json::from_slice::<Value>(&patch[..]).map_err(
                |e| format!("patch file {patch_path:?} shall contain a valid json: {e}"),
            )?)
        }
        GenesisBuildAction::Full(FullCmd { ref config_path }) => {
            let config = fs::read(config_path.as_path())
                .map_err(|e| format!("config file {config_path:?} shall be readable: {e}"))?;
            builder.with_genesis_config(serde_json::from_slice::<Value>(&config[..]).map_err(
                |e| format!("config file {config_path:?} shall contain a valid json: {e}"),
            )?)
        }
        GenesisBuildAction::Default(DefaultCmd {}) => {
            let caller: GenesisConfigBuilderRuntimeCaller =
                GenesisConfigBuilderRuntimeCaller::new(&code[..]);
            let default_config = caller
                .get_default_config()
                .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
            builder.with_genesis_config(default_config)
        }
    };

    let chain_spec = builder.build();

    match (cmd.verify, cmd.raw_storage) {
        (_, true) => chain_spec.as_json(true),
        (true, false) => {
            chain_spec.as_json(true)?;
            println!("Genesis config verification: OK");
            chain_spec.as_json(false)
        }
        (false, false) => chain_spec.as_json(false),
    }
}
