use {
    crate::{
        cli::{
            log_instruction_custom_error, request_and_confirm_airdrop, CliCommand, CliCommandInfo,
            CliConfig, CliError, ProcessResult,
        },
        memo::WithMemo,
        nonce::check_nonce_account,
        spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount},
    },
    clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand},
    solana_account_decoder::{UiAccount, UiAccountEncoding},
    solana_clap_utils::{
        fee_payer::*,
        input_parsers::*,
        input_validators::*,
        keypair::{DefaultSigner, SignerIndex},
        memo::*,
        nonce::*,
        offline::*,
    },
    solana_cli_output::{
        display::build_balance_message, return_signers_with_config, CliAccount,
        CliSignatureVerificationStatus, CliTransaction, CliTransactionConfirmation, OutputFormat,
        ReturnSignersConfig,
    },
    solana_client::{
        blockhash_query::BlockhashQuery, nonce_utils, rpc_client::RpcClient,
        rpc_config::RpcTransactionConfig, rpc_response::RpcKeyedAccount,
    },
    solana_remote_wallet::remote_wallet::RemoteWalletManager,
    solana_sdk::{
        commitment_config::CommitmentConfig,
        message::Message,
        pubkey::Pubkey,
        signature::Signature,
        stake,
        system_instruction::{self, SystemError},
        system_program,
        transaction::Transaction,
    },
    solana_transaction_status::{EncodedTransaction, UiTransactionEncoding},
    std::{fmt::Write as FmtWrite, fs::File, io::Write, sync::Arc},
};

pub trait WalletSubCommands {
    fn wallet_subcommands(self) -> Self;
}

impl WalletSubCommands for App<'_, '_> {
    fn wallet_subcommands(self) -> Self {
        self.subcommand(
            SubCommand::with_name("account")
                .about("Show the contents of an account")
                .alias("account")
                .arg(
                    pubkey!(Arg::with_name("account_pubkey")
                        .index(1)
                        .value_name("ACCOUNT_ADDRESS")
                        .required(true),
                        "Account key URI. ")
                )
                .arg(
                    Arg::with_name("output_file")
                        .long("output-file")
                        .short("o")
                        .value_name("FILEPATH")
                        .takes_value(true)
                        .help("Write the account data to this file"),
                )
                .arg(
                    Arg::with_name("lamports")
                        .long("lamports")
                        .takes_value(false)
                        .help("Display balance in lamports instead of SOL"),
                ),
        )
        .subcommand(
            SubCommand::with_name("address")
                .about("Get your public key")
                .arg(
                    Arg::with_name("confirm_key")
                        .long("confirm-key")
                        .takes_value(false)
                        .help("Confirm key on device; only relevant if using remote wallet"),
                ),
        )
        .subcommand(
            SubCommand::with_name("airdrop")
                .about("Request SOL from a faucet")
                .arg(
                    Arg::with_name("amount")
                        .index(1)
                        .value_name("AMOUNT")
                        .takes_value(true)
                        .validator(is_amount)
                        .required(true)
                        .help("The airdrop amount to request, in SOL"),
                )
                .arg(
                    pubkey!(Arg::with_name("to")
                        .index(2)
                        .value_name("RECIPIENT_ADDRESS"),
                        "The account address of airdrop recipient. "),
                ),
        )
        .subcommand(
            SubCommand::with_name("balance")
                .about("Get your balance")
                .arg(
                    pubkey!(Arg::with_name("pubkey")
                        .index(1)
                        .value_name("ACCOUNT_ADDRESS"),
                        "The account address of the balance to check. ")
                )
                .arg(
                    Arg::with_name("lamports")
                        .long("lamports")
                        .takes_value(false)
                        .help("Display balance in lamports instead of SOL"),
                ),
        )
        .subcommand(
            SubCommand::with_name("confirm")
                .about("Confirm transaction by signature")
                .arg(
                    Arg::with_name("signature")
                        .index(1)
                        .value_name("TRANSACTION_SIGNATURE")
                        .takes_value(true)
                        .required(true)
                        .help("The transaction signature to confirm"),
                )
                .after_help(// Formatted specifically for the manually-indented heredoc string
                   "Note: This will show more detailed information for finalized transactions with verbose mode (-v/--verbose).\
                  \n\
                  \nAccount modes:\
                  \n  |srwx|\
                  \n    s: signed\
                  \n    r: readable (always true)\
                  \n    w: writable\
                  \n    x: program account (inner instructions excluded)\
                   "
                ),
        )
        .subcommand(
            SubCommand::with_name("create-address-with-seed")
                .about("Generate a derived account address with a seed")
                .arg(
                    Arg::with_name("seed")
                        .index(1)
                        .value_name("SEED_STRING")
                        .takes_value(true)
                        .required(true)
                        .validator(is_derived_address_seed)
                        .help("The seed.  Must not take more than 32 bytes to encode as utf-8"),
                )
                .arg(
                    Arg::with_name("program_id")
                        .index(2)
                        .value_name("PROGRAM_ID")
                        .takes_value(true)
                        .required(true)
                        .help(
                            "The program_id that the address will ultimately be used for, \n\
                             or one of NONCE, STAKE, and VOTE keywords",
                        ),
                )
                .arg(
                    pubkey!(Arg::with_name("from")
                        .long("from")
                        .value_name("FROM_PUBKEY")
                        .required(false),
                        "From (base) key, [default: cli config keypair]. "),
                ),
        )
        .subcommand(
            SubCommand::with_name("decode-transaction")
                .about("Decode a serialized transaction")
                .arg(
                    Arg::with_name("transaction")
                        .index(1)
                        .value_name("TRANSACTION")
                        .takes_value(true)
                        .required(true)
                        .help("transaction to decode"),
                )
                .arg(
                    Arg::with_name("encoding")
                        .index(2)
                        .value_name("ENCODING")
                        .possible_values(&["base58", "base64"]) // Subset of `UiTransactionEncoding` enum
                        .default_value("base58")
                        .takes_value(true)
                        .required(true)
                        .help("transaction encoding"),
                ),
        )
        .subcommand(
            SubCommand::with_name("resolve-signer")
                .about("Checks that a signer is valid, and returns its specific path; useful for signers that may be specified generally, eg. usb://ledger")
                .arg(
                    Arg::with_name("signer")
                        .index(1)
                        .value_name("SIGNER_KEYPAIR")
                        .takes_value(true)
                        .required(true)
                        .validator(is_valid_signer)
                        .help("The signer path to resolve")
                )
        )
        .subcommand(
            SubCommand::with_name("transfer")
                .about("Transfer funds between system accounts")
                .alias("pay")
                .arg(
                    pubkey!(Arg::with_name("to")
                        .index(1)
                        .value_name("RECIPIENT_ADDRESS")
                        .required(true),
                        "The account address of recipient. "),
                )
                .arg(
                    Arg::with_name("amount")
                        .index(2)
                        .value_name("AMOUNT")
                        .takes_value(true)
                        .validator(is_amount_or_all)
                        .required(true)
                        .help("The amount to send, in SOL; accepts keyword ALL"),
                )
                .arg(
                    pubkey!(Arg::with_name("from")
                        .long("from")
                        .value_name("FROM_ADDRESS"),
                        "Source account of funds (if different from client local account). "),
                )
                .arg(
                    Arg::with_name("no_wait")
                        .long("no-wait")
                        .takes_value(false)
                        .help("Return signature immediately after submitting the transaction, instead of waiting for confirmations"),
                )
                .arg(
                    Arg::with_name("derived_address_seed")
                        .long("derived-address-seed")
                        .takes_value(true)
                        .value_name("SEED_STRING")
                        .requires("derived_address_program_id")
                        .validator(is_derived_address_seed)
                        .hidden(true)
                )
                .arg(
                    Arg::with_name("derived_address_program_id")
                        .long("derived-address-program-id")
                        .takes_value(true)
                        .value_name("PROGRAM_ID")
                        .requires("derived_address_seed")
                        .hidden(true)
                )
                .arg(
                    Arg::with_name("allow_unfunded_recipient")
                        .long("allow-unfunded-recipient")
                        .takes_value(false)
                        .help("Complete the transfer even if the recipient address is not funded")
                )
                .offline_args()
                .nonce_args(false)
                .arg(memo_arg())
                .arg(fee_payer_arg()),
        )
    }
}

fn resolve_derived_address_program_id(matches: &ArgMatches<'_>, arg_name: &str) -> Option<Pubkey> {
    matches.value_of(arg_name).and_then(|v| match v {
        "NONCE" => Some(system_program::id()),
        "STAKE" => Some(stake::program::id()),
        "VOTE" => Some(solana_vote_program::id()),
        _ => pubkey_of(matches, arg_name),
    })
}

pub fn parse_account(
    matches: &ArgMatches<'_>,
    wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
    let account_pubkey = pubkey_of_signer(matches, "account_pubkey", wallet_manager)?.unwrap();
    let output_file = matches.value_of("output_file");
    let use_lamports_unit = matches.is_present("lamports");
    Ok(CliCommandInfo {
        command: CliCommand::ShowAccount {
            pubkey: account_pubkey,
            output_file: output_file.map(ToString::to_string),
            use_lamports_unit,
        },
        signers: vec![],
    })
}

pub fn parse_airdrop(
    matches: &ArgMatches<'_>,
    default_signer: &DefaultSigner,
    wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
    let pubkey = pubkey_of_signer(matches, "to", wallet_manager)?;
    let signers = if pubkey.is_some() {
        vec![]
    } else {
        vec![default_signer.signer_from_path(matches, wallet_manager)?]
    };
    let lamports = lamports_of_sol(matches, "amount").unwrap();
    Ok(CliCommandInfo {
        command: CliCommand::Airdrop { pubkey, lamports },
        signers,
    })
}

pub fn parse_balance(
    matches: &ArgMatches<'_>,
    default_signer: &DefaultSigner,
    wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
    let pubkey = pubkey_of_signer(matches, "pubkey", wallet_manager)?;
    let signers = if pubkey.is_some() {
        vec![]
    } else {
        vec![default_signer.signer_from_path(matches, wallet_manager)?]
    };
    Ok(CliCommandInfo {
        command: CliCommand::Balance {
            pubkey,
            use_lamports_unit: matches.is_present("lamports"),
        },
        signers,
    })
}

pub fn parse_decode_transaction(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
    let blob = value_t_or_exit!(matches, "transaction", String);
    let encoding = match matches.value_of("encoding").unwrap() {
        "base58" => UiTransactionEncoding::Base58,
        "base64" => UiTransactionEncoding::Base64,
        _ => unreachable!(),
    };

    let encoded_transaction = EncodedTransaction::Binary(blob, encoding);
    if let Some(transaction) = encoded_transaction.decode() {
        Ok(CliCommandInfo {
            command: CliCommand::DecodeTransaction(transaction),
            signers: vec![],
        })
    } else {
        Err(CliError::BadParameter(
            "Unable to decode transaction".to_string(),
        ))
    }
}

pub fn parse_create_address_with_seed(
    matches: &ArgMatches<'_>,
    default_signer: &DefaultSigner,
    wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
    let from_pubkey = pubkey_of_signer(matches, "from", wallet_manager)?;
    let signers = if from_pubkey.is_some() {
        vec![]
    } else {
        vec![default_signer.signer_from_path(matches, wallet_manager)?]
    };

    let program_id = resolve_derived_address_program_id(matches, "program_id").unwrap();

    let seed = matches.value_of("seed").unwrap().to_string();

    Ok(CliCommandInfo {
        command: CliCommand::CreateAddressWithSeed {
            from_pubkey,
            seed,
            program_id,
        },
        signers,
    })
}

pub fn parse_transfer(
    matches: &ArgMatches<'_>,
    default_signer: &DefaultSigner,
    wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
    let amount = SpendAmount::new_from_matches(matches, "amount");
    let to = pubkey_of_signer(matches, "to", wallet_manager)?.unwrap();
    let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
    let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name);
    let no_wait = matches.is_present("no_wait");
    let blockhash_query = BlockhashQuery::new_from_matches(matches);
    let nonce_account = pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager)?;
    let (nonce_authority, nonce_authority_pubkey) =
        signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?;
    let memo = matches.value_of(MEMO_ARG.name).map(String::from);
    let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
    let (from, from_pubkey) = signer_of(matches, "from", wallet_manager)?;
    let allow_unfunded_recipient = matches.is_present("allow_unfunded_recipient");

    let mut bulk_signers = vec![fee_payer, from];
    if nonce_account.is_some() {
        bulk_signers.push(nonce_authority);
    }

    let signer_info =
        default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;

    let derived_address_seed = matches
        .value_of("derived_address_seed")
        .map(|s| s.to_string());
    let derived_address_program_id =
        resolve_derived_address_program_id(matches, "derived_address_program_id");

    Ok(CliCommandInfo {
        command: CliCommand::Transfer {
            amount,
            to,
            sign_only,
            dump_transaction_message,
            allow_unfunded_recipient,
            no_wait,
            blockhash_query,
            nonce_account,
            nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
            memo,
            fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
            from: signer_info.index_of(from_pubkey).unwrap(),
            derived_address_seed,
            derived_address_program_id,
        },
        signers: signer_info.signers,
    })
}

pub fn process_show_account(
    rpc_client: &RpcClient,
    config: &CliConfig,
    account_pubkey: &Pubkey,
    output_file: &Option<String>,
    use_lamports_unit: bool,
) -> ProcessResult {
    let account = rpc_client.get_account(account_pubkey)?;
    let data = account.data.clone();
    let cli_account = CliAccount {
        keyed_account: RpcKeyedAccount {
            pubkey: account_pubkey.to_string(),
            account: UiAccount::encode(
                account_pubkey,
                &account,
                UiAccountEncoding::Base64,
                None,
                None,
            ),
        },
        use_lamports_unit,
    };

    let mut account_string = config.output_format.formatted_string(&cli_account);

    if config.output_format == OutputFormat::Display
        || config.output_format == OutputFormat::DisplayVerbose
    {
        if let Some(output_file) = output_file {
            let mut f = File::create(output_file)?;
            f.write_all(&data)?;
            writeln!(&mut account_string)?;
            writeln!(&mut account_string, "Wrote account data to {}", output_file)?;
        } else if !data.is_empty() {
            use pretty_hex::*;
            writeln!(&mut account_string, "{:?}", data.hex_dump())?;
        }
    }

    Ok(account_string)
}

pub fn process_airdrop(
    rpc_client: &RpcClient,
    config: &CliConfig,
    pubkey: &Option<Pubkey>,
    lamports: u64,
) -> ProcessResult {
    let pubkey = if let Some(pubkey) = pubkey {
        *pubkey
    } else {
        config.pubkey()?
    };
    println!(
        "Requesting airdrop of {}",
        build_balance_message(lamports, false, true),
    );

    let pre_balance = rpc_client.get_balance(&pubkey)?;

    let result = request_and_confirm_airdrop(rpc_client, config, &pubkey, lamports);
    if let Ok(signature) = result {
        let signature_cli_message = log_instruction_custom_error::<SystemError>(result, config)?;
        println!("{}", signature_cli_message);

        let current_balance = rpc_client.get_balance(&pubkey)?;

        if current_balance < pre_balance.saturating_add(lamports) {
            println!("Balance unchanged");
            println!("Run `solana confirm -v {:?}` for more info", signature);
            Ok("".to_string())
        } else {
            Ok(build_balance_message(current_balance, false, true))
        }
    } else {
        log_instruction_custom_error::<SystemError>(result, config)
    }
}

pub fn process_balance(
    rpc_client: &RpcClient,
    config: &CliConfig,
    pubkey: &Option<Pubkey>,
    use_lamports_unit: bool,
) -> ProcessResult {
    let pubkey = if let Some(pubkey) = pubkey {
        *pubkey
    } else {
        config.pubkey()?
    };
    let balance = rpc_client.get_balance(&pubkey)?;
    Ok(build_balance_message(balance, use_lamports_unit, true))
}

pub fn process_confirm(
    rpc_client: &RpcClient,
    config: &CliConfig,
    signature: &Signature,
) -> ProcessResult {
    match rpc_client.get_signature_statuses_with_history(&[*signature]) {
        Ok(status) => {
            let cli_transaction = if let Some(transaction_status) = &status.value[0] {
                let mut transaction = None;
                let mut get_transaction_error = None;
                if config.verbose {
                    match rpc_client.get_transaction_with_config(
                        signature,
                        RpcTransactionConfig {
                            encoding: Some(UiTransactionEncoding::Base64),
                            commitment: Some(CommitmentConfig::confirmed()),
                        },
                    ) {
                        Ok(confirmed_transaction) => {
                            let decoded_transaction = confirmed_transaction
                                .transaction
                                .transaction
                                .decode()
                                .expect("Successful decode");
                            let json_transaction = EncodedTransaction::encode(
                                decoded_transaction.clone(),
                                UiTransactionEncoding::Json,
                            );

                            transaction = Some(CliTransaction {
                                transaction: json_transaction,
                                meta: confirmed_transaction.transaction.meta,
                                block_time: confirmed_transaction.block_time,
                                slot: Some(confirmed_transaction.slot),
                                decoded_transaction,
                                prefix: "  ".to_string(),
                                sigverify_status: vec![],
                            });
                        }
                        Err(err) => {
                            get_transaction_error = Some(format!("{:?}", err));
                        }
                    }
                }
                CliTransactionConfirmation {
                    confirmation_status: Some(transaction_status.confirmation_status()),
                    transaction,
                    get_transaction_error,
                    err: transaction_status.err.clone(),
                }
            } else {
                CliTransactionConfirmation {
                    confirmation_status: None,
                    transaction: None,
                    get_transaction_error: None,
                    err: None,
                }
            };
            Ok(config.output_format.formatted_string(&cli_transaction))
        }
        Err(err) => Err(CliError::RpcRequestError(format!("Unable to confirm: {}", err)).into()),
    }
}

#[allow(clippy::unnecessary_wraps)]
pub fn process_decode_transaction(config: &CliConfig, transaction: &Transaction) -> ProcessResult {
    let sigverify_status = CliSignatureVerificationStatus::verify_transaction(transaction);
    let decode_transaction = CliTransaction {
        decoded_transaction: transaction.clone(),
        transaction: EncodedTransaction::encode(transaction.clone(), UiTransactionEncoding::Json),
        meta: None,
        block_time: None,
        slot: None,
        prefix: "".to_string(),
        sigverify_status,
    };
    Ok(config.output_format.formatted_string(&decode_transaction))
}

pub fn process_create_address_with_seed(
    config: &CliConfig,
    from_pubkey: Option<&Pubkey>,
    seed: &str,
    program_id: &Pubkey,
) -> ProcessResult {
    let from_pubkey = if let Some(pubkey) = from_pubkey {
        *pubkey
    } else {
        config.pubkey()?
    };
    let address = Pubkey::create_with_seed(&from_pubkey, seed, program_id)?;
    Ok(address.to_string())
}

#[allow(clippy::too_many_arguments)]
pub fn process_transfer(
    rpc_client: &RpcClient,
    config: &CliConfig,
    amount: SpendAmount,
    to: &Pubkey,
    from: SignerIndex,
    sign_only: bool,
    dump_transaction_message: bool,
    allow_unfunded_recipient: bool,
    no_wait: bool,
    blockhash_query: &BlockhashQuery,
    nonce_account: Option<&Pubkey>,
    nonce_authority: SignerIndex,
    memo: Option<&String>,
    fee_payer: SignerIndex,
    derived_address_seed: Option<String>,
    derived_address_program_id: Option<&Pubkey>,
) -> ProcessResult {
    let from = config.signers[from];
    let mut from_pubkey = from.pubkey();

    let (recent_blockhash, fee_calculator) =
        blockhash_query.get_blockhash_and_fee_calculator(rpc_client, config.commitment)?;

    if !sign_only && !allow_unfunded_recipient {
        let recipient_balance = rpc_client
            .get_balance_with_commitment(to, config.commitment)?
            .value;
        if recipient_balance == 0 {
            return Err(format!(
                "The recipient address ({}) is not funded. \
                                Add `--allow-unfunded-recipient` to complete the transfer \
                               ",
                to
            )
            .into());
        }
    }

    let nonce_authority = config.signers[nonce_authority];
    let fee_payer = config.signers[fee_payer];

    let derived_parts = derived_address_seed.zip(derived_address_program_id);
    let with_seed = if let Some((seed, program_id)) = derived_parts {
        let base_pubkey = from_pubkey;
        from_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, program_id)?;
        Some((base_pubkey, seed, program_id, from_pubkey))
    } else {
        None
    };

    let build_message = |lamports| {
        let ixs = if let Some((base_pubkey, seed, program_id, from_pubkey)) = with_seed.as_ref() {
            vec![system_instruction::transfer_with_seed(
                from_pubkey,
                base_pubkey,
                seed.clone(),
                program_id,
                to,
                lamports,
            )]
            .with_memo(memo)
        } else {
            vec![system_instruction::transfer(&from_pubkey, to, lamports)].with_memo(memo)
        };

        if let Some(nonce_account) = &nonce_account {
            Message::new_with_nonce(
                ixs,
                Some(&fee_payer.pubkey()),
                nonce_account,
                &nonce_authority.pubkey(),
            )
        } else {
            Message::new(&ixs, Some(&fee_payer.pubkey()))
        }
    };

    let (message, _) = resolve_spend_tx_and_check_account_balances(
        rpc_client,
        sign_only,
        amount,
        &fee_calculator,
        &from_pubkey,
        &fee_payer.pubkey(),
        build_message,
        config.commitment,
    )?;
    let mut tx = Transaction::new_unsigned(message);

    if sign_only {
        tx.try_partial_sign(&config.signers, recent_blockhash)?;
        return_signers_with_config(
            &tx,
            &config.output_format,
            &ReturnSignersConfig {
                dump_transaction_message,
            },
        )
    } else {
        if let Some(nonce_account) = &nonce_account {
            let nonce_account = nonce_utils::get_account_with_commitment(
                rpc_client,
                nonce_account,
                config.commitment,
            )?;
            check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
        }

        tx.try_sign(&config.signers, recent_blockhash)?;
        let result = if no_wait {
            rpc_client.send_transaction(&tx)
        } else {
            rpc_client.send_and_confirm_transaction_with_spinner(&tx)
        };
        log_instruction_custom_error::<SystemError>(result, config)
    }
}
