use std::sync::Arc;

use crate::chaindef::OutPointHash;
use crate::chaindef::TokenID;
use crate::chaindef::Transaction;
use crate::indexes::heightindex::HeightIndexRow;
use crate::indexes::inputindex::InputIndexRow;
use crate::indexes::outputindex::OutputIndexRow;
use crate::indexes::outputtokenindex::OutputTokenIndexRow;
use crate::indexes::DBRow;
use crate::mempool::MEMPOOL_HEIGHT;
use crate::query::tx::TxQuery;
use crate::store::{DBStore, Row};
use anyhow::*;
use bitcoin_hashes::Hash;
use bitcoincash::hash_types::Txid;
use futures_util::StreamExt;

pub(crate) async fn get_row<T: DBRow>(store: &DBStore, key: Vec<u8>) -> Option<T> {
    let (key, value) = store.get(T::CF, key).await;
    let row = Row {
        key: key.into_boxed_slice(),
        value: value?.into_boxed_slice(),
    };
    Some(T::from_row(&row))
}

/**
 * Fetche a single row found by key prefix.
 */
pub(crate) async fn get_row_by_key_prefix<T: DBRow>(
    store: &DBStore,
    key: Vec<u8>,
) -> Result<Option<T>> {
    let (query, mut iter) = store.scan(T::CF, key, None).await;
    let res = iter.next().await.map(|r| T::from_row(&r));
    query.await??;
    Ok(res)
}

/**
 * Find transaction height given a txid
 */
pub(crate) async fn height_by_txid(store: &DBStore, txid: &Txid) -> Option<u32> {
    let key = txid.to_vec();
    let (_key, value) = store.get(HeightIndexRow::CF, key).await;
    let height = HeightIndexRow::height_from_dbvalue(&value?);
    if height == MEMPOOL_HEIGHT {
        None
    } else {
        Some(height)
    }
}

/**
 * Get a single output defined by its outpointhash HASH(txid, n)
 */
pub async fn get_utxo(store: &DBStore, outpointhash: &OutPointHash) -> Option<OutputIndexRow> {
    let key = outpointhash.to_vec();
    let (key, value) = store.get(OutputIndexRow::CF, key).await;

    Some(OutputIndexRow::from_row(&Row {
        key: key.into_boxed_slice(),
        value: value?.into_boxed_slice(),
    }))
}

pub(crate) async fn multi_get_utxo(
    store: &DBStore,
    outpointhash: &[OutPointHash],
) -> Vec<Option<OutputIndexRow>> {
    let keys: Vec<Vec<u8>> = outpointhash.iter().map(|o| o.to_vec()).collect();

    let keys_clone = keys.clone();
    let values = store.multi_get(OutputIndexRow::CF, keys).await;

    keys_clone
        .into_iter()
        .zip(values)
        .map(|(key, value)| {
            Some(OutputIndexRow::from_row(&Row {
                key: key.into_boxed_slice(),
                value: value?.into_boxed_slice(),
            }))
        })
        .collect()
}

pub(crate) async fn token_from_outpoint(
    store: &DBStore,
    outpointhash: &OutPointHash,
) -> Result<Option<(TokenID, i64, Option<Vec<u8>>)>> {
    let key = OutputTokenIndexRow::filter_by_outpointhash(outpointhash);
    Ok(
        match get_row_by_key_prefix::<OutputTokenIndexRow>(store, key).await? {
            Some(token_row) => {
                let amount = token_row.token_amount();
                #[cfg(nexa)]
                {
                    // In nexa, subgroup is part of the token ID, so we always return None
                    // to void copy.
                    Some((token_row.into_token_id(), amount, None))
                }
                #[cfg(bch)]
                {
                    let (token_id, commitment) = token_row.into_token_id_and_commitment();
                    Some((
                        token_id,
                        amount,
                        if commitment.is_empty() {
                            None
                        } else {
                            Some(commitment)
                        },
                    ))
                }
            }
            None => None,
        },
    )
}

pub(crate) async fn get_tx_spending_prevout(
    store: &DBStore,
    txquery: &TxQuery,
    outpoint: &OutPointHash,
) -> Result<
    Option<(
        Transaction,
        u32, /* input index */
        u32, /* confirmation height */
    )>,
> {
    let spender = tx_spending_outpoint(store, outpoint).await;
    if spender.is_none() {
        return Ok(None);
    };
    let spender = spender.unwrap();
    let txid = spender.txid();
    let height = height_by_txid(store, &txid).await;
    let tx = txquery.get(&txid, None, height, false).await?;
    Ok(Some((tx, spender.index(), height.unwrap_or_default())))
}

#[inline]
pub(crate) async fn outpoint_is_spent(store: &Arc<DBStore>, outpoint: OutPointHash) -> bool {
    store
        .exists_32bit_key(InputIndexRow::CF, outpoint.into_inner())
        .await
}

#[inline]
pub(crate) async fn outpoints_are_spent(
    store: &Arc<DBStore>,
    outpoints: &[OutPointHash],
) -> Vec<bool> {
    store
        .exists_multi_32bit_keys(
            InputIndexRow::CF,
            outpoints.iter().map(|o| o.into_inner()).collect(),
        )
        .await
}

pub(crate) async fn tx_spending_outpoint(
    store: &DBStore,
    outpoint: &OutPointHash,
) -> Option<InputIndexRow> {
    let key = InputIndexRow::filter_by_outpointhash(outpoint);
    let (key, value) = store.get(InputIndexRow::CF, key).await;
    Some(InputIndexRow::from_row(&Row {
        key: key.into_boxed_slice(),
        value: value?.into_boxed_slice(),
    }))
}

pub async fn get_tx_funding_prevout(
    store: &DBStore,
    txquery: &TxQuery,
    outpoint: &OutPointHash,
) -> Result<
    Option<(
        Transaction,
        u32, /* input index */
        u32, /* confirmation height */
    )>,
> {
    let spender = tx_funding_outpoint(store, outpoint).await;
    if spender.is_none() {
        return Ok(None);
    };
    let spender = spender.unwrap();
    let txid = spender.txid();
    let height = height_by_txid(store, &txid).await;
    let tx = txquery.get(&txid, None, height, false).await?;
    Ok(Some((tx, spender.index(), height.unwrap_or_default())))
}

/**
 * Locate transaction that funded given output (created this utxo)
 */
pub async fn tx_funding_outpoint(
    store: &DBStore,
    outpoint: &OutPointHash,
) -> Option<OutputIndexRow> {
    let key = OutputIndexRow::filter_by_outpointhash(outpoint);
    let (key, value) = store.get(OutputIndexRow::CF, key).await;
    Some(OutputIndexRow::from_row(&Row {
        key: key.into_boxed_slice(),
        value: value?.into_boxed_slice(),
    }))
}
