use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Sats, TxOutIndex};
use rayon::prelude::*;
use tracing::info;
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec};

use super::Vecs;

const SORT_MEMORY_BUDGET: usize = 2 * 1024 * 1024 * 1024;
const BATCH_SIZE: usize = SORT_MEMORY_BUDGET / (size_of::<Entry>() + size_of::<Sats>());

impl Vecs {
    pub(super) fn compute_value(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
        let starting_lengths = indexer.safe_lengths();
        let txout_indexes = &indexer.vecs.inputs.txout_index;
        let dep_version = txout_indexes.version() + indexer.vecs.outputs.value.version();
        self.value.validate_computed_version_or_reset(dep_version)?;

        let target = txout_indexes.len();
        let starting = starting_lengths.txin_index.to_usize();
        let min = self.value.len().min(starting);
        if min >= target {
            return Ok(());
        }

        let value_reader = indexer.vecs.outputs.value.reader();
        let mut entries = Vec::with_capacity((target - min).min(BATCH_SIZE));
        let mut values = Vec::with_capacity((target - min).min(BATCH_SIZE));

        let mut batch_start = min;
        while batch_start < target {
            let batch_end = (batch_start + BATCH_SIZE).min(target);
            let batch_len = batch_end - batch_start;

            entries.clear();
            let mut original_index = 0_usize;
            txout_indexes.for_each_range_at(batch_start, batch_end, |txout_index| {
                entries.push(Entry {
                    original_index,
                    txout_index,
                });
                original_index += 1;
            });

            values.clear();
            values.resize(batch_len, Sats::MAX);
            fill_values(&mut entries, &mut values, |txout_index| {
                value_reader.get(txout_index)
            });

            self.value.truncate_if_needed_at(batch_start)?;
            for value in values.iter().copied() {
                self.value.push(value);
            }

            let _lock = exit.lock();
            self.value.write()?;

            if batch_end < target {
                info!(
                    "Input values: {:.2}%",
                    batch_end as f64 / target as f64 * 100.0
                );
            }
            batch_start = batch_end;
        }

        Ok(())
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Entry {
    original_index: usize,
    txout_index: TxOutIndex,
}

fn fill_values(
    entries: &mut [Entry],
    values: &mut [Sats],
    mut get_value: impl FnMut(TxOutIndex) -> Sats,
) {
    entries.par_sort_unstable_by_key(|entry| entry.txout_index);
    for entry in entries {
        if entry.txout_index.is_coinbase() {
            break;
        }
        values[entry.original_index] = get_value(entry.txout_index);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn values_are_read_in_txout_order_and_scattered_to_input_order() {
        let mut entries = vec![
            Entry {
                original_index: 0,
                txout_index: TxOutIndex::from(8_usize),
            },
            Entry {
                original_index: 1,
                txout_index: TxOutIndex::COINBASE,
            },
            Entry {
                original_index: 2,
                txout_index: TxOutIndex::from(2_usize),
            },
            Entry {
                original_index: 3,
                txout_index: TxOutIndex::from(5_usize),
            },
        ];
        let mut values = vec![Sats::MAX; entries.len()];
        let mut reads = Vec::new();

        fill_values(&mut entries, &mut values, |txout_index| {
            reads.push(txout_index);
            Sats::from(txout_index.to_usize() * 10)
        });

        assert_eq!(
            reads,
            [
                TxOutIndex::from(2_usize),
                TxOutIndex::from(5_usize),
                TxOutIndex::from(8_usize)
            ]
        );
        assert_eq!(
            values,
            [
                Sats::from(80_usize),
                Sats::MAX,
                Sats::from(20_usize),
                Sats::from(50_usize)
            ]
        );
    }
}
