use clap::Parser;
use eyre::Result;
use foundry_cli::json::print_json_success;
use foundry_common::{fs, sh_err, sh_println, shell};
use foundry_config::Config;
use foundry_wallets::wallet_multi::MultiWalletOptsBuilder;
use serde::Serialize;
use std::{borrow::Cow, env};

/// CLI arguments for `cast wallet list`.
#[derive(Clone, Debug, Parser)]
pub struct ListArgs {
    /// List all the accounts in the keystore directory.
    /// Default keystore directory is used if no path provided.
    #[arg(long, default_missing_value = "", num_args(0..=1))]
    dir: Option<String>,

    /// List accounts from a Ledger hardware wallet.
    #[arg(long, short, group = "hw-wallets")]
    ledger: bool,

    /// List accounts from a Trezor hardware wallet.
    #[arg(long, short, group = "hw-wallets")]
    trezor: bool,

    /// List accounts from AWS KMS.
    ///
    /// Ensure either one of AWS_KMS_KEY_IDS (comma-separated) or AWS_KMS_KEY_ID environment
    /// variables are set.
    #[arg(long, hide = !cfg!(feature = "aws-kms"))]
    aws: bool,

    /// List accounts from Google Cloud KMS.
    ///
    /// Ensure the following environment variables are set: GCP_PROJECT_ID, GCP_LOCATION,
    /// GCP_KEY_RING, GCP_KEY_NAME, GCP_KEY_VERSION.
    ///
    /// See: <https://cloud.google.com/kms/docs>
    #[arg(long, hide = !cfg!(feature = "gcp-kms"))]
    gcp: bool,

    /// List accounts from Turnkey.
    #[arg(long, hide = !cfg!(feature = "turnkey"))]
    turnkey: bool,

    /// List all configured accounts.
    #[arg(long, group = "hw-wallets")]
    all: bool,

    /// Max number of addresses to display from hardware wallets.
    #[arg(long, short, default_value = "3", requires = "hw-wallets")]
    max_senders: Option<usize>,
}

impl ListArgs {
    pub async fn run(self) -> Result<()> {
        let format_json = shell::is_json();
        let mut accounts: Vec<WalletAccount> = Vec::new();

        // list local accounts as files in keystore dir, no need to unlock / provide password
        if self.dir.is_some()
            || self.all
            || (!self.ledger && !self.trezor && !self.aws && !self.gcp)
        {
            match self.list_local_senders(format_json) {
                Ok(local) => accounts.extend(local),
                Err(e) if !self.all => {
                    sh_err!("{}", e)?;
                }
                _ => {}
            }
        }

        // Create options for multi wallet - ledger, trezor and AWS
        let list_opts = MultiWalletOptsBuilder::default()
            .ledger(self.ledger || self.all)
            .mnemonic_indexes(Some(vec![0]))
            .trezor(self.trezor || self.all)
            .aws(self.aws || self.all)
            .gcp(self.gcp || (self.all && gcp_env_vars_set()))
            .turnkey(self.turnkey || self.all)
            .interactives(0)
            .interactive(false)
            .browser(Default::default())
            .build()
            .expect("build multi wallet");

        // macro to collect or print senders for a list of signers
        macro_rules! list_senders {
            ($signers:expr, $label:literal) => {
                match $signers.await {
                    Ok(signers) => {
                        for signer in signers.unwrap_or_default().iter() {
                            for sender in
                                signer.available_senders(self.max_senders.unwrap()).await?
                            {
                                if format_json {
                                    accounts.push(WalletAccount {
                                        address: sender.to_string(),
                                        source: $label,
                                    });
                                } else {
                                    let _ = sh_println!("{} ({})", sender, $label);
                                }
                            }
                        }
                    }
                    Err(e) if !self.all => {
                        sh_err!("{}", e)?;
                    }
                    _ => {}
                }
            };
        }

        list_senders!(list_opts.ledgers(), "Ledger");
        list_senders!(list_opts.trezors(), "Trezor");
        list_senders!(list_opts.aws_signers(), "AWS");
        list_senders!(list_opts.gcp_signers(), "GCP");

        if format_json {
            print_json_success(accounts)?;
        }

        Ok(())
    }

    fn list_local_senders(&self, format_json: bool) -> Result<Vec<WalletAccount>> {
        let keystore_path = self.dir.as_deref().unwrap_or_default();
        let keystore_dir = if keystore_path.is_empty() {
            // Create the keystore default directory if it doesn't exist
            let default_dir = Config::foundry_keystores_dir().unwrap();
            fs::create_dir_all(&default_dir)?;
            default_dir
        } else {
            dunce::canonicalize(keystore_path)?
        };

        let mut accounts = Vec::new();

        // List all files within the keystore directory.
        for entry in std::fs::read_dir(keystore_dir)? {
            let path = entry?.path();
            if path.is_file()
                && let Some(file_name) = path.file_name()
                && let Some(name) = file_name.to_str()
            {
                let account = local_account_name(name);
                if format_json {
                    accounts.push(WalletAccount { address: account.into_owned(), source: "Local" });
                } else {
                    sh_println!("{} (Local)", account)?;
                }
            }
        }

        Ok(accounts)
    }
}

/// Extracts the address from a Geth-style keystore filename, preserving custom names.
fn local_account_name(name: &str) -> Cow<'_, str> {
    if let Some((timestamp, address)) =
        name.strip_prefix("UTC--").and_then(|suffix| suffix.rsplit_once("--"))
        && !timestamp.is_empty()
        && address.len() == 40
        && address.bytes().all(|byte| byte.is_ascii_hexdigit())
    {
        Cow::Owned(format!("0x{address}"))
    } else {
        Cow::Borrowed(name)
    }
}

#[derive(Serialize)]
struct WalletAccount {
    address: String,
    source: &'static str,
}

fn gcp_env_vars_set() -> bool {
    let required_vars =
        ["GCP_PROJECT_ID", "GCP_LOCATION", "GCP_KEY_RING", "GCP_KEY_NAME", "GCP_KEY_VERSION"];

    required_vars.iter().all(|&var| env::var(var).is_ok())
}
