use brk_error::{Error, Result};
use brk_types::{
    Bitcoin, Cents, Date, Day1, Dollars, Height, PartsPerMillionSigned64, Sats, Version,
};
use vecdb::{
    BinaryTransform, CachedBoxedVec, CheckedSub, ReadableCloneableVec, ReadableVec, TypedVec,
    VecIndex,
};

use super::cached_dca_sats::CachedDcaSats;
use super::vecs::{ClassVecs, DcaStack, LumpSumStack, PeriodVecs};
use super::{ByDcaCagr, ByDcaClass, ByDcaPeriod, DCA_AMOUNT, Vecs};
use crate::{
    blocks, indexes,
    internal::{
        CentsUnsignedToDollars, Identity, LazyIndexedVec, LazyPerBlock, LazyPercentPerBlock,
        LazyPreviousDeltaVec, LazySinceDayVec, LazyWindowVec, Price, RatioDiffCents, SatsToBitcoin,
        SatsToCents,
    },
    price,
};

impl Vecs {
    pub(crate) fn forced_import(
        parent_version: Version,
        indexes: &indexes::Vecs,
        blocks: &blocks::Vecs,
        prices: &price::Vecs,
    ) -> Result<Self> {
        let version = parent_version;

        let cached_days = indexes.height.day1_cached_boxed_clone();
        let cached_dca_sats = CachedDcaSats::new(
            prices.split.close.usd.day1.read_only_boxed_clone(),
            cached_days.clone(),
        );
        let sats_cumulative = cached_dca_sats.read_only_boxed_clone();
        let sats_per_day =
            LazyPreviousDeltaVec::new("dca_sats_per_day", version, sats_cumulative.clone());

        let cached_starts = ByDcaPeriod::try_new(|_, days| {
            Ok::<_, Error>(
                blocks
                    .lookback
                    .cached_start_vec(days as usize)
                    .read_only_cached_boxed_clone(),
            )
        })?;
        let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();

        let dca_stack =
            ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
                let metric_name = format!("dca_stack_{name}");
                let source = LazyWindowVec::<Height, Sats, Sats>::new(
                    &format!("{metric_name}_sats_source"),
                    version,
                    sats_cumulative.clone(),
                    window_starts.clone(),
                    true,
                    |current, before, _| current.checked_sub(before).unwrap_or_default(),
                );
                dca_stack_from_source(&metric_name, version, indexes, source, &spot_price)
            })?;

        let first_price_day = Day1::try_from(Date::new(2010, 7, 12)).unwrap();
        let dca_cost_basis = ByDcaPeriod::try_from_period(&dca_stack, |name, days, stack| {
            let metric_name = format!("dca_cost_basis_{name}");
            let source = LazyIndexedVec::new(
                &format!("{metric_name}_cents_source"),
                version,
                stack.sats.height.read_only_boxed_clone(),
                cached_days.clone(),
                move |_, stack_sats, day| {
                    if day <= first_price_day {
                        return Cents::ZERO;
                    }
                    let num_days =
                        (days as usize).min(day.to_usize() + 1 - first_price_day.to_usize());
                    Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats))
                },
            );
            Ok::<_, Error>(Price::from_uncached_height_source(
                &metric_name,
                version,
                source,
                indexes,
            ))
        })?;

        let dca_return =
            ByDcaPeriod::try_from_period(&dca_cost_basis, |name, _days, cost_basis| {
                let metric_name = format!("dca_return_{name}");
                let source = LazyIndexedVec::new(
                    &format!("{metric_name}_ppm_source"),
                    version,
                    cost_basis.cents.height.read_only_boxed_clone(),
                    spot_price.clone(),
                    |_, cost_basis, spot| {
                        RatioDiffCents::<PartsPerMillionSigned64>::apply(spot, cost_basis)
                    },
                );
                Ok::<_, Error>(LazyPercentPerBlock::from_uncached_height_source(
                    &metric_name,
                    version,
                    source,
                    indexes,
                ))
            })?;

        let dca_cagr = ByDcaCagr::try_new(&dca_return, |name, days, source| {
            Ok::<_, Error>(LazyPercentPerBlock::from_lazy_cagr(
                &format!("dca_cagr_{name}"),
                version,
                (days / 365) as u8,
                source,
            ))
        })?;

        let lump_sum_stack =
            ByDcaPeriod::try_from_period(&cached_starts, |name, days, window_starts| {
                lump_sum_stack(
                    &format!("lump_sum_stack_{name}"),
                    days,
                    version,
                    indexes,
                    window_starts,
                    prices,
                )
            })?;

        let lump_sum_return =
            ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
                let metric_name = format!("lump_sum_return_{name}");
                let source = LazyWindowVec::<Height, Cents, PartsPerMillionSigned64>::new(
                    &format!("{metric_name}_ppm_source"),
                    version,
                    prices.spot.cents.height.read_only_boxed_clone(),
                    window_starts.clone(),
                    false,
                    |current, past, _| {
                        RatioDiffCents::<PartsPerMillionSigned64>::apply(current, past)
                    },
                );
                Ok::<_, Error>(LazyPercentPerBlock::from_uncached_height_source(
                    &metric_name,
                    version,
                    source,
                    indexes,
                ))
            })?;

        let class_stack = ByDcaClass::try_new(|name, _year, day| {
            let metric_name = format!("dca_stack_{name}");
            let source = LazySinceDayVec::new(
                &format!("{metric_name}_sats_source"),
                version,
                sats_cumulative.clone(),
                cached_days.clone(),
                day,
                |current, before| current.checked_sub(before).unwrap_or_default(),
            );
            dca_stack_from_source(&metric_name, version, indexes, source, &spot_price)
        })?;

        let class_cost_basis =
            ByDcaClass::try_from_class(&class_stack, |name, _year, from, stack| {
                let metric_name = format!("dca_cost_basis_{name}");
                let source = LazyIndexedVec::new(
                    &format!("{metric_name}_cents_source"),
                    version,
                    stack.sats.height.read_only_boxed_clone(),
                    cached_days.clone(),
                    move |_, stack_sats, day| {
                        if day < from {
                            return Cents::ZERO;
                        }
                        let num_days = day.to_usize() + 1 - from.to_usize();
                        Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats))
                    },
                );
                Ok::<_, Error>(Price::from_uncached_height_source(
                    &metric_name,
                    version,
                    source,
                    indexes,
                ))
            })?;

        let class_return =
            ByDcaClass::try_from_class(&class_cost_basis, |name, _year, _from, cost_basis| {
                let metric_name = format!("dca_return_{name}");
                let source = LazyIndexedVec::new(
                    &format!("{metric_name}_ppm_source"),
                    version,
                    cost_basis.cents.height.read_only_boxed_clone(),
                    spot_price.clone(),
                    |_, cost_basis, spot| {
                        RatioDiffCents::<PartsPerMillionSigned64>::apply(spot, cost_basis)
                    },
                );
                Ok::<_, Error>(LazyPercentPerBlock::from_uncached_height_source(
                    &metric_name,
                    version,
                    source,
                    indexes,
                ))
            })?;

        Ok(Self {
            cached_dca_sats,
            sats_per_day,
            period: PeriodVecs {
                dca_stack,
                dca_cost_basis,
                dca_return,
                dca_cagr,
                lump_sum_stack,
                lump_sum_return,
            },
            class: ClassVecs {
                dca_stack: class_stack,
                dca_cost_basis: class_cost_basis,
                dca_return: class_return,
            },
        })
    }
}

fn dca_stack_from_source<V>(
    name: &str,
    version: Version,
    indexes: &indexes::Vecs,
    source: V,
    spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<DcaStack>
where
    V: TypedVec<I = Height, T = Sats> + ReadableVec<Height, Sats> + Clone + 'static,
{
    let sats = LazyPerBlock::from_uncached_height_source::<Identity<Sats>, _>(
        &format!("{name}_sats"),
        version,
        source,
        indexes,
    );
    let btc = LazyPerBlock::from_lazy::<SatsToBitcoin, Sats>(name, version, &sats);
    let cents_source = LazyIndexedVec::new(
        &format!("{name}_cents_source"),
        version,
        sats.height.read_only_boxed_clone(),
        spot_price.clone(),
        |_, sats, spot| SatsToCents::apply(sats, spot),
    );
    let cents = LazyPerBlock::from_uncached_height_source::<Identity<Cents>, _>(
        &format!("{name}_cents"),
        version,
        cents_source,
        indexes,
    );
    let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
        &format!("{name}_usd"),
        version,
        &cents,
    );
    Ok(DcaStack {
        btc,
        sats,
        usd,
        cents,
    })
}

fn lump_sum_stack(
    name: &str,
    days: u32,
    version: Version,
    indexes: &indexes::Vecs,
    window_starts: &CachedBoxedVec<Height, Height>,
    prices: &price::Vecs,
) -> Result<LumpSumStack> {
    let total_invested = DCA_AMOUNT * days as usize;

    let sats_source = LazyWindowVec::<Height, Cents, Sats>::new(
        &format!("{name}_sats_source"),
        version,
        prices.spot.cents.height.read_only_boxed_clone(),
        window_starts.clone(),
        false,
        move |_, past, _| lump_sum_sats(total_invested, past),
    );
    let sats = LazyPerBlock::from_uncached_height_source::<Identity<Sats>, _>(
        &format!("{name}_sats"),
        version,
        sats_source,
        indexes,
    );
    let btc = LazyPerBlock::from_lazy::<SatsToBitcoin, Sats>(name, version, &sats);

    let cents_source = LazyWindowVec::<Height, Cents, Cents>::new(
        &format!("{name}_cents_source"),
        version,
        prices.spot.cents.height.read_only_boxed_clone(),
        window_starts.clone(),
        false,
        move |current, past, _| SatsToCents::apply(lump_sum_sats(total_invested, past), current),
    );
    let cents = LazyPerBlock::from_uncached_height_source::<Identity<Cents>, _>(
        &format!("{name}_cents"),
        version,
        cents_source,
        indexes,
    );
    let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
        &format!("{name}_usd"),
        version,
        &cents,
    );

    Ok(LumpSumStack {
        btc,
        sats,
        usd,
        cents,
    })
}

fn lump_sum_sats(total_invested: Dollars, past_price: Cents) -> Sats {
    if past_price == Cents::ZERO {
        Sats::ZERO
    } else {
        Sats::from(Bitcoin::from(total_invested / Dollars::from(past_price)))
    }
}
