"""Weighted revenue-speed forecast and versioned parameter calculation."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from decimal import Decimal

from revenue_forecast import (
    RevenueForecast,
    RevenueObservation,
    RevenueTrendWindow,
)


@dataclass(frozen=True)
class RevenueSpeedSample:
    data_date: date
    report_time: datetime
    parameter_version: str
    cumulative_revenue: Decimal
    final_revenue: Decimal
    latest_interval_revenue: Decimal
    previous_interval_revenue: Decimal
    latest_weight: Decimal
    weighted_speed: Decimal
    remaining_revenue: Decimal
    remaining_multiplier: Decimal


@dataclass(frozen=True)
class RevenueSpeedParameter:
    parameter_version: str
    time_slot: time
    training_start: date
    training_end: date
    sample_count: int
    latest_weight: Decimal
    multiplier_p10: Decimal
    multiplier_p50: Decimal
    multiplier_p90: Decimal
    multiplier_mad: Decimal


class SpeedForecastUnavailable(RuntimeError):
    """The current snapshot cannot produce a weighted-speed forecast."""


def decimal_quantile(values: list[Decimal], percentile: Decimal) -> Decimal:
    if not values:
        raise ValueError("values must not be empty")
    if percentile < 0 or percentile > 1:
        raise ValueError("percentile must be between 0 and 1")
    ordered = sorted(values)
    if len(ordered) == 1:
        return ordered[0]
    position = Decimal(len(ordered) - 1) * percentile
    lower_index = int(position)
    upper_index = min(lower_index + 1, len(ordered) - 1)
    fraction = position - Decimal(lower_index)
    return ordered[lower_index] + (
        ordered[upper_index] - ordered[lower_index]
    ) * fraction


def build_speed_samples(
    *,
    data_date: date,
    intervals: list[tuple[datetime, Decimal]],
    parameter_version: str,
    latest_weight: Decimal,
    start_time: time,
    stop_time: time,
) -> list[RevenueSpeedSample]:
    if latest_weight < 0 or latest_weight > 1:
        raise ValueError("latest_weight must be between 0 and 1")
    if len(intervals) != 96:
        return []
    intervals = sorted(intervals, key=lambda item: item[0])
    if any(
        current[0] - previous[0] != timedelta(minutes=15)
        for previous, current in zip(intervals, intervals[1:])
    ):
        return []
    if any(window_start.date() != data_date for window_start, _ in intervals):
        return []
    final_revenue = sum((value for _, value in intervals), Decimal("0"))
    cumulative = Decimal("0")
    samples: list[RevenueSpeedSample] = []
    for index, (window_start, revenue) in enumerate(intervals):
        cumulative += revenue
        report_time = window_start + timedelta(minutes=15)
        if index == 0 or not start_time <= report_time.time() <= stop_time:
            continue
        previous_revenue = intervals[index - 1][1]
        speed = (
            latest_weight * revenue
            + (Decimal("1") - latest_weight) * previous_revenue
        )
        remaining = final_revenue - cumulative
        if speed <= 0 or remaining < 0:
            continue
        samples.append(
            RevenueSpeedSample(
                data_date=data_date,
                report_time=report_time,
                parameter_version=parameter_version,
                cumulative_revenue=cumulative,
                final_revenue=final_revenue,
                latest_interval_revenue=revenue,
                previous_interval_revenue=previous_revenue,
                latest_weight=latest_weight,
                weighted_speed=speed,
                remaining_revenue=remaining,
                remaining_multiplier=remaining / speed,
            )
        )
    return samples


def aggregate_speed_parameters(
    samples: list[RevenueSpeedSample],
) -> list[RevenueSpeedParameter]:
    by_time: dict[time, list[RevenueSpeedSample]] = {}
    for sample in samples:
        by_time.setdefault(sample.report_time.time(), []).append(sample)

    parameters: list[RevenueSpeedParameter] = []
    for time_slot, items in sorted(by_time.items()):
        versions = {item.parameter_version for item in items}
        weights = {item.latest_weight for item in items}
        if len(versions) != 1 or len(weights) != 1:
            raise ValueError("Speed samples must use one version and one weight")
        values = [item.remaining_multiplier for item in items]
        median = decimal_quantile(values, Decimal("0.5"))
        mad = decimal_quantile(
            [abs(value - median) for value in values], Decimal("0.5")
        )
        parameters.append(
            RevenueSpeedParameter(
                parameter_version=items[0].parameter_version,
                time_slot=time_slot,
                training_start=min(item.data_date for item in items),
                training_end=max(item.data_date for item in items),
                sample_count=len(items),
                latest_weight=items[0].latest_weight,
                multiplier_p10=decimal_quantile(values, Decimal("0.1")),
                multiplier_p50=median,
                multiplier_p90=decimal_quantile(values, Decimal("0.9")),
                multiplier_mad=mad,
            )
        )
    return parameters


def calculate_speed_forecast(
    current: RevenueObservation,
    trend_windows: list[RevenueTrendWindow],
    parameter: RevenueSpeedParameter | None,
    *,
    forecast_version: str,
    parameter_version: str,
    target_cost_ratio: Decimal,
    minimum_samples: int,
    cost_reserve_date: date | None = None,
    channel_costs: tuple[tuple[str, Decimal], ...] = (),
    non_miniapp_reserved_cost: Decimal = Decimal("0"),
) -> RevenueForecast:
    if current.today_revenue < 0:
        raise ValueError("today_revenue must not be negative")
    if target_cost_ratio <= 0:
        raise ValueError("target_cost_ratio must be positive")
    if non_miniapp_reserved_cost < 0:
        raise ValueError("non_miniapp_reserved_cost must not be negative")
    unavailable_reason = None
    if parameter is None:
        unavailable_reason = "NO_PARAMETER"
    elif parameter.parameter_version != parameter_version:
        unavailable_reason = "PARAMETER_VERSION_MISMATCH"
    elif parameter.time_slot != current.report_time.time():
        unavailable_reason = "PARAMETER_TIME_MISMATCH"
    elif parameter.training_end >= current.report_time.date():
        unavailable_reason = "PARAMETER_USES_CURRENT_OR_FUTURE_DATA"
    elif parameter.sample_count < minimum_samples:
        unavailable_reason = "INSUFFICIENT_PARAMETER_SAMPLES"
    elif not (
        Decimal("0")
        <= parameter.multiplier_p10
        <= parameter.multiplier_p50
        <= parameter.multiplier_p90
    ):
        unavailable_reason = "INVALID_PARAMETER_QUANTILES"
    elif len(trend_windows) < 2:
        unavailable_reason = "INSUFFICIENT_SPEED_WINDOWS"
    if unavailable_reason is not None:
        raise SpeedForecastUnavailable(unavailable_reason)

    assert parameter is not None
    previous_window, latest_window = trend_windows[-2:]
    if latest_window.report_time - previous_window.report_time != timedelta(
        minutes=15
    ):
        raise SpeedForecastUnavailable("NON_CONTIGUOUS_SPEED_WINDOWS")
    speed = (
        parameter.latest_weight * latest_window.today_revenue
        + (Decimal("1") - parameter.latest_weight)
        * previous_window.today_revenue
    )
    if speed <= 0:
        raise SpeedForecastUnavailable("NON_POSITIVE_SPEED")

    lower = current.today_revenue + speed * parameter.multiplier_p10
    forecast = current.today_revenue + speed * parameter.multiplier_p50
    upper = current.today_revenue + speed * parameter.multiplier_p90
    target_daily_cost = forecast / target_cost_ratio
    miniapp_target = max(
        target_daily_cost - non_miniapp_reserved_cost,
        Decimal("0"),
    )
    return RevenueForecast(
        report_time=current.report_time,
        forecast_version=forecast_version,
        parameter_version=parameter.parameter_version,
        current_cumulative_revenue=current.today_revenue,
        speed_latest_weight=parameter.latest_weight,
        latest_interval_revenue=latest_window.today_revenue,
        previous_interval_revenue=previous_window.today_revenue,
        weighted_speed=speed,
        remaining_multiplier_p10=parameter.multiplier_p10,
        remaining_multiplier_p50=parameter.multiplier_p50,
        remaining_multiplier_p90=parameter.multiplier_p90,
        parameter_sample_count=parameter.sample_count,
        forecast_revenue=forecast,
        forecast_lower=lower,
        forecast_upper=upper,
        target_cost_ratio=target_cost_ratio,
        target_daily_cost=target_daily_cost,
        cost_reserve_date=cost_reserve_date,
        channel_costs=channel_costs,
        non_miniapp_reserved_cost=non_miniapp_reserved_cost,
        miniapp_target_daily_cost=miniapp_target,
        status="WEIGHTED_SPEED_P50",
    )
