use bdk::{
    bitcoin::{psbt::PartiallySignedTransaction, secp256k1::PublicKey, Network, Transaction},
    blockchain::EsploraBlockchain,
    database::MemoryDatabase,
    keys::ExtendedKey,
    template::Bip84,
    wallet::AddressInfo,
    KeychainKind, SignOptions, SyncOptions, Wallet,
};

use crate::{bitcoin::keys, utils};

/// A client that uses Esplora to interact with the Bitcoin network.
pub struct BitcoinClient {
    pub(crate) blockchain: bdk::blockchain::EsploraBlockchain,
    name: String,
    pub(crate) wallet: Wallet<MemoryDatabase>,
    pub(crate) public_key: PublicKey,
}

impl BitcoinClient {
    pub(crate) fn sync(&self) -> anyhow::Result<()> {
        tracing::info!("syncing {}'s wallet", self.name);
        self.wallet.sync(&self.blockchain, SyncOptions::default())?;
        Ok(())
    }

    pub(crate) fn broadcast(&self, tx: &Transaction) -> anyhow::Result<()> {
        tracing::info!(
            "broadcasting transaction, output will be at https://mutinynet.com/tx/{}",
            tx.txid()
        );
        let _ = self.blockchain.broadcast(&tx);
        Ok(())
    }

    /// Builds and signs a send transaction to send coins between addresses.
    ///
    /// Does NOT send it, you must call `broadcast` to do that.
    ///
    /// We could split the creation and signing easily if needed.
    pub(crate) fn build_and_sign_send_tx(
        &mut self,
        recipient: AddressInfo,
        amount: u64,
    ) -> Result<bdk::bitcoin::Transaction, anyhow::Error> {
        let mut tx_builder = self.wallet.build_tx();
        tx_builder
            .add_recipient(recipient.script_pubkey(), amount)
            .enable_rbf();
        let (mut psbt, _) = tx_builder.finish()?;
        let tx = self.sign(&mut psbt, true)?.extract_tx();
        Ok(tx)
    }

    pub(crate) fn sign(
        &self,
        psbt: &mut PartiallySignedTransaction,
        finalize: bool,
    ) -> Result<PartiallySignedTransaction, anyhow::Error> {
        tracing::info!("{} signing PSBT", self.name);

        let options = SignOptions {
            try_finalize: finalize,
            ..Default::default()
        };

        let finalized = self.wallet.sign(psbt, options)?;

        // make sure the PSBT is finalized if we asked for it
        if finalize {
            assert!(finalized)
        }
        Ok(psbt.to_owned())
    }

    /// Creates a Bitcoin descriptor wallet with the mnemonic in the given user directory.
    pub(crate) fn create(name: &str, network: Network) -> anyhow::Result<BitcoinClient> {
        let keys_dir = utils::home(name);

        // let mnemonic_path = crate::utils::side_paths(keys_dir).1; // TODO: this tuple stinks
        // let words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file");

        let xkey: ExtendedKey = keys::load_from_file(&keys_dir)?;

        let xprv = xkey
            .into_xprv(Network::Signet)
            .expect("couldn't turn xkey into xprv");

        let secp = bdk::bitcoin::secp256k1::Secp256k1::new();

        let external_descriptor1 = Bip84(xprv.clone(), KeychainKind::External);
        let external_descriptor2 = Bip84(xprv, KeychainKind::External);
        let internal_descriptor = Some(Bip84(xprv, KeychainKind::Internal));

        let wallet = Wallet::new(
            external_descriptor1,
            internal_descriptor,
            network,
            MemoryDatabase::default(),
        )?;

        let blockchain = EsploraBlockchain::new("https://mutinynet.com/api", 20);
        let external_public_key = external_descriptor2.0.private_key.public_key(&secp);

        let esplora = BitcoinClient {
            name: name.to_string(),
            wallet,
            blockchain,
            public_key: external_public_key,
        };

        Ok(esplora)
    }
}
