#[cfg(feature = "fakewallet")]
use std::collections::HashMap;
#[cfg(feature = "fakewallet")]
use std::collections::HashSet;
use std::path::Path;
#[cfg(feature = "ldk-node")]
use std::path::PathBuf;
use std::sync::Arc;

use async_trait::async_trait;
#[cfg(feature = "fakewallet")]
use bip39::rand::{thread_rng, Rng};
use cdk::cdk_database::KVStore;
use cdk::cdk_payment::MintPayment;
use cdk::nuts::CurrencyUnit;
#[cfg(any(
    feature = "lnbits",
    feature = "cln",
    feature = "lnd",
    feature = "ldk-node",
    feature = "fakewallet"
))]
use cdk::types::FeeReserve;

use crate::config::{self, Settings};
#[cfg(feature = "cln")]
use crate::expand_path;

#[async_trait]
pub trait LnBackendSetup {
    async fn setup(
        &self,
        settings: &Settings,
        unit: CurrencyUnit,
        runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
        work_dir: &Path,
        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
    ) -> anyhow::Result<impl MintPayment>;
}

#[cfg(feature = "cln")]
#[async_trait]
impl LnBackendSetup for config::Cln {
    async fn setup(
        &self,
        _settings: &Settings,
        _unit: CurrencyUnit,
        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
        _work_dir: &Path,
        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
    ) -> anyhow::Result<cdk_cln::Cln> {
        // Validate required connection field
        if self.rpc_path.as_os_str().is_empty() {
            return Err(anyhow::anyhow!(
                "CLN rpc_path must be set via config or CDK_MINTD_CLN_RPC_PATH env var"
            ));
        }

        let cln_socket = expand_path(
            self.rpc_path
                .to_str()
                .ok_or(anyhow::anyhow!("cln socket not defined"))?,
        )
        .ok_or(anyhow::anyhow!("cln socket not defined"))?;

        let fee_reserve = FeeReserve {
            min_fee_reserve: self.reserve_fee_min,
            percent_fee_reserve: self.fee_percent,
        };

        let cln = cdk_cln::Cln::new(
            cln_socket,
            fee_reserve,
            kv_store.expect("Cln needs kv store"),
        )
        .await?;

        Ok(cln)
    }
}

#[cfg(feature = "lnbits")]
#[async_trait]
impl LnBackendSetup for config::LNbits {
    async fn setup(
        &self,
        _settings: &Settings,
        _unit: CurrencyUnit,
        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
        _work_dir: &Path,
        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
    ) -> anyhow::Result<cdk_lnbits::LNbits> {
        use anyhow::bail;

        // Validate required connection fields
        if self.admin_api_key.is_empty() {
            bail!("LNbits admin_api_key must be set via config or CDK_MINTD_LNBITS_ADMIN_API_KEY env var");
        }
        if self.invoice_api_key.is_empty() {
            bail!("LNbits invoice_api_key must be set via config or CDK_MINTD_LNBITS_INVOICE_API_KEY env var");
        }
        if self.lnbits_api.is_empty() {
            bail!(
                "LNbits lnbits_api must be set via config or CDK_MINTD_LNBITS_LNBITS_API env var"
            );
        }

        let admin_api_key = &self.admin_api_key;
        let invoice_api_key = &self.invoice_api_key;

        let fee_reserve = FeeReserve {
            min_fee_reserve: self.reserve_fee_min,
            percent_fee_reserve: self.fee_percent,
        };

        let lnbits = cdk_lnbits::LNbits::new(
            admin_api_key.clone(),
            invoice_api_key.clone(),
            self.lnbits_api.clone(),
            fee_reserve,
        )
        .await?;

        // Use v1 websocket API
        lnbits.subscribe_ws().await?;

        Ok(lnbits)
    }
}

#[cfg(feature = "lnd")]
#[async_trait]
impl LnBackendSetup for config::Lnd {
    async fn setup(
        &self,
        _settings: &Settings,
        _unit: CurrencyUnit,
        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
        _work_dir: &Path,
        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
    ) -> anyhow::Result<cdk_lnd::Lnd> {
        use anyhow::bail;
        // Validate required connection fields
        if self.address.is_empty() {
            bail!("LND address must be set via config or CDK_MINTD_LND_ADDRESS env var");
        }
        if self.cert_file.as_os_str().is_empty() {
            bail!("LND cert_file must be set via config or CDK_MINTD_LND_CERT_FILE env var");
        }
        if self.macaroon_file.as_os_str().is_empty() {
            bail!(
                "LND macaroon_file must be set via config or CDK_MINTD_LND_MACAROON_FILE env var"
            );
        }

        let address = &self.address;
        let cert_file = &self.cert_file;
        let macaroon_file = &self.macaroon_file;

        let fee_reserve = FeeReserve {
            min_fee_reserve: self.reserve_fee_min,
            percent_fee_reserve: self.fee_percent,
        };

        let lnd = cdk_lnd::Lnd::new(
            address.to_string(),
            cert_file.clone(),
            macaroon_file.clone(),
            fee_reserve,
            kv_store.expect("Lnd needs kv store"),
        )
        .await?;

        Ok(lnd)
    }
}

#[cfg(feature = "fakewallet")]
#[async_trait]
impl LnBackendSetup for config::FakeWallet {
    async fn setup(
        &self,
        _settings: &Settings,
        unit: CurrencyUnit,
        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
        _work_dir: &Path,
        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
    ) -> anyhow::Result<cdk_fake_wallet::FakeWallet> {
        let fee_reserve = FeeReserve {
            min_fee_reserve: self.reserve_fee_min,
            percent_fee_reserve: self.fee_percent,
        };

        // calculate random delay time
        let mut rng = thread_rng();
        let delay_time = rng.gen_range(self.min_delay_time..=self.max_delay_time);

        let fake_wallet = cdk_fake_wallet::FakeWallet::new(
            fee_reserve,
            HashMap::default(),
            HashSet::default(),
            delay_time,
            unit,
        );

        Ok(fake_wallet)
    }
}

#[cfg(feature = "grpc-processor")]
#[async_trait]
impl LnBackendSetup for config::GrpcProcessor {
    async fn setup(
        &self,
        _settings: &Settings,
        _unit: CurrencyUnit,
        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
        _work_dir: &Path,
        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
    ) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
        let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
            &self.addr,
            self.port,
            self.tls_dir.clone(),
        )
        .await?;

        Ok(payment_processor)
    }
}

#[cfg(feature = "ldk-node")]
#[async_trait]
impl LnBackendSetup for config::LdkNode {
    async fn setup(
        &self,
        settings: &Settings,
        _unit: CurrencyUnit,
        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
        work_dir: &Path,
        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
    ) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
        use std::net::SocketAddr;

        use anyhow::bail;
        use bip39::Mnemonic;
        use bitcoin::Network;

        let fee_reserve = FeeReserve {
            min_fee_reserve: self.reserve_fee_min,
            percent_fee_reserve: self.fee_percent,
        };

        // Parse network from config
        let network = match self
            .bitcoin_network
            .as_ref()
            .map(|n| n.to_lowercase())
            .as_deref()
            .unwrap_or("regtest")
        {
            "mainnet" | "bitcoin" => Network::Bitcoin,
            "testnet" => Network::Testnet,
            "signet" => Network::Signet,
            _ => Network::Regtest,
        };

        // Parse chain source from config
        let chain_source = match self
            .chain_source_type
            .as_ref()
            .map(|s| s.to_lowercase())
            .as_deref()
            .unwrap_or("esplora")
        {
            "bitcoinrpc" => {
                let host = self
                    .bitcoind_rpc_host
                    .clone()
                    .unwrap_or_else(|| "127.0.0.1".to_string());
                let port = self.bitcoind_rpc_port.unwrap_or(18443);
                let user = self
                    .bitcoind_rpc_user
                    .clone()
                    .unwrap_or_else(|| "testuser".to_string());
                let password = self
                    .bitcoind_rpc_password
                    .clone()
                    .unwrap_or_else(|| "testpass".to_string());

                cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
                    host,
                    port,
                    user,
                    password,
                })
            }
            _ => {
                let esplora_url = self
                    .esplora_url
                    .clone()
                    .unwrap_or_else(|| "https://mutinynet.com/api".to_string());
                cdk_ldk_node::ChainSource::Esplora(esplora_url)
            }
        };

        // Parse gossip source from config
        let gossip_source = match self.rgs_url.clone() {
            Some(rgs_url) => cdk_ldk_node::GossipSource::RapidGossipSync(rgs_url),
            None => cdk_ldk_node::GossipSource::P2P,
        };

        // Get storage directory path
        let storage_dir_path = if let Some(dir_path) = &self.storage_dir_path {
            dir_path.clone()
        } else {
            let mut work_dir = work_dir.to_path_buf();
            work_dir.push("ldk-node");
            work_dir.to_string_lossy().to_string()
        };

        // Get LDK node listen address
        let host = self
            .ldk_node_host
            .clone()
            .unwrap_or_else(|| "127.0.0.1".to_string());
        let port = self.ldk_node_port.unwrap_or(8090);

        let socket_addr = SocketAddr::new(host.parse()?, port);

        // Parse socket address using ldk_node's SocketAddress
        // We need to get the actual socket address struct from ldk_node
        // For now, let's construct it manually based on the cdk-ldk-node implementation
        let listen_address = vec![socket_addr.into()];

        // Check if ldk_node_mnemonic is provided in the ldk_node config
        let mnemonic_opt = settings
            .clone()
            .ldk_node
            .as_ref()
            .and_then(|ldk_config| ldk_config.ldk_node_mnemonic.clone());

        // Only set seed if mnemonic is explicitly provided
        // This maintains backward compatibility with existing nodes that use LDK's default seed storage
        let seed = if let Some(mnemonic_str) = mnemonic_opt {
            Some(
                mnemonic_str
                    .parse::<Mnemonic>()
                    .map_err(|e| anyhow::anyhow!("invalid ldk_node_mnemonic in config: {e}"))?,
            )
        } else {
            // Check if this is a new node or an existing node
            let storage_dir = PathBuf::from(&storage_dir_path);
            let keys_seed_file = storage_dir.join("keys_seed");

            if !keys_seed_file.exists() {
                bail!("ldk_node_mnemonic should be set in the [ldk_node] configuration section.");
            }

            // Existing node with stored seed, don't set a mnemonic
            None
        };

        let ldk_node_settings = settings
            .ldk_node
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("ldk_node configuration is required"))?;
        let announce_addrs: Vec<_> = ldk_node_settings
            .ldk_node_announce_addresses
            .as_ref()
            .map(|addrs| addrs.iter().filter_map(|addr| addr.parse().ok()).collect())
            .unwrap_or_default();

        let mut ldk_node_builder = cdk_ldk_node::CdkLdkNodeBuilder::new(
            network,
            chain_source,
            gossip_source,
            storage_dir_path,
            fee_reserve,
            listen_address,
        );

        // Only set seed if provided
        if let Some(mnemonic) = seed {
            ldk_node_builder = ldk_node_builder.with_seed(mnemonic);
        }

        if !announce_addrs.is_empty() {
            ldk_node_builder = ldk_node_builder.with_announcement_address(announce_addrs)
        }
        // Configure webserver address if specified
        let webserver_addr = if let Some(host) = &self.webserver_host {
            let port = self.webserver_port.unwrap_or(8091);
            let socket_addr: SocketAddr = format!("{host}:{port}").parse()?;
            Some(socket_addr)
        } else if self.webserver_port.is_some() {
            // If only port is specified, use default host
            let port = self.webserver_port.unwrap_or(8091);
            let socket_addr: SocketAddr = format!("127.0.0.1:{port}").parse()?;
            Some(socket_addr)
        } else {
            // Use default webserver address if nothing is configured
            Some(cdk_ldk_node::CdkLdkNode::default_web_addr())
        };

        println!(
            "webserver: {}",
            webserver_addr.map_or("none".to_string(), |a| a.to_string())
        );
        if let Some(log_dir_path) = ldk_node_settings.log_dir_path.as_ref() {
            ldk_node_builder = ldk_node_builder.with_log_dir_path(log_dir_path.clone());
        }
        let mut ldk_node = ldk_node_builder.build()?;
        ldk_node.set_web_addr(webserver_addr);

        Ok(ldk_node)
    }
}
