use std::sync::Arc;

use brk_types::{Cents, Height, StoredU64, Version};
use vecdb::{
    AnyVec, CachedBoxedVec, CachedReadableVec, CachedVec, PrintableIndex, ReadableCloneableVec,
    ReadableVec, TypedVec, short_type_name,
};

use crate::{indexes, internal::LazyPriceWithRatioPerBlock};

use super::lazy_sma::LazySmaVec;

pub(super) struct CachedSmaSource {
    spot_price: CachedBoxedVec<Height, Cents>,
    prefix_sum: CachedVec<PricePrefixSumVec>,
}

impl CachedSmaSource {
    pub(super) fn new(version: Version, spot_price: CachedBoxedVec<Height, Cents>) -> Self {
        let prefix_sum = CachedVec::wrap(PricePrefixSumVec::new(
            "price_sma_prefix_sum",
            version,
            spot_price.clone(),
        ));

        Self {
            spot_price,
            prefix_sum,
        }
    }

    pub(super) fn price(
        &self,
        name: &str,
        version: Version,
        window_starts: &(impl ReadableCloneableVec<Height, Height> + 'static),
        indexes: &indexes::Vecs,
    ) -> LazyPriceWithRatioPerBlock {
        let source = LazySmaVec::new(
            &format!("{name}_cents_source"),
            version,
            window_starts.read_only_boxed_clone(),
            self.prefix_sum.cached_boxed_clone(),
        );

        LazyPriceWithRatioPerBlock::from_uncached_height_source(
            name,
            version,
            source,
            indexes,
            &self.spot_price,
        )
    }
}

#[derive(Clone)]
pub(super) struct PricePrefixSumVec {
    name: Arc<str>,
    version: Version,
    spot_price: CachedBoxedVec<Height, Cents>,
}

impl PricePrefixSumVec {
    pub(super) fn new(
        name: &str,
        version: Version,
        spot_price: CachedBoxedVec<Height, Cents>,
    ) -> Self {
        Self {
            name: Arc::from(name),
            version,
            spot_price,
        }
    }

    fn try_for_each_value<E>(
        &self,
        from: usize,
        to: usize,
        mut each: impl FnMut(StoredU64) -> Result<(), E>,
    ) -> Result<(), E> {
        let prices = self.spot_price.cached();
        let to = to.min(prices.len());
        if from >= to {
            return Ok(());
        }

        let mut sum = 0_u64;
        for (index, price) in prices[..to].iter().copied().enumerate() {
            sum = sum
                .checked_add(price.inner())
                .expect("price SMA prefix sum overflow");
            if index >= from {
                each(StoredU64::from(sum))?;
            }
        }
        Ok(())
    }

    fn for_each_value(&self, from: usize, to: usize, mut each: impl FnMut(StoredU64)) {
        let result = self.try_for_each_value(from, to, |value| {
            each(value);
            Ok::<_, std::convert::Infallible>(())
        });
        match result {
            Ok(()) => {}
            Err(error) => match error {},
        }
    }
}

impl AnyVec for PricePrefixSumVec {
    fn version(&self) -> Version {
        self.version + self.spot_price.version()
    }

    fn name(&self) -> &str {
        &self.name
    }

    fn len(&self) -> usize {
        self.spot_price.len()
    }

    fn index_type_to_string(&self) -> &'static str {
        <Height as PrintableIndex>::to_string()
    }

    fn region_names(&self) -> Vec<String> {
        Vec::new()
    }

    fn value_type_to_size_of(&self) -> usize {
        size_of::<StoredU64>()
    }

    fn value_type_to_string(&self) -> &'static str {
        short_type_name::<StoredU64>()
    }
}

impl TypedVec for PricePrefixSumVec {
    type I = Height;
    type T = StoredU64;
}

impl ReadableVec<Height, StoredU64> for PricePrefixSumVec {
    fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec<StoredU64>) {
        buf.reserve(to.min(self.len()).saturating_sub(from));
        self.for_each_value(from, to, |value| buf.push(value));
    }

    fn for_each_range_dyn_at(&self, from: usize, to: usize, each: &mut dyn FnMut(StoredU64)) {
        self.for_each_value(from, to, each);
    }

    fn fold_range_at<B, F: FnMut(B, StoredU64) -> B>(
        &self,
        from: usize,
        to: usize,
        init: B,
        mut fold: F,
    ) -> B {
        let mut acc = Some(init);
        self.for_each_value(from, to, |value| {
            acc = Some(fold(acc.take().unwrap(), value));
        });
        acc.unwrap()
    }

    fn try_fold_range_at<B, E, F: FnMut(B, StoredU64) -> Result<B, E>>(
        &self,
        from: usize,
        to: usize,
        init: B,
        mut fold: F,
    ) -> Result<B, E> {
        let mut acc = Some(init);
        self.try_for_each_value(from, to, |value| {
            acc = Some(fold(acc.take().unwrap(), value)?);
            Ok(())
        })?;
        Ok(acc.unwrap())
    }
}
