use std::time::Duration;

use crate::observer::BitcoinConfig;
use crate::try_debug;
use crate::utils::Context;
use bitcoincore_rpc::bitcoin::hashes::Hash;
use bitcoincore_rpc::bitcoin::{self, Amount, BlockHash};
use bitcoincore_rpc::jsonrpc::error::RpcError;
use bitcoincore_rpc_json::GetRawTransactionResultVoutScriptPubKey;
use chainhook_types::bitcoin::{OutPoint, TxIn, TxOut};
use chainhook_types::{
    BitcoinBlockData, BitcoinBlockMetadata, BitcoinNetwork, BitcoinTransactionData,
    BitcoinTransactionMetadata, BlockHeader, BlockIdentifier, TransactionIdentifier,
};
use hiro_system_kit::slog;
use reqwest::Client as HttpClient;
use serde::Deserialize;

#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BitcoinBlockFullBreakdown {
    pub hash: String,
    pub height: usize,
    pub tx: Vec<BitcoinTransactionFullBreakdown>,
    pub time: usize,
    pub nonce: u32,
    pub previousblockhash: Option<String>,
    pub confirmations: i32,
}

impl BitcoinBlockFullBreakdown {
    pub fn get_block_header(&self) -> BlockHeader {
        // Block id
        let hash = format!("0x{}", self.hash);
        let block_identifier = BlockIdentifier {
            index: self.height as u64,
            hash,
        };
        // Parent block id
        let parent_block_hash = match self.previousblockhash {
            Some(ref value) => format!("0x{}", value),
            None => format!("0x{}", BlockHash::all_zeros()),
        };
        let parent_block_identifier = BlockIdentifier {
            index: (self.height - 1) as u64,
            hash: parent_block_hash,
        };
        BlockHeader {
            block_identifier,
            parent_block_identifier,
        }
    }
}

#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BitcoinTransactionFullBreakdown {
    pub txid: String,
    pub vin: Vec<BitcoinTransactionInputFullBreakdown>,
    pub vout: Vec<BitcoinTransactionOutputFullBreakdown>,
}

#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BitcoinTransactionInputFullBreakdown {
    pub sequence: u32,
    /// The raw scriptSig in case of a coinbase tx.
    // #[serde(default, with = "bitcoincore_rpc_json::serde_hex::opt")]
    // pub coinbase: Option<Vec<u8>>,
    /// Not provided for coinbase txs.
    pub txid: Option<String>,
    /// Not provided for coinbase txs.
    pub vout: Option<u32>,
    /// The scriptSig in case of a non-coinbase tx.
    pub script_sig: Option<GetRawTransactionResultVinScriptSig>,
    /// Not provided for coinbase txs.
    pub txinwitness: Option<Vec<String>>,
    pub prevout: Option<BitcoinTransactionInputPrevoutFullBreakdown>,
}

#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetRawTransactionResultVinScriptSig {
    pub hex: String,
}

impl BitcoinTransactionInputFullBreakdown {
    /// Whether this input is from a coinbase tx. If there is not a [BitcoinTransactionInputFullBreakdown::txid] field, the transaction is a coinbase transaction.
    // Note: vout and script_sig fields are also not provided for coinbase transactions.
    pub fn is_coinbase(&self) -> bool {
        self.txid.is_none()
    }
}

#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BitcoinTransactionInputPrevoutFullBreakdown {
    pub height: u64,
    #[serde(with = "bitcoin::amount::serde::as_btc")]
    pub value: Amount,
}

#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BitcoinTransactionOutputFullBreakdown {
    #[serde(with = "bitcoin::amount::serde::as_btc")]
    pub value: Amount,
    pub n: u32,
    pub script_pub_key: GetRawTransactionResultVoutScriptPubKey,
}

#[derive(Deserialize, Serialize)]
pub struct NewBitcoinBlock {
    pub burn_block_hash: String,
    pub burn_block_height: u64,
    pub reward_slot_holders: Vec<String>,
    pub reward_recipients: Vec<RewardParticipant>,
    pub burn_amount: u64,
}

#[allow(dead_code)]
#[derive(Deserialize, Serialize)]
pub struct RewardParticipant {
    recipient: String,
    amt: u64,
}

pub fn build_http_client() -> HttpClient {
    HttpClient::builder()
        .timeout(Duration::from_secs(15))
        .http1_only()
        .no_hickory_dns()
        .connect_timeout(Duration::from_secs(15))
        .tcp_keepalive(Some(Duration::from_secs(15)))
        .no_proxy()
        .danger_accept_invalid_certs(true)
        .build()
        .expect("Unable to build http client")
}

pub async fn download_and_parse_block_with_retry(
    http_client: &HttpClient,
    block_hash: &str,
    bitcoin_config: &BitcoinConfig,
    ctx: &Context,
) -> Result<BitcoinBlockFullBreakdown, String> {
    let mut errors_count = 0;
    let max_retries = 20;
    let block = loop {
        match download_and_parse_block(http_client, block_hash, bitcoin_config, ctx).await {
            Ok(result) => break result,
            Err(e) => {
                errors_count += 1;
                if errors_count > 3 && errors_count < max_retries {
                    ctx.try_log(|logger| {
                        slog::warn!(
                            logger,
                            "unable to fetch and parse block #{block_hash}: will retry in a few seconds (attempt #{errors_count}). Error: {e}",
                        )
                    });
                } else if errors_count == max_retries {
                    return Err(format!("unable to fetch and parse block #{block_hash} after {errors_count} attempts. Error: {e}"));
                }
                std::thread::sleep(std::time::Duration::from_secs(1));
            }
        }
    };
    Ok(block)
}

pub async fn retrieve_block_hash_with_retry(
    http_client: &HttpClient,
    block_height: &u64,
    bitcoin_config: &BitcoinConfig,
    ctx: &Context,
) -> Result<String, String> {
    let mut errors_count = 0;
    let max_retries = 10;
    let block_hash = loop {
        match retrieve_block_hash(http_client, block_height, bitcoin_config, ctx).await {
            Ok(result) => break result,
            Err(e) => {
                errors_count += 1;
                if errors_count > 3 && errors_count < max_retries {
                    ctx.try_log(|logger| {
                        slog::warn!(
                            logger,
                            "unable to retrieve block hash #{block_height}: will retry in a few seconds (attempt #{errors_count}). Error: {e}",
                        )
                    });
                } else if errors_count == max_retries {
                    return Err(format!("unable to retrieve block hash #{block_height} after {errors_count} attempts. Error: {e}"));
                }
                std::thread::sleep(std::time::Duration::from_secs(2));
            }
        }
    };
    Ok(block_hash)
}

pub async fn retrieve_block_hash(
    http_client: &HttpClient,
    block_height: &u64,
    bitcoin_config: &BitcoinConfig,
    _ctx: &Context,
) -> Result<String, String> {
    let body = json!({
        "jsonrpc": "1.0",
        "id": "chainhook-cli",
        "method": "getblockhash",
        "params": [block_height]
    });
    let block_hash = http_client
        .post(&bitcoin_config.rpc_url)
        .basic_auth(&bitcoin_config.username, Some(&bitcoin_config.password))
        .header("Content-Type", "application/json")
        .header("Host", &bitcoin_config.rpc_url[7..])
        .json(&body)
        .send()
        .await
        .map_err(|e| format!("unable to send request ({})", e))?
        .json::<bitcoincore_rpc::jsonrpc::Response>()
        .await
        .map_err(|e| format!("unable to parse response ({})", e))?
        .result::<String>()
        .map_err(|e| format!("unable to parse response ({})", e))?;

    Ok(block_hash)
}

// not used internally by chainhook; exported for ordhook
pub async fn try_download_block_bytes_with_retry(
    http_client: HttpClient,
    block_height: u64,
    bitcoin_config: BitcoinConfig,
    ctx: Context,
) -> Result<Vec<u8>, String> {
    let block_hash =
        retrieve_block_hash_with_retry(&http_client, &block_height, &bitcoin_config, &ctx)
            .await
            .unwrap();

    let mut errors_count = 0;

    let response = loop {
        match download_block(&http_client, &block_hash, &bitcoin_config, &ctx).await {
            Ok(result) => break result,
            Err(_e) => {
                errors_count += 1;
                if errors_count > 1 {
                    ctx.try_log(|logger| {
                        slog::warn!(
                            logger,
                            "unable to fetch block #{block_hash}: will retry in a few seconds (attempt #{errors_count}).",
                        )
                    });
                }
                std::thread::sleep(std::time::Duration::from_millis(1500));
                continue;
            }
        }
    };
    Ok(response)
}

#[derive(Debug, Clone, Deserialize)]
pub struct RpcErrorResponse {
    pub error: RpcError,
}

pub async fn download_block(
    http_client: &HttpClient,
    block_hash: &str,
    bitcoin_config: &BitcoinConfig,
    _ctx: &Context,
) -> Result<Vec<u8>, String> {
    let body = json!({
        "jsonrpc": "1.0",
        "id": "chainhook-cli",
        "method": "getblock",
        "params": [block_hash, 3]
    });
    let res = http_client
        .post(&bitcoin_config.rpc_url)
        .basic_auth(&bitcoin_config.username, Some(&bitcoin_config.password))
        .header("Content-Type", "application/json")
        .header("Host", &bitcoin_config.rpc_url[7..])
        .json(&body)
        .send()
        .await
        .map_err(|e| format!("unable to send request ({})", e))?;

    // Check status code
    if !res.status().is_success() {
        return Err(format!(
            "http request unsuccessful ({:?})",
            res.error_for_status()
        ));
    }

    let rpc_response_bytes = res
        .bytes()
        .await
        .map_err(|e| format!("unable to get bytes ({})", e))?
        .to_vec();

    // Check rpc error presence
    if let Ok(rpc_error) = serde_json::from_slice::<RpcErrorResponse>(&rpc_response_bytes[..]) {
        return Err(format!(
            "rpc request unsuccessful ({})",
            rpc_error.error.message
        ));
    }

    Ok(rpc_response_bytes)
}

pub fn parse_downloaded_block(
    downloaded_block: Vec<u8>,
) -> Result<BitcoinBlockFullBreakdown, String> {
    let block = serde_json::from_slice::<bitcoincore_rpc::jsonrpc::Response>(&downloaded_block[..])
        .map_err(|e| format!("unable to parse jsonrpc payload ({})", e))?
        .result::<BitcoinBlockFullBreakdown>()
        .map_err(|e| format!("unable to parse block ({})", e))?;
    Ok(block)
}

pub async fn download_and_parse_block(
    http_client: &HttpClient,
    block_hash: &str,
    bitcoin_config: &BitcoinConfig,
    _ctx: &Context,
) -> Result<BitcoinBlockFullBreakdown, String> {
    let response = download_block(http_client, block_hash, bitcoin_config, _ctx).await?;
    parse_downloaded_block(response)
}

pub fn standardize_bitcoin_block(
    block: BitcoinBlockFullBreakdown,
    network: &BitcoinNetwork,
    ctx: &Context,
) -> Result<BitcoinBlockData, (String, bool)> {
    let mut transactions = vec![];
    let block_height = block.height as u64;

    try_debug!(ctx, "Standardizing Bitcoin block #{} {}", block.height, block.hash);

    for (tx_index, mut tx) in block.tx.into_iter().enumerate() {
        let txid = tx.txid.to_string();

        let mut inputs = vec![];
        let mut sats_in = 0;
        for (index, input) in tx.vin.drain(..).enumerate() {
            if input.is_coinbase() {
                continue;
            }
            let prevout = input.prevout.as_ref().ok_or((
                format!(
                    "error retrieving prevout for transaction {}, input #{} (block #{})",
                    tx.txid, index, block.height
                ),
                true,
            ))?;

            let txid = input.txid.as_ref().ok_or((
                format!(
                    "error retrieving txid for transaction {}, input #{} (block #{})",
                    tx.txid, index, block.height
                ),
                true,
            ))?;

            let vout = input.vout.ok_or((
                format!(
                    "error retrieving vout for transaction {}, input #{} (block #{})",
                    tx.txid, index, block.height
                ),
                true,
            ))?;

            let script_sig = input.script_sig.ok_or((
                format!(
                    "error retrieving script_sig for transaction {}, input #{} (block #{})",
                    tx.txid, index, block.height
                ),
                true,
            ))?;

            sats_in += prevout.value.to_sat();

            inputs.push(TxIn {
                previous_output: OutPoint {
                    txid: TransactionIdentifier::new(&txid.to_string()),
                    vout,
                    block_height: prevout.height,
                    value: prevout.value.to_sat(),
                },
                script_sig: format!("0x{}", script_sig.hex),
                sequence: input.sequence,
                witness: input
                    .txinwitness
                    .unwrap_or(vec![])
                    .to_vec()
                    .iter()
                    .map(|w| format!("0x{}", w))
                    .collect::<Vec<_>>(),
            });
        }

        let mut outputs = vec![];
        let mut sats_out = 0;
        for output in tx.vout.drain(..) {
            let value = output.value.to_sat();
            sats_out += value;
            outputs.push(TxOut {
                value,
                script_pubkey: format!("0x{}", hex::encode(&output.script_pub_key.hex)),
            });
        }

        let tx = BitcoinTransactionData {
            transaction_identifier: TransactionIdentifier {
                hash: format!("0x{}", txid),
            },
            operations: vec![],
            metadata: BitcoinTransactionMetadata {
                inputs,
                outputs,
                ordinal_operations: vec![],
                brc20_operation: None,
                proof: None,
                fee: sats_in.saturating_sub(sats_out),
                index: tx_index as u32,
            },
        };
        transactions.push(tx);
    }

    Ok(BitcoinBlockData {
        block_identifier: BlockIdentifier {
            hash: format!("0x{}", block.hash),
            index: block_height,
        },
        parent_block_identifier: BlockIdentifier {
            hash: format!(
                "0x{}",
                block
                    .previousblockhash
                    .unwrap_or(BlockHash::all_zeros().to_string())
            ),
            index: match block_height {
                0 => 0,
                _ => block_height - 1,
            },
        },
        timestamp: block.time as u32,
        metadata: BitcoinBlockMetadata {
            network: network.clone(),
        },
        transactions,
    })
}

#[cfg(test)]
pub mod tests;

// Test vectors
// 1) Devnet PoB
// 2022-10-26T03:06:17.376341Z  INFO chainhook_event_observer::indexer: BitcoinBlockData { block_identifier: BlockIdentifier { index: 104, hash: "0x210d0d095a75d88fc059cb97f453eee33b1833153fb1f81b9c3c031c26bb106b" }, parent_block_identifier: BlockIdentifier { index: 103, hash: "0x5d5a4b8113c35f20fb0b69b1fb1ae1b88461ea57e2a2e4c036f97fae70ca1abb" }, timestamp: 1666753576, transactions: [BitcoinTransactionData { transaction_identifier: TransactionIdentifier { hash: "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f" }, operations: [], metadata: BitcoinTransactionMetadata { inputs: [TxIn { previous_output: OutPoint { txid: "0000000000000000000000000000000000000000000000000000000000000000", vout: 4294967295 }, script_sig: "01680101", sequence: 4294967295, witness: [] }], outputs: [TxOut { value: 5000017550, script_pubkey: "76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac" }, TxOut { value: 0, script_pubkey: "6a24aa21a9ed4a190dfdc77e260409c2a693e6d3b8eca43afbc4bffb79ddcdcc9516df804d9b" }], stacks_operations: [] } }, BitcoinTransactionData { transaction_identifier: TransactionIdentifier { hash: "0x59193c24cb2325cd2271b89f790f958dcd4065088680ffbc201a0ebb2f3cbf25" }, operations: [], metadata: BitcoinTransactionMetadata { inputs: [TxIn { previous_output: OutPoint { txid: "9eebe848baaf8dd4810e4e4a91168e2e471c949439faf5d768750ca21d067689", vout: 3 }, script_sig: "483045022100a20f90e9e3c3bb7e558ad4fa65902d8cf6ce4bff1f5af0ac0a323b547385069c022021b9877abbc9d1eef175c7f712ac1b2d8f5ce566be542714effe42711e75b83801210239810ebf35e6f6c26062c99f3e183708d377720617c90a986859ec9c95d00be9", sequence: 4294967293, witness: [] }], outputs: [TxOut { value: 0, script_pubkey: "6a4c5069645b1681995f8e568287e0e4f5cbc1d6727dafb5e3a7822a77c69bd04208265aca9424d0337dac7d9e84371a2c91ece1891d67d3554bd9fdbe60afc6924d4b0773d90000006700010000006600012b" }, TxOut { value: 10000, script_pubkey: "76a914000000000000000000000000000000000000000088ac" }, TxOut { value: 10000, script_pubkey: "76a914000000000000000000000000000000000000000088ac" }, TxOut { value: 4999904850, script_pubkey: "76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac" }], stacks_operations: [PobBlockCommitment(PobBlockCommitmentData { signers: [], stacks_block_hash: "0x5b1681995f8e568287e0e4f5cbc1d6727dafb5e3a7822a77c69bd04208265aca", amount: 10000 })] } }], metadata: BitcoinBlockMetadata }
// 2022-10-26T03:06:21.929157Z  INFO chainhook_event_observer::indexer: BitcoinBlockData { block_identifier: BlockIdentifier { index: 105, hash: "0x0302c4c6063eb7199d3a565351bceeea9df4cb4aa09293194dbab277e46c2979" }, parent_block_identifier: BlockIdentifier { index: 104, hash: "0x210d0d095a75d88fc059cb97f453eee33b1833153fb1f81b9c3c031c26bb106b" }, timestamp: 1666753581, transactions: [BitcoinTransactionData { transaction_identifier: TransactionIdentifier { hash: "0xe7de433aa89c1f946f89133b0463b6cfebb26ad73b0771a79fd66c6acbfe3fb9" }, operations: [], metadata: BitcoinTransactionMetadata { inputs: [TxIn { previous_output: OutPoint { txid: "0000000000000000000000000000000000000000000000000000000000000000", vout: 4294967295 }, script_sig: "01690101", sequence: 4294967295, witness: [] }], outputs: [TxOut { value: 5000017600, script_pubkey: "76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac" }, TxOut { value: 0, script_pubkey: "6a24aa21a9ed98ac3bc4e0c9ed53e3418a3bf3aa511dcd76088cf0e1c4fc71fb9755840d7a08" }], stacks_operations: [] } }, BitcoinTransactionData { transaction_identifier: TransactionIdentifier { hash: "0xe654501805d80d59ef0d95b57ad7a924f3be4a4dc0db5a785dfebe1f70c4e23e" }, operations: [], metadata: BitcoinTransactionMetadata { inputs: [TxIn { previous_output: OutPoint { txid: "59193c24cb2325cd2271b89f790f958dcd4065088680ffbc201a0ebb2f3cbf25", vout: 3 }, script_sig: "483045022100b59d2d07f68ea3a4f27a49979080a07b2432cfad9fc90e1edd0241496f0fd83f02205ac233f4cb68ada487f16339abedb7093948b683ba7d76b3b4058b2c0181a68901210239810ebf35e6f6c26062c99f3e183708d377720617c90a986859ec9c95d00be9", sequence: 4294967293, witness: [] }], outputs: [TxOut { value: 0, script_pubkey: "6a4c5069645b351bb015ef4f7dcdce4c9d95cbf157f85a3714626252cfc9078f3f1591ccdb13c3c7e22b34c4ffc2f6064a41df6fcd7f1b759d4f28b2f7cb6b27f283c868406e0000006800010000006600012c" }, TxOut { value: 10000, script_pubkey: "76a914000000000000000000000000000000000000000088ac" }, TxOut { value: 10000, script_pubkey: "76a914000000000000000000000000000000000000000088ac" }, TxOut { value: 4999867250, script_pubkey: "76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac" }], stacks_operations: [PobBlockCommitment(PobBlockCommitmentData { signers: [], stacks_block_hash: "0x5b351bb015ef4f7dcdce4c9d95cbf157f85a3714626252cfc9078f3f1591ccdb", amount: 10000 })] } }], metadata: BitcoinBlockMetadata }
// 2022-10-26T03:07:53.298531Z  INFO chainhook_event_observer::indexer: BitcoinBlockData { block_identifier: BlockIdentifier { index: 106, hash: "0x52eb2aa15aa99afc4b918a552cef13e8b6eed84b257be097ad954b4f37a7e98d" }, parent_block_identifier: BlockIdentifier { index: 105, hash: "0x0302c4c6063eb7199d3a565351bceeea9df4cb4aa09293194dbab277e46c2979" }, timestamp: 1666753672, transactions: [BitcoinTransactionData { transaction_identifier: TransactionIdentifier { hash: "0xd28d7f5411416f94b95e9f999d5ee8ded5543ba9daae9f612b80f01c5107862d" }, operations: [], metadata: BitcoinTransactionMetadata { inputs: [TxIn { previous_output: OutPoint { txid: "0000000000000000000000000000000000000000000000000000000000000000", vout: 4294967295 }, script_sig: "016a0101", sequence: 4294967295, witness: [] }], outputs: [TxOut { value: 5000017500, script_pubkey: "76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac" }, TxOut { value: 0, script_pubkey: "6a24aa21a9ed71aaf7e5384879a1b112bf623ac8b46dd88b39c3d2c6f8a1d264fc4463e6356a" }], stacks_operations: [] } }, BitcoinTransactionData { transaction_identifier: TransactionIdentifier { hash: "0x72e8e43afc4362cf921ccc57fde3e07b4cb6fac5f306525c86d38234c18e21d1" }, operations: [], metadata: BitcoinTransactionMetadata { inputs: [TxIn { previous_output: OutPoint { txid: "e654501805d80d59ef0d95b57ad7a924f3be4a4dc0db5a785dfebe1f70c4e23e", vout: 3 }, script_sig: "4730440220798bb7d7fb14df35610db2ef04e5d5b6588440b7c429bf650a96f8570904052b02204a817e13e7296a24a8f6cc8737bddb55d1835e513ec2b9dcb03424e4536ae34c01210239810ebf35e6f6c26062c99f3e183708d377720617c90a986859ec9c95d00be9", sequence: 4294967293, witness: [] }], outputs: [TxOut { value: 0, script_pubkey: "6a4c5069645b504d310fc27c86a6b65d0b0e0297db1e185d3432fdab9fa96a1053407ed07b537b8b7d23c6309dfd24340e85b75cff11ad685f8b310c1d2098748a0fffb146ec00000069000100000066000128" }, TxOut { value: 20000, script_pubkey: "76a914000000000000000000000000000000000000000088ac" }, TxOut { value: 4999829750, script_pubkey: "76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac" }], stacks_operations: [PobBlockCommitment(PobBlockCommitmentData { signers: [], stacks_block_hash: "0x5b504d310fc27c86a6b65d0b0e0297db1e185d3432fdab9fa96a1053407ed07b", amount: 20000 })] } }], metadata: BitcoinBlockMetadata }
