use std::collections::HashMap;

use anyhow::{Context, Result};
use bitcoin_hashes::{hex::ToHex, Hash};
use bitcoincash::consensus::{deserialize, serialize};
use futures_util::StreamExt;

use crate::{
    chaindef::{BlockHash, BlockHeader},
    store::{db_decode, db_encode, DBStore, Row},
};

use super::DBRow;

const HEADERINDEX_CF: &str = "header";

#[derive(serde::Serialize, serde::Deserialize)]
pub struct HeaderRow {
    header: Vec<u8>,
}

impl HeaderRow {
    pub fn new(header: &BlockHeader) -> Self {
        Self {
            header: serialize(&header),
        }
    }

    pub(crate) async fn read_headers_unsorted(store: &DBStore) -> Vec<BlockHeader> {
        store
            .iter(HEADERINDEX_CF, false)
            .await
            .map(|r| Self::from_row(&r))
            .map(|h| h.header())
            .collect()
            .await
    }

    /**
     * Returns list of headers from genesis up until tip provided.
     */
    pub(crate) async fn headers_up_to_tip(
        store: &DBStore,
        tip_hash: &BlockHash,
    ) -> Result<Vec<BlockHeader>> {
        info!("Building header chain");
        let mut headers: HashMap<BlockHash, BlockHeader> = Self::read_headers_unsorted(store)
            .await
            .into_iter()
            .map(|h| (h.block_hash(), h))
            .collect();

        let tip = headers.remove(tip_hash).context(anyhow!(
            "Tip {} not found in header database",
            tip_hash.to_hex()
        ))?;

        let mut header_chain: Vec<BlockHeader> = Vec::with_capacity(headers.len());
        header_chain.push(tip);

        loop {
            let parent_header = header_chain.last().unwrap();
            let prev = &parent_header.prev_blockhash;
            if prev == &BlockHash::all_zeros() {
                // At genesis
                break;
            }

            let prev = headers
                .remove(prev)
                .context(anyhow!("Header {} missing from database", prev.to_hex()))?;
            header_chain.push(prev);
        }
        header_chain.reverse();
        info!("Done building chain");
        Ok(header_chain)
    }

    pub(crate) fn header(&self) -> BlockHeader {
        deserialize(&self.header).expect("failed to decode header")
    }
}

impl DBRow for HeaderRow {
    const CF: &'static str = HEADERINDEX_CF;
    fn to_row(&self) -> Row {
        Row {
            key: db_encode(&self).unwrap().into_boxed_slice(),
            value: vec![].into_boxed_slice(),
        }
    }

    fn from_row(row: &Row) -> HeaderRow {
        db_decode(&row.key).expect("failed to parse HeaderRow")
    }
}
