use brk_types::{Bitcoin, Day1, Dollars, Height, Sats, Version};
use vecdb::{
    AnyVec, CachedBoxedVec, CachedVec, PrintableIndex, ReadOnlyClone, ReadableBoxedVec,
    ReadableVec, TypedVec, VecIndex, short_type_name,
};

use super::DCA_AMOUNT;

/// Small pinned cumulative DCA cache indexed by day.
#[derive(Clone)]
pub(super) struct CachedDcaSats {
    daily: CachedVec<DcaSatsByDay>,
    days: CachedBoxedVec<Height, Day1>,
}

impl CachedDcaSats {
    pub(super) fn new(
        daily_close: ReadableBoxedVec<Day1, Option<Dollars>>,
        days: CachedBoxedVec<Height, Day1>,
    ) -> Self {
        Self {
            daily: CachedVec::wrap(DcaSatsByDay { daily_close }),
            days,
        }
    }

    /// Daily closes can be rewritten without changing their length.
    pub(super) fn invalidate(&self) {
        self.daily.clear();
    }

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

        let last = daily.last().copied().unwrap_or_default();
        for day in &days[from..to] {
            each(daily.get(day.to_usize()).copied().unwrap_or(last))?;
        }
        Ok(())
    }

    fn for_each_value(&self, from: usize, to: usize, mut each: impl FnMut(Sats)) {
        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 CachedDcaSats {
    fn version(&self) -> Version {
        self.days.version() + self.daily.version()
    }

    fn name(&self) -> &str {
        "dca_sats_cumulative"
    }

    fn len(&self) -> usize {
        self.days.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::<Sats>()
    }

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

impl TypedVec for CachedDcaSats {
    type I = Height;
    type T = Sats;
}

impl ReadableVec<Height, Sats> for CachedDcaSats {
    fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec<Sats>) {
        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(Sats)) {
        self.for_each_value(from, to, each);
    }

    fn fold_range_at<B, F: FnMut(B, Sats) -> 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, Sats) -> 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())
    }
}

impl ReadOnlyClone for CachedDcaSats {
    type ReadOnly = Self;

    fn read_only_clone(&self) -> Self {
        self.clone()
    }
}

#[derive(Clone)]
struct DcaSatsByDay {
    daily_close: ReadableBoxedVec<Day1, Option<Dollars>>,
}

impl DcaSatsByDay {
    fn try_for_each_cumulative<E>(
        &self,
        from: usize,
        to: usize,
        mut each: impl FnMut(Sats) -> Result<(), E>,
    ) -> Result<(), E> {
        let to = to.min(self.len());
        if from >= to {
            return Ok(());
        }

        let mut cumulative = Sats::ZERO;
        for (day, price) in self
            .daily_close
            .collect_range_dyn(0, to)
            .into_iter()
            .enumerate()
        {
            cumulative += sats_from_dca(price.unwrap_or_default());
            if day >= from {
                each(cumulative)?;
            }
        }
        Ok(())
    }

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

impl AnyVec for DcaSatsByDay {
    fn version(&self) -> Version {
        self.daily_close.version()
    }

    fn name(&self) -> &str {
        "dca_sats_cumulative"
    }

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

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

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

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

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

impl TypedVec for DcaSatsByDay {
    type I = Day1;
    type T = Sats;
}

impl ReadableVec<Day1, Sats> for DcaSatsByDay {
    fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec<Sats>) {
        buf.reserve(to.min(self.len()).saturating_sub(from));
        self.for_each_cumulative(from, to, |value| buf.push(value));
    }

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

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

    fn try_fold_range_at<B, E, F: FnMut(B, Sats) -> 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_cumulative(from, to, |value| {
            acc = Some(fold(acc.take().unwrap(), value)?);
            Ok(())
        })?;
        Ok(acc.unwrap())
    }
}

fn sats_from_dca(price: Dollars) -> Sats {
    if price == Dollars::ZERO {
        Sats::ZERO
    } else {
        Sats::from(Bitcoin::from(DCA_AMOUNT / price))
    }
}

#[cfg(test)]
mod tests {
    use std::{marker::PhantomData, sync::Arc};

    use parking_lot::RwLock;
    use vecdb::{CachedReadableVec, VecValue, short_type_name};

    use super::*;

    #[derive(Clone)]
    struct MemoryVec<I, T> {
        values: Arc<RwLock<Vec<T>>>,
        index: PhantomData<fn() -> I>,
    }

    impl<I, T> MemoryVec<I, T> {
        fn new(values: impl IntoIterator<Item = T>) -> Self {
            Self {
                values: Arc::new(RwLock::new(values.into_iter().collect())),
                index: PhantomData,
            }
        }

        fn replace(&self, index: usize, value: T) {
            self.values.write()[index] = value;
        }
    }

    impl<I: VecIndex, T: VecValue> AnyVec for MemoryVec<I, T> {
        fn version(&self) -> Version {
            Version::ONE
        }

        fn name(&self) -> &str {
            "memory"
        }

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

        fn index_type_to_string(&self) -> &'static str {
            I::to_string()
        }

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

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

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

    impl<I: VecIndex, T: VecValue> TypedVec for MemoryVec<I, T> {
        type I = I;
        type T = T;
    }

    impl<I: VecIndex, T: VecValue> ReadableVec<I, T> for MemoryVec<I, T> {
        fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec<T>) {
            let values = self.values.read();
            let to = to.min(values.len());
            if from < to {
                buf.extend_from_slice(&values[from..to]);
            }
        }

        fn for_each_range_dyn_at(&self, from: usize, to: usize, each: &mut dyn FnMut(T)) {
            let values = self.values.read();
            let to = to.min(values.len());
            for value in &values[from.min(to)..to] {
                each(value.clone());
            }
        }

        fn fold_range_at<B, F: FnMut(B, T) -> B>(
            &self,
            from: usize,
            to: usize,
            init: B,
            mut fold: F,
        ) -> B {
            let values = self.values.read();
            let to = to.min(values.len());
            values[from.min(to)..to]
                .iter()
                .cloned()
                .fold(init, &mut fold)
        }

        fn try_fold_range_at<B, E, F: FnMut(B, T) -> Result<B, E>>(
            &self,
            from: usize,
            to: usize,
            init: B,
            mut fold: F,
        ) -> Result<B, E> {
            let values = self.values.read();
            let to = to.min(values.len());
            values[from.min(to)..to]
                .iter()
                .cloned()
                .try_fold(init, &mut fold)
        }
    }

    #[test]
    fn daily_cache_is_cumulative_and_refreshes_same_length_rewrites() {
        let prices = MemoryVec::<Day1, Option<Dollars>>::new([
            Some(Dollars::mint(100.0)),
            None,
            Some(Dollars::mint(200.0)),
        ]);
        let days = CachedVec::wrap(MemoryVec::<Height, Day1>::new([
            Day1::from(0),
            Day1::from(1),
            Day1::from(2),
        ]));
        let cached = CachedDcaSats::new(Box::new(prices.clone()), days.cached_boxed_clone());

        let first = sats_from_dca(Dollars::mint(100.0));
        let third = first + sats_from_dca(Dollars::mint(200.0));
        assert_eq!(cached.daily.cached().as_ref(), &[first, first, third]);

        prices.replace(0, Some(Dollars::mint(50.0)));
        assert_eq!(cached.daily.cached().as_ref(), &[first, first, third]);

        cached.invalidate();
        let replaced = sats_from_dca(Dollars::mint(50.0));
        let third = replaced + sats_from_dca(Dollars::mint(200.0));
        assert_eq!(cached.daily.cached().as_ref(), &[replaced, replaced, third]);
    }

    #[test]
    fn height_source_maps_days_and_carries_the_latest_available_total() {
        let prices = MemoryVec::<Day1, Option<Dollars>>::new([
            Some(Dollars::mint(100.0)),
            None,
            Some(Dollars::mint(200.0)),
        ]);
        let days = CachedVec::wrap(MemoryVec::<Height, Day1>::new([
            Day1::from(0),
            Day1::from(0),
            Day1::from(1),
            Day1::from(2),
            Day1::from(3),
        ]));
        let cached = CachedDcaSats::new(Box::new(prices), days.cached_boxed_clone());

        let first = sats_from_dca(Dollars::mint(100.0));
        let third = first + sats_from_dca(Dollars::mint(200.0));
        assert_eq!(cached.collect(), [first, first, first, third, third]);
    }
}
