mod cache;

use std::collections::hash_map::Entry;

use brk_cohort::ByAddrType;
use brk_error::{Error, Result};
use brk_types::{AddrBytes, AddrHash, OutputType, TxIndex, TypeIndex, Vout};
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use tracing::error;
use vecdb::likely;

use self::cache::AddressCache;
use super::{ProcessedOutput, processed::ProcessedOutputData};
use crate::processor::BlockProcessor;

struct Lookup {
    index: usize,
    output_type: OutputType,
    hash: AddrHash,
    type_index: Option<TypeIndex>,
}

#[derive(Default)]
pub(crate) struct BlockAddresses {
    cache: AddressCache,
    indexes: ByAddrType<FxHashMap<AddrHash, usize>>,
    lookups: Vec<Lookup>,
    unique: Vec<(OutputType, AddrHash)>,
    resolved: Vec<Option<TypeIndex>>,
}

impl BlockAddresses {
    pub(super) fn resolve(
        &mut self,
        processor: &BlockProcessor,
        outputs: &[ProcessedOutput],
    ) -> Result<()> {
        self.clear_block();

        for output in outputs {
            let ProcessedOutputData::Address(addr_hash) = &output.data else {
                continue;
            };

            if let Entry::Vacant(entry) = self
                .indexes
                .get_mut_unwrap(output.output_type)
                .entry(*addr_hash)
            {
                entry.insert(self.unique.len());
                self.unique.push((output.output_type, *addr_hash));
            }
        }

        self.resolved.resize(self.unique.len(), None);

        for (index, &(output_type, hash)) in self.unique.iter().enumerate() {
            if let Some(type_index) = self.cache.get(output_type, hash) {
                self.resolved[index] = Some(type_index);
            } else {
                self.lookups.push(Lookup {
                    index,
                    output_type,
                    hash,
                    type_index: None,
                });
            }
        }

        self.lookups
            .sort_unstable_by_key(|lookup| (lookup.output_type, lookup.hash));

        let stores = &processor.stores.addr_type_to_addr_hash_to_addr_index;
        let lengths = &*processor.lengths;

        self.lookups
            .par_iter_mut()
            .try_for_each(|lookup| -> Result<()> {
                lookup.type_index = stores
                    .get_unwrap(lookup.output_type)
                    .get(&lookup.hash)?
                    .map(|type_index| *type_index)
                    .filter(|type_index| *type_index < lengths.to_type_index(lookup.output_type));
                Ok(())
            })?;

        for lookup in &self.lookups {
            self.resolved[lookup.index] = lookup.type_index;
            if let Some(type_index) = lookup.type_index {
                self.cache
                    .insert(lookup.output_type, lookup.hash, type_index);
            }
        }

        if likely(!processor.check_collisions) {
            return Ok(());
        }

        let mut output_offset = 0;
        for (block_tx_index, tx) in processor.block.txdata.iter().enumerate() {
            let tx_index = processor.lengths.tx_index + TxIndex::from(block_tx_index);
            let next_output_offset = output_offset + tx.output.len();

            for (vout, (txout, output)) in tx
                .output
                .iter()
                .zip(&outputs[output_offset..next_output_offset])
                .enumerate()
            {
                let ProcessedOutputData::Address(addr_hash) = &output.data else {
                    continue;
                };
                let Some(type_index) = self.index(output.output_type, addr_hash) else {
                    continue;
                };
                let addr_bytes =
                    AddrBytes::try_from((&txout.script_pubkey, output.output_type)).unwrap();

                let prev_addrbytes = processor
                    .vecs
                    .addrs
                    .get_bytes_by_type(output.output_type, type_index, &processor.readers.addrbytes)
                    .ok_or(Error::Internal("Missing addrbytes"))?;

                if prev_addrbytes != addr_bytes {
                    error!(
                        height = ?processor.height,
                        vout = ?Vout::from(vout),
                        ?tx_index,
                        addr_type = ?output.output_type,
                        ?prev_addrbytes,
                        ?addr_bytes,
                        ?type_index,
                        "Address hash collision"
                    );
                    return Err(Error::Internal("Address hash collision"));
                }
            }

            output_offset = next_output_offset;
        }

        Ok(())
    }

    pub(super) fn index_mut(
        &mut self,
        addr_type: OutputType,
        addr_hash: &AddrHash,
    ) -> &mut Option<TypeIndex> {
        let index = *self
            .indexes
            .get_mut_unwrap(addr_type)
            .get_mut(addr_hash)
            .unwrap();
        &mut self.resolved[index]
    }

    fn index(&self, addr_type: OutputType, addr_hash: &AddrHash) -> Option<TypeIndex> {
        let index = *self.indexes.get_unwrap(addr_type).get(addr_hash).unwrap();
        self.resolved[index]
    }

    pub(crate) fn clear_cache(&mut self) {
        self.cache.clear();
    }

    fn clear_block(&mut self) {
        self.indexes.values_mut().for_each(FxHashMap::clear);
        self.lookups.clear();
        self.unique.clear();
        self.resolved.clear();
    }
}
