//! FFI Wallet bindings

use std::str::FromStr;
use std::sync::Arc;

use bip39::Mnemonic;
use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};

use crate::error::FfiError;
use crate::token::Token;
use crate::types::payment_request::PaymentRequest;
use crate::types::*;

/// FFI-compatible Wallet

#[derive(uniffi::Object)]
pub struct Wallet {
    inner: Arc<CdkWallet>,
}

impl Wallet {
    /// Create a Wallet from an existing CDK wallet (internal use only)
    pub(crate) fn from_inner(inner: Arc<CdkWallet>) -> Self {
        Self { inner }
    }
}

#[uniffi::export(async_runtime = "tokio")]
impl Wallet {
    /// Create a new Wallet from mnemonic using WalletDatabaseFfi trait
    #[uniffi::constructor]
    pub fn new(
        mint_url: String,
        unit: CurrencyUnit,
        mnemonic: String,
        db: Arc<dyn crate::database::WalletDatabase>,
        config: WalletConfig,
    ) -> Result<Self, FfiError> {
        // Parse mnemonic and generate seed without passphrase
        let m = Mnemonic::parse(&mnemonic)
            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
        let seed = m.to_seed_normalized("");
        tracing::info!("creating ffi wallet");
        // Convert the FFI database trait to a CDK database implementation
        let localstore = crate::database::create_cdk_database_from_ffi(db);

        let wallet = CdkWalletBuilder::new()
            .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| {
                FfiError::internal(format!("Invalid URL: {}", e))
            })?)
            .unit(unit.into())
            .localstore(localstore)
            .seed(seed)
            .target_proof_count(config.target_proof_count.unwrap_or(3) as usize)
            .build()
            .map_err(FfiError::from)?;

        Ok(Self {
            inner: Arc::new(wallet),
        })
    }

    /// Get the mint URL
    pub fn mint_url(&self) -> MintUrl {
        self.inner.mint_url.clone().into()
    }

    /// Get the currency unit
    pub fn unit(&self) -> CurrencyUnit {
        self.inner.unit.clone().into()
    }

    /// Set metadata cache TTL (time-to-live) in seconds
    ///
    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
    /// before requiring a refresh from the mint server.
    ///
    /// # Arguments
    ///
    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires and is always used.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Cache expires after 5 minutes
    /// wallet.set_metadata_cache_ttl(Some(300));
    ///
    /// // Cache never expires (default)
    /// wallet.set_metadata_cache_ttl(None);
    /// ```
    pub fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>) {
        let ttl = ttl_secs.map(std::time::Duration::from_secs);
        self.inner.set_metadata_cache_ttl(ttl);
    }

    /// Get total balance
    pub async fn total_balance(&self) -> Result<Amount, FfiError> {
        let balance = self.inner.total_balance().await?;
        Ok(balance.into())
    }

    /// Get total pending balance
    pub async fn total_pending_balance(&self) -> Result<Amount, FfiError> {
        let balance = self.inner.total_pending_balance().await?;
        Ok(balance.into())
    }

    /// Get total reserved balance
    pub async fn total_reserved_balance(&self) -> Result<Amount, FfiError> {
        let balance = self.inner.total_reserved_balance().await?;
        Ok(balance.into())
    }

    /// Get mint info from mint
    pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
        let info = self.inner.fetch_mint_info().await?;
        Ok(info.map(Into::into))
    }

    /// Load mint info
    ///
    /// This will get mint info from cache if it is fresh
    pub async fn load_mint_info(&self) -> Result<MintInfo, FfiError> {
        let info = self.inner.load_mint_info().await?;
        Ok(info.into())
    }

    /// Receive tokens
    pub async fn receive(
        &self,
        token: std::sync::Arc<Token>,
        options: ReceiveOptions,
    ) -> Result<Amount, FfiError> {
        let amount = self
            .inner
            .receive(&token.to_string(), options.into())
            .await?;
        Ok(amount.into())
    }

    /// Restore wallet from seed
    pub async fn restore(&self) -> Result<Restored, FfiError> {
        let restored = self.inner.restore().await?;
        Ok(restored.into())
    }

    /// Verify token DLEQ proofs
    pub async fn verify_token_dleq(&self, token: std::sync::Arc<Token>) -> Result<(), FfiError> {
        let cdk_token = token.inner.clone();
        self.inner.verify_token_dleq(&cdk_token).await?;
        Ok(())
    }

    /// Receive proofs directly
    pub async fn receive_proofs(
        &self,
        proofs: Proofs,
        options: ReceiveOptions,
        memo: Option<String>,
        token: Option<String>,
    ) -> Result<Amount, FfiError> {
        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
            proofs.into_iter().map(|p| p.try_into()).collect();
        let cdk_proofs = cdk_proofs?;

        let amount = self
            .inner
            .receive_proofs(cdk_proofs, options.into(), memo, token)
            .await?;
        Ok(amount.into())
    }

    /// Get all pending send operations
    pub async fn get_pending_sends(&self) -> Result<Vec<String>, FfiError> {
        let sends = self.inner.get_pending_sends().await?;
        Ok(sends.into_iter().map(|id| id.to_string()).collect())
    }

    /// Revoke a pending send operation
    pub async fn revoke_send(&self, operation_id: String) -> Result<Amount, FfiError> {
        let uuid = uuid::Uuid::parse_str(&operation_id)
            .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
        let amount = self.inner.revoke_send(uuid).await?;
        Ok(amount.into())
    }

    /// Check status of a pending send operation
    pub async fn check_send_status(&self, operation_id: String) -> Result<bool, FfiError> {
        let uuid = uuid::Uuid::parse_str(&operation_id)
            .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
        let claimed = self.inner.check_send_status(uuid).await?;
        Ok(claimed)
    }

    /// Prepare a send operation
    pub async fn prepare_send(
        &self,
        amount: Amount,
        options: SendOptions,
    ) -> Result<std::sync::Arc<PreparedSend>, FfiError> {
        let prepared = self
            .inner
            .prepare_send(amount.into(), options.into())
            .await?;
        Ok(std::sync::Arc::new(PreparedSend::new(
            self.inner.clone(),
            &prepared,
        )))
    }

    /// Get a mint quote
    pub async fn mint_quote(
        &self,
        payment_method: PaymentMethod,
        amount: Option<Amount>,
        description: Option<String>,
        extra: Option<String>,
    ) -> Result<MintQuote, FfiError> {
        let quote = self
            .inner
            .mint_quote(payment_method, amount.map(Into::into), description, extra)
            .await?;
        Ok(quote.into())
    }

    /// Check a mint quote status from the mint.
    ///
    /// Calls `GET /v1/mint/quote/{method}/{quote_id}` per NUT-04.
    /// Updates local store with current state from mint.
    /// If there was a crashed mid-mint (pending saga), attempts to complete it.
    /// Does NOT mint tokens directly - use mint() for that.
    ///
    /// **Note:** The mint quote must be known to the wallet (stored locally) for this
    /// function to work. If the quote is not stored locally, use `fetch_mint_quote`
    /// instead.
    pub async fn check_mint_quote(&self, quote_id: String) -> Result<MintQuote, FfiError> {
        let quote = self.inner.check_mint_quote_status(&quote_id).await?;
        Ok(quote.into())
    }

    /// Fetch a mint quote from the mint and store it locally
    ///
    /// Works with all payment methods (Bolt11, Bolt12, and custom payment methods).
    ///
    /// # Arguments
    /// * `quote_id` - The ID of the quote to fetch
    /// * `payment_method` - The payment method for the quote. Required if the quote
    ///   is not already stored locally. If the quote exists locally, the stored
    ///   payment method will be used and this parameter is ignored.
    pub async fn fetch_mint_quote(
        &self,
        quote_id: String,
        payment_method: Option<PaymentMethod>,
    ) -> Result<MintQuote, FfiError> {
        let method = payment_method.map(Into::into);
        let quote = self.inner.fetch_mint_quote(&quote_id, method).await?;
        Ok(quote.into())
    }

    /// Mint tokens
    pub async fn mint(
        &self,
        quote_id: String,
        amount_split_target: SplitTarget,
        spending_conditions: Option<SpendingConditions>,
    ) -> Result<Proofs, FfiError> {
        // Convert spending conditions if provided
        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;

        let proofs = self
            .inner
            .mint(&quote_id, amount_split_target.into(), conditions)
            .await?;
        Ok(proofs.into_iter().map(|p| p.into()).collect())
    }

    /// Prepare a melt operation
    ///
    /// Returns a `PreparedMelt` that can be confirmed or cancelled.
    pub async fn prepare_melt(&self, quote_id: String) -> Result<PreparedMelt, FfiError> {
        let prepared = self
            .inner
            .prepare_melt(&quote_id, std::collections::HashMap::new())
            .await?;
        Ok(PreparedMelt::new(Arc::clone(&self.inner), &prepared))
    }

    /// Prepare a melt operation with specific proofs
    ///
    /// This method allows melting proofs that may not be in the wallet's database,
    /// similar to how `receive_proofs` handles external proofs. The proofs will be
    /// added to the database and used for the melt operation.
    ///
    /// # Arguments
    ///
    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
    /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
    ///
    /// # Returns
    ///
    /// A `PreparedMelt` that can be confirmed or cancelled
    pub async fn prepare_melt_proofs(
        &self,
        quote_id: String,
        proofs: Proofs,
    ) -> Result<PreparedMelt, FfiError> {
        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
            proofs.into_iter().map(|p| p.try_into()).collect();
        let cdk_proofs = cdk_proofs?;

        let prepared = self
            .inner
            .prepare_melt_proofs(&quote_id, cdk_proofs, std::collections::HashMap::new())
            .await?;
        Ok(PreparedMelt::new(Arc::clone(&self.inner), &prepared))
    }

    pub async fn mint_unified(
        &self,
        quote_id: String,
        amount_split_target: SplitTarget,
        spending_conditions: Option<SpendingConditions>,
    ) -> Result<Proofs, FfiError> {
        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;

        let proofs = self
            .inner
            .mint(&quote_id, amount_split_target.into(), conditions)
            .await?;

        Ok(proofs.into_iter().map(|p| p.into()).collect())
    }
    /// Get a melt quote using a unified interface for any payment method
    ///
    /// This method supports bolt11, bolt12, and custom payment methods.
    /// For custom methods, you can pass extra JSON data that will be forwarded
    /// to the payment processor.
    ///
    /// # Arguments
    /// * `method` - Payment method to use (bolt11, bolt12, or custom)
    /// * `request` - Payment request string (invoice, offer, or custom format)
    /// * `options` - Optional melt options (MPP, amountless, etc.)
    /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods)
    pub async fn melt_quote(
        &self,
        method: PaymentMethod,
        request: String,
        options: Option<MeltOptions>,
        extra: Option<String>,
    ) -> Result<MeltQuote, FfiError> {
        let cdk_options = options.map(Into::into);
        let quote = self
            .inner
            .melt_quote::<cdk::nuts::PaymentMethod, _>(method.into(), request, cdk_options, extra)
            .await?;
        Ok(quote.into())
    }
    /// Swap proofs
    pub async fn swap(
        &self,
        amount: Option<Amount>,
        amount_split_target: SplitTarget,
        input_proofs: Proofs,
        spending_conditions: Option<SpendingConditions>,
        include_fees: bool,
    ) -> Result<Option<Proofs>, FfiError> {
        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
            input_proofs.into_iter().map(|p| p.try_into()).collect();
        let cdk_proofs = cdk_proofs?;

        // Convert spending conditions if provided
        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;

        let result = self
            .inner
            .swap(
                amount.map(Into::into),
                amount_split_target.into(),
                cdk_proofs,
                conditions,
                include_fees,
            )
            .await?;

        Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
    }

    /// Get proofs by states
    pub async fn get_proofs_by_states(&self, states: Vec<ProofState>) -> Result<Proofs, FfiError> {
        let mut all_proofs = Vec::new();

        for state in states {
            let proofs = match state {
                ProofState::Unspent => self.inner.get_unspent_proofs().await?,
                ProofState::Pending => self.inner.get_pending_proofs().await?,
                ProofState::Reserved => self.inner.get_reserved_proofs().await?,
                ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?,
                ProofState::Spent => {
                    // CDK doesn't have a method to get spent proofs directly
                    // They are removed from the database when spent
                    continue;
                }
            };

            for proof in proofs {
                all_proofs.push(proof.into());
            }
        }

        Ok(all_proofs)
    }

    /// Check if proofs are spent
    pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<bool>, FfiError> {
        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
            proofs.into_iter().map(|p| p.try_into()).collect();
        let cdk_proofs = cdk_proofs?;

        let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?;
        // Convert ProofState to bool (spent = true, unspent = false)
        let spent_bools = proof_states
            .into_iter()
            .map(|proof_state| {
                matches!(
                    proof_state.state,
                    cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent
                )
            })
            .collect();
        Ok(spent_bools)
    }

    /// List transactions
    pub async fn list_transactions(
        &self,
        direction: Option<TransactionDirection>,
    ) -> Result<Vec<Transaction>, FfiError> {
        let cdk_direction = direction.map(Into::into);
        let transactions = self.inner.list_transactions(cdk_direction).await?;
        Ok(transactions.into_iter().map(Into::into).collect())
    }

    /// Get transaction by ID
    pub async fn get_transaction(
        &self,
        id: TransactionId,
    ) -> Result<Option<Transaction>, FfiError> {
        let cdk_id = id.try_into()?;
        let transaction = self.inner.get_transaction(cdk_id).await?;
        Ok(transaction.map(Into::into))
    }

    /// Get proofs for a transaction by transaction ID
    ///
    /// This retrieves all proofs associated with a transaction by looking up
    /// the transaction's Y values and fetching the corresponding proofs.
    pub async fn get_proofs_for_transaction(
        &self,
        id: TransactionId,
    ) -> Result<Vec<Proof>, FfiError> {
        let cdk_id = id.try_into()?;
        let proofs = self.inner.get_proofs_for_transaction(cdk_id).await?;
        Ok(proofs.into_iter().map(Into::into).collect())
    }

    /// Revert a transaction
    pub async fn revert_transaction(&self, id: TransactionId) -> Result<(), FfiError> {
        let cdk_id = id.try_into()?;
        self.inner.revert_transaction(cdk_id).await?;
        Ok(())
    }

    /// Subscribe to wallet events
    pub async fn subscribe(
        &self,
        params: SubscribeParams,
    ) -> Result<std::sync::Arc<ActiveSubscription>, FfiError> {
        let cdk_params: cdk::nuts::nut17::Params<Arc<String>> = params.clone().into();
        let sub_id = cdk_params.id.to_string();
        let active_sub = self.inner.subscribe(cdk_params).await?;
        Ok(std::sync::Arc::new(ActiveSubscription::new(
            active_sub, sub_id,
        )))
    }

    /// Refresh keysets from the mint
    pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
        let keysets = self.inner.refresh_keysets().await?;
        Ok(keysets.into_iter().map(Into::into).collect())
    }

    /// Get the active keyset for the wallet's unit
    pub async fn get_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
        let keyset = self.inner.get_active_keyset().await?;
        Ok(keyset.into())
    }

    /// Get fees for a specific keyset ID
    pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
        let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
        Ok(self
            .inner
            .get_keyset_fees_and_amounts_by_id(id)
            .await?
            .fee())
    }

    /// Check all pending proofs and return the total amount still pending
    ///
    /// This function checks orphaned pending proofs (not managed by active sagas)
    /// with the mint and marks spent proofs accordingly.
    pub async fn check_all_pending_proofs(&self) -> Result<Amount, FfiError> {
        let amount = self.inner.check_all_pending_proofs().await?;
        Ok(amount.into())
    }

    /// Calculate fee for a given number of proofs with the specified keyset
    pub async fn calculate_fee(
        &self,
        proof_count: u32,
        keyset_id: String,
    ) -> Result<Amount, FfiError> {
        let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
        let fee = self
            .inner
            .get_keyset_count_fee(&id, proof_count as u64)
            .await?;
        Ok(fee.into())
    }

    /// Pay a NUT-18 payment request
    ///
    /// This method prepares and sends a payment for the given payment request.
    /// It will use the Nostr or HTTP transport specified in the request.
    ///
    /// # Arguments
    ///
    /// * `payment_request` - The NUT-18 payment request to pay
    /// * `custom_amount` - Optional amount to pay (required if request has no amount)
    pub async fn pay_request(
        &self,
        payment_request: std::sync::Arc<PaymentRequest>,
        custom_amount: Option<Amount>,
    ) -> Result<(), FfiError> {
        self.inner
            .pay_request(
                payment_request.inner().clone(),
                custom_amount.map(Into::into),
            )
            .await?;
        Ok(())
    }
}

/// BIP353 methods for Wallet
#[cfg(not(target_arch = "wasm32"))]
#[uniffi::export(async_runtime = "tokio")]
impl Wallet {
    /// Get a quote for a BIP353 melt
    ///
    /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
    /// and then creates a melt quote for that offer.
    pub async fn melt_bip353_quote(
        &self,
        bip353_address: String,
        amount_msat: Amount,
    ) -> Result<MeltQuote, FfiError> {
        let cdk_amount: cdk::Amount = amount_msat.into();
        let quote = self
            .inner
            .melt_bip353_quote(&bip353_address, cdk_amount)
            .await?;
        Ok(quote.into())
    }

    /// Get a quote for a Lightning address melt
    ///
    /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
    /// and then creates a melt quote for that invoice.
    pub async fn melt_lightning_address_quote(
        &self,
        lightning_address: String,
        amount_msat: Amount,
    ) -> Result<MeltQuote, FfiError> {
        let cdk_amount: cdk::Amount = amount_msat.into();
        let quote = self
            .inner
            .melt_lightning_address_quote(&lightning_address, cdk_amount)
            .await?;
        Ok(quote.into())
    }

    /// Get a quote for a human-readable address melt
    ///
    /// This method accepts a human-readable address that could be either a BIP353 address
    /// or a Lightning address. It intelligently determines which to try based on mint support:
    ///
    /// 1. If the mint supports Bolt12, it tries BIP353 first
    /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
    /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
    /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
    pub async fn melt_human_readable(
        &self,
        address: String,
        amount_msat: Amount,
    ) -> Result<MeltQuote, FfiError> {
        let cdk_amount: cdk::Amount = amount_msat.into();
        let quote = self
            .inner
            .melt_human_readable_quote(&address, cdk_amount)
            .await?;
        Ok(quote.into())
    }
}

/// Auth methods for Wallet
#[uniffi::export(async_runtime = "tokio")]
impl Wallet {
    /// Set Clear Auth Token (CAT) for authentication
    pub async fn set_cat(&self, cat: String) -> Result<(), FfiError> {
        self.inner.set_cat(cat).await?;
        Ok(())
    }

    /// Set refresh token for authentication
    pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), FfiError> {
        self.inner.set_refresh_token(refresh_token).await?;
        Ok(())
    }

    /// Refresh access token using the stored refresh token
    pub async fn refresh_access_token(&self) -> Result<(), FfiError> {
        self.inner.refresh_access_token().await?;
        Ok(())
    }

    /// Mint blind auth tokens
    pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, FfiError> {
        let proofs = self.inner.mint_blind_auth(amount.into()).await?;
        Ok(proofs.into_iter().map(|p| p.into()).collect())
    }

    /// Get unspent auth proofs
    pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, FfiError> {
        let auth_proofs = self.inner.get_unspent_auth_proofs().await?;
        Ok(auth_proofs.into_iter().map(Into::into).collect())
    }
}

/// Configuration for creating wallets
#[derive(Debug, Clone, uniffi::Record)]
pub struct WalletConfig {
    pub target_proof_count: Option<u32>,
}

/// Generates a new random mnemonic phrase
#[uniffi::export]
pub fn generate_mnemonic() -> Result<String, FfiError> {
    let mnemonic = Mnemonic::generate(12)
        .map_err(|e| FfiError::internal(format!("Failed to generate mnemonic: {}", e)))?;
    Ok(mnemonic.to_string())
}

/// Converts a mnemonic phrase to its entropy bytes
#[uniffi::export]
pub fn mnemonic_to_entropy(mnemonic: String) -> Result<Vec<u8>, FfiError> {
    let m = Mnemonic::parse(&mnemonic)
        .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
    Ok(m.to_entropy())
}
