use std::{collections::HashSet, sync::Arc};

use anyhow::*;
use bitcoin_hashes::hex::ToHex;
use bitcoincash::Network;
use futures::stream::StreamExt;
use futures::TryStreamExt;
use serde::Serialize;
use serde_json::Value;
use tokio::join;

use crate::{
    chaindef::{ScriptHash, TokenID, TxIn},
    encode::outpoint_hash,
    errors::rpc_invalid_params,
    indexes::outputtokenindex::OutputTokenIndexRow,
    mempool::MEMPOOL_HEIGHT,
    query::{
        self,
        balance::balance_at_tip,
        queryfilter::QueryFilter,
        status::{
            confirmed_scripthash_token_balance, scripthash_history, scripthash_listunspent,
            unconfirmed_history, unconfirmed_scripthash_token_balance, HistoryItem,
        },
        token::find_token_genesis,
        Query,
    },
    scripthash::addr_to_scripthash,
};

use anyhow::Result;

use crate::rpc::parseutil::{str_from_value, tokenid_from_value};

use super::parseutil::{commitment_from_value, cursor_from_value, hash_from_value};
#[cfg(bch)]
use crate::utilserialize::as_hex;

#[derive(Serialize)]
struct NFTListItem {
    #[cfg(nexa)]
    pub token_id: TokenID,

    #[cfg(bch)]
    #[serde(skip_serializing)]
    #[allow(dead_code)]
    token_id: TokenID,

    #[cfg(bch)]
    #[serde(serialize_with = "as_hex")]
    commitment: Vec<u8>,
}

impl NFTListItem {
    #[cfg(nexa)]
    pub fn new(parent_id: [u8; 32], subgroup: Vec<u8>) -> Self {
        let token_id = TokenID::from_parent_and_subgroup(parent_id, subgroup);
        NFTListItem { token_id }
    }

    #[cfg(bch)]
    pub fn new(parent_id: [u8; 32], commitment: Vec<u8>) -> Self {
        use crate::bitcoin_hashes::Hash;
        NFTListItem {
            token_id: TokenID::from_inner(parent_id),
            commitment,
        }
    }
}

pub struct TokenRPC {
    query: Arc<Query>,
    #[allow(dead_code)] // used by nexa, not bch
    network: Network,
}

impl TokenRPC {
    pub fn new(query: Arc<Query>, network: Network) -> Self {
        Self { query, network }
    }

    pub async fn address_get_balance(&self, params: &[Value]) -> Result<Value> {
        let addr = str_from_value(params.first(), "address")?;
        let scripthash = addr_to_scripthash(&addr)?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        let filter = QueryFilter::from_param_custom_default(
            params.get(3),
            QueryFilter::filter_token_only(),
        )?;

        Ok(self.get_balance(scripthash, filter, token).await?)
    }

    pub async fn address_get_history(&self, params: &[Value]) -> Result<Value> {
        let addr = str_from_value(params.first(), "address")?;
        let scripthash = addr_to_scripthash(&addr)?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        let filter = QueryFilter::from_param_custom_default(
            params.get(3),
            QueryFilter::filter_token_only(),
        )?;

        self.get_scripthash_history(scripthash, token, filter).await
    }

    pub(crate) async fn address_get_mempool(&self, params: &[Value]) -> Result<Value> {
        let addr = str_from_value(params.first(), "address")?;
        let scripthash = addr_to_scripthash(&addr)?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        let filter = QueryFilter::from_param_custom_default(
            params.get(3),
            QueryFilter::filter_token_only(),
        )?;
        self.get_scripthash_mempool(scripthash, token, filter).await
    }

    pub(crate) async fn address_listunspent(&self, params: &[Value]) -> Result<Value> {
        let addr = str_from_value(params.first(), "address")?;
        let scripthash = addr_to_scripthash(&addr)?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        self.listunspent(scripthash, token).await
    }

    pub(crate) async fn scripthash_get_balance(&self, params: &[Value]) -> Result<Value> {
        let scripthash = hash_from_value::<ScriptHash>(params.first())?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        let filter = QueryFilter::from_param_custom_default(
            params.get(3),
            QueryFilter::filter_token_only(),
        )?;
        self.get_balance(scripthash, filter, token).await
    }

    pub(crate) async fn scripthash_get_history(&self, params: &[Value]) -> Result<Value> {
        let scripthash: ScriptHash = hash_from_value(params.first())?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        let filter = QueryFilter::from_param_custom_default(
            params.get(3),
            QueryFilter::filter_token_only(),
        )?;
        self.get_scripthash_history(scripthash, token, filter).await
    }

    pub(crate) async fn scripthash_get_mempool(&self, params: &[Value]) -> Result<Value> {
        let scripthash = hash_from_value::<ScriptHash>(params.first())?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        let filter = QueryFilter::from_param_custom_default(
            params.get(3),
            QueryFilter::filter_token_only(),
        )?;
        self.get_scripthash_mempool(scripthash, token, filter).await
    }

    pub(crate) async fn scripthash_listunspent(&self, params: &[Value]) -> Result<Value> {
        let scripthash = hash_from_value::<ScriptHash>(params.first())?;
        let _cursor = cursor_from_value(params.get(1))?;
        let token = tokenid_from_value(params.get(2))?;
        self.listunspent(scripthash, token).await
    }

    pub(crate) async fn token_transaction_history(&self, params: &[Value]) -> Result<Value> {
        let token_id = match tokenid_from_value(params.first())? {
            Some(t) => t,
            None => return Err(rpc_invalid_params("token_id missing".to_string())),
        };
        let _cursor = cursor_from_value(params.get(1))?;
        let commitment = commitment_from_value(params.get(2))?;

        let confirmed_history =
            query::token::get_token_history(&token_id, &commitment, self.query.confirmed_index());
        let mempool_history = query::token::get_token_history(
            &token_id,
            &commitment,
            self.query.unconfirmed_index().index(),
        );

        let (confirmed_history, mempool_history) = join!(confirmed_history, mempool_history);
        let (confirmed_history, mempool_history) = (confirmed_history?, mempool_history?);

        let as_history_items = mempool_history
            .iter()
            .chain(confirmed_history.iter())
            .map(|h| HistoryItem {
                tx_hash: h.0,
                height: if h.1 == MEMPOOL_HEIGHT { 0 } else { h.1 as i32 },
                fee: None,
            });

        Ok(json!({
            "cursor": None::<String>,
            "history": as_history_items.collect::<Vec<HistoryItem>>()
        }))
    }

    #[cfg(nexa)]
    fn get_token_in_output(out: &crate::nexa::transaction::TxOut) -> Option<TokenID> {
        use crate::nexa::token::parse_token_from_scriptpubkey;
        parse_token_from_scriptpubkey(&out.script_pubkey).map(|(token_id, _)| token_id)
    }

    pub(crate) async fn genesis_info(&self, params: &[Value]) -> Result<Value> {
        use crate::indexes::DBRow;

        let token_id = match tokenid_from_value(params.first())? {
            Some(t) => t,
            None => return Err(rpc_invalid_params("token_id missing".to_string())),
        };
        let (tx, height) = {
            #[cfg(nexa)]
            {
                find_token_genesis(&token_id, self.query.confirmed_index(), self.query.tx()).await
            }
            #[cfg(bch)]
            {
                find_token_genesis(
                    &token_id,
                    self.query.confirmed_index(),
                    self.query.unconfirmed_index().index(),
                    self.query.tx(),
                )
                .await
            }
        }?;

        let fetch_token_if_any = |i: TxIn| async move {
            let prefix =
                OutputTokenIndexRow::filter_by_outpointhash(&outpoint_hash(&i.previous_output));
            let (task, stream) = self
                .query
                .confirmed_index()
                .scan(OutputTokenIndexRow::CF, prefix, None)
                .await;

            let token = stream
                .map(|r| OutputTokenIndexRow::from_row(&r))
                .map(|r| r.into_token_id())
                .next()
                .await;

            task.await??;
            Ok(token)
        };

        let tokens_in: Vec<Option<TokenID>> = futures::stream::iter(tx.clone().input.into_iter())
            .then(|i| async move { fetch_token_if_any(i).await })
            .try_collect()
            .await?;

        let tokens_in: HashSet<TokenID> = tokens_in.into_iter().flatten().collect();

        if tokens_in.contains(&token_id) {
            bail!(
                "Expected {} to be genesis tx for {}, but it also had token as input.",
                tx.txid(),
                token_id,
            );
        }

        let mut tx_details = json!({
            "height": height,
        });

        #[cfg(bch)]
        {
            use crate::bch::bcmr::get_token_bcmr;
            use crate::bch::crc20::get_token_crc20;
            use futures_util::join;
            let d = tx_details.as_object_mut().unwrap();
            d.insert("tx_hash".to_string(), json!(tx.txid().to_hex()));
            d.insert("token_id".to_string(), json!(token_id));

            let txid = tx.txid();
            let bcmr = get_token_bcmr(
                &txid,
                self.query.confirmed_index(),
                self.query.unconfirmed_index().index(),
                self.query.tx(),
            );
            let crc20 = get_token_crc20(&tx, self.query.confirmed_index(), self.query.tx());
            let (bcmr, crc20) = join!(bcmr, crc20);

            d.insert("bcmr".to_string(), json!(bcmr?));
            d.insert("crc20".to_string(), json!(crc20?));
        }
        #[cfg(nexa)]
        {
            use crate::nexa::token::parse_token_description;
            // Nexa supports token metadata as a OP_RETURN output.
            // There must be exactly one OP_RETURN output and exactly one new token.

            let tokens_out: HashSet<TokenID> = tx
                .output
                .iter()
                .filter_map(Self::get_token_in_output)
                .collect();

            let genesis_outputs = tokens_out.difference(&tokens_in);
            let (op_return, token) = if genesis_outputs.count() == 1 {
                // Ours is the only token genesis output of the transaction.
                // If there is a OP_RETURN with token description, it belongs to this token.
                let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return());
                let token = op_return
                    .map(|o| parse_token_description(&o.script_pubkey).unwrap_or_default());
                (op_return.map(|o| o.script_pubkey.as_bytes()), token)
            } else {
                (None, None)
            };

            let d = tx_details.as_object_mut().unwrap();
            d.insert("txid".to_string(), json!(tx.txid().to_hex()));
            d.insert("txidem".to_string(), json!(tx.txidem().to_hex()));
            d.insert(
                "group".to_string(),
                json!(token_id.as_cashaddr(self.network)),
            );
            d.insert("token_id_hex".to_string(), json!(token_id));
            d.insert(
                "ticker".to_string(),
                json!(token.as_ref().map(|t| t.ticker.clone())),
            );
            d.insert(
                "name".to_string(),
                json!(token.as_ref().map(|t| t.name.clone())),
            );
            d.insert(
                "document_hash".to_string(),
                json!(token.as_ref().map(|t| t.document_hash.map(|d| d.to_hex()))),
            );
            d.insert(
                "document_url".to_string(),
                json!(token.as_ref().map(|t| t.document_url.clone())),
            );
            d.insert(
                "decimal_places".to_string(),
                json!(token.as_ref().map(|t| t.decimal_places)),
            );
            d.insert(
                "op_return".to_string(),
                json!(op_return.map(|o| o.to_hex())),
            );
            d.insert(
                "op_return_id".to_string(),
                json!(token.map(|t| t.op_return_id)),
            );
        }

        Ok(tx_details)
    }

    async fn get_balance(
        &self,
        scripthash: ScriptHash,
        filter: QueryFilter,
        token: Option<TokenID>,
    ) -> Result<Value> {
        if !filter.has_height_filter() {
            // we can use the faster utxo index
            let (_cbch, confirmed, _ubch, unconfirmed) = balance_at_tip(
                self.query.confirmed_index(),
                self.query.unconfirmed_index().index(),
                &scripthash,
                &filter,
                &token,
            )
            .await?;

            return Ok(json!({
                "confirmed": confirmed,
                "unconfirmed": unconfirmed,
                "cursor": None::<String>,
            }));
        }

        let (confirmed, outputs) = confirmed_scripthash_token_balance(
            self.query.confirmed_index(),
            scripthash,
            token.clone(),
        )
        .await?;
        let unconfirmed = unconfirmed_scripthash_token_balance(
            self.query.unconfirmed_index().index(),
            self.query.confirmed_index(),
            outputs,
            scripthash,
            token,
        )
        .await?;
        Ok(json!({
            "confirmed": confirmed,
            "unconfirmed": unconfirmed,
            "cursor": None::<String>,
        }))
    }

    async fn get_scripthash_history(
        &self,
        scripthash: ScriptHash,
        token: Option<TokenID>,
        filter: QueryFilter,
    ) -> Result<Value> {
        let history = scripthash_history(
            self.query.confirmed_index(),
            self.query.unconfirmed_index(),
            scripthash,
            filter,
            token,
        )
        .await?;

        Ok(json!({
            "transactions": history,
            "cursor": None::<String>,
        }))
    }

    async fn get_scripthash_mempool(
        &self,
        scripthash: ScriptHash,
        token: Option<TokenID>,
        filter: QueryFilter,
    ) -> Result<Value> {
        let mut history =
            unconfirmed_history(self.query.unconfirmed_index(), scripthash, filter, token).await?;
        filter.filter_limit_offset(&mut history);
        Ok(json!({
            "transactions": history,
            "cursor": None::<String>,
        }))
    }

    async fn listunspent(&self, scripthash: ScriptHash, token: Option<TokenID>) -> Result<Value> {
        let unspent = scripthash_listunspent(
            self.query.confirmed_index(),
            self.query.unconfirmed_index().index(),
            scripthash,
            QueryFilter::filter_token_only(),
            token,
        )
        .await?;

        // Custom serialization to add group to entry
        #[cfg(nexa)]
        let unspent = {
            unspent
                .into_iter()
                .map(|u| {
                    json!({
                        "tx_hash": u.txid,
                        "tx_pos": u.vout,
                        "outpoint_hash": u.outpoint_hash,
                        "height": if u.height == MEMPOOL_HEIGHT { 0 } else { u.height },
                        "value": u.value,
                        "group": u.token_id.as_ref().map(|t| t.as_cashaddr(self.network)),
                        "token_id_hex": u.token_id.map(|t| t.to_hex()),
                        "token_amount": u.token_amount,

                    })
                })
                .collect::<Vec<Value>>()
        };

        Ok(json!({
            "cursor": None::<String>,
            "unspent": unspent,
        }))
    }

    pub(crate) async fn list_nft(&self, params: &[Value]) -> Result<Value> {
        let token_id = match tokenid_from_value(params.first())? {
            Some(t) => t,
            None => return Err(rpc_invalid_params("token_id missing".to_string())),
        };
        let nfts = query::token::list_nft(&token_id, self.query.confirmed_index())
            .await?
            .into_iter()
            .chain(query::token::list_nft(&token_id, self.query.unconfirmed_index().index()).await?)
            .map(|(token_id, subgroup)| NFTListItem::new(token_id, subgroup));

        #[cfg(nexa)]
        let nfts = {
            // custom serialization to add `group` field
            nfts.into_iter()
                .map(|nft| {
                    let hex = nft.token_id.to_hex();
                    let cashaddr = nft.token_id.as_cashaddr(self.network);
                    json!({
                        "group": cashaddr,
                        "token_id_hex": hex
                    })
                })
                .collect::<Vec<Value>>()
        };
        #[cfg(bch)]
        let nfts = { nfts.collect::<Vec<NFTListItem>>() };

        Ok(json!({
            "cursor": None::<String>,
            "nft": nfts,
        }))
    }
}
