from __future__ import annotations

from datetime import datetime, timezone
from typing import cast

import numpy as np
import polars as pl

from uforia.analytics import build_vol_index
from uforia.storage import DatasetName, DatasetStore, ReadMode


def validate_btc_vvix(
    store: DatasetStore,
    start: datetime,
    end: datetime,
) -> dict[str, str | float | int | None]:
    return validate_vol_suite(store, start, end, underlying="BTC")


def validate_vol_suite(
    store: DatasetStore,
    start: datetime,
    end: datetime,
    *,
    underlying: str,
) -> dict[str, str | float | int | None]:
    dvol = build_vol_index(store, start, end, source="dvol", underlying=underlying)
    bvix = build_vol_index(store, start, end, source="bvix", underlying=underlying)
    surface = store.read(
        DatasetName.SURFACE_FEATURES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    vvix = store.read(
        DatasetName.VVIX_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    razor = store.read(
        DatasetName.RAZOR_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    ingestion_runs = store.read(
        DatasetName.INGESTION_RUNS,
        start=start,
        end=end,
        filters={"underlying": underlying},
        read_mode=ReadMode.ALL_RUNS,
    )

    joined = dvol.join(
        bvix.select(["ts", pl.col("value").alias("bvix_value")]),
        on="ts",
        how="inner",
    )
    diff_mean = None
    correlation = None
    paired = joined.drop_nulls(["value", "bvix_value"])
    if not paired.is_empty():
        diff_mean = float(cast(float, (paired["value"] - paired["bvix_value"]).mean()))
        if paired.height > 2:
            correlation = float(
                np.corrcoef(paired["value"].to_numpy(), paired["bvix_value"].to_numpy())[0, 1]
            )

    archiver_runs = ingestion_runs.filter(
        pl.col("dataset") == DatasetName.OPTION_CHAIN_SNAPSHOT.value
    )
    latest_run_lag_hours = None
    if not archiver_runs.is_empty():
        latest_run = cast(datetime, archiver_runs.sort("logical_ts").tail(1).item(0, "logical_ts"))
        latest_run_lag_hours = float(
            (datetime.now(tz=timezone.utc) - latest_run).total_seconds() / 3600.0
        )

    return {
        "underlying": underlying,
        "dvol_rows": dvol.height,
        "bvix_rows": bvix.height,
        "surface_rows": surface.height,
        "vvix_rows": vvix.height,
        "razor_rows": razor.height,
        "mean_dvol_minus_bvix": diff_mean,
        "dvol_bvix_correlation": correlation,
        "surface_fit_mean_quality": float(cast(float, surface["fit_quality"].mean()))
        if not surface.is_empty()
        else None,
        "vvix_missing_q": int(vvix.filter(pl.col("qvvix_30d").is_null()).height)
        if not vvix.is_empty()
        else 0,
        "razor_pnl_outlier_count": int(
            razor.filter(pl.col("razor_pnl").abs() > 1e6).height if not razor.is_empty() else 0
        ),
        "razor_vrp_outlier_count": int(
            razor.filter(pl.col("razor_vrp").abs() > 1e6).height if not razor.is_empty() else 0
        ),
        "archiver_run_count": archiver_runs.height,
        "archiver_gap_count": archiver_runs.filter(pl.col("status") == "gap").height,
        "archiver_failed_count": archiver_runs.filter(pl.col("status") == "failed").height,
        "archiver_duplicate_hours": int(
            archiver_runs.group_by("logical_ts").len().filter(pl.col("len") > 1).height
        ),
        "archiver_latest_run_lag_hours": latest_run_lag_hours,
    }


def validate_options_family(
    store: DatasetStore,
    start: datetime,
    end: datetime,
    *,
    underlying: str,
) -> dict[str, str | float | int | None]:
    surface = store.read(
        DatasetName.OPTIONS_SURFACE_SNAPSHOT,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    carry = store.read(
        DatasetName.OPTIONS_CARRY_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    skew = store.read(
        DatasetName.OPTIONS_SKEW_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    dislocation = store.read(
        DatasetName.OPTIONS_DISLOCATION_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    positioning = store.read(
        DatasetName.OPTIONS_POSITIONING_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    cone = store.read(
        DatasetName.VOL_CONE_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    term = store.read(
        DatasetName.TERM_STRUCTURE_MOMENTUM_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    gamma = store.read(
        DatasetName.GAMMA_EXPOSURE_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    funding = store.read(
        DatasetName.FUNDING_CONTEXT_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    breakeven = store.read(
        DatasetName.STRADDLE_BREAKEVEN_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    transitions = store.read(
        DatasetName.VOL_REGIME_TRANSITION_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    signals = store.read(
        DatasetName.OPTIONS_SIGNAL_SERIES,
        start=start,
        end=end,
        filters={"underlying": underlying},
    )
    relative = store.read(
        DatasetName.OPTIONS_RELATIVE_VALUE_SERIES,
        start=start,
        end=end,
    )
    return {
        "underlying": underlying,
        "surface_snapshot_rows": surface.height,
        "carry_rows": carry.height,
        "skew_rows": skew.height,
        "dislocation_rows": dislocation.height,
        "positioning_rows": positioning.height,
        "vol_cone_rows": cone.height,
        "term_structure_rows": term.height,
        "gamma_exposure_rows": gamma.height,
        "funding_context_rows": funding.height,
        "straddle_breakeven_rows": breakeven.height,
        "transition_rows": transitions.height,
        "relative_value_rows": relative.height,
        "options_signal_rows": signals.height,
        "surface_low_quality_rows": int(
            surface.filter(pl.col("quality_flag") != "ok").height if not surface.is_empty() else 0
        ),
        "carry_missing_realized_7d": int(
            carry.filter(pl.col("realized_vol_7d").is_null()).height if not carry.is_empty() else 0
        ),
        "skew_panic_rows": int(
            skew.filter(pl.col("crash_premium_regime") == "panic").height
            if not skew.is_empty()
            else 0
        ),
        "dislocation_max_residual": float(cast(float, dislocation["max_residual"].max()))
        if not dislocation.is_empty()
        else None,
        "positioning_max_oi_share": float(cast(float, positioning["open_interest_share"].max()))
        if not positioning.is_empty()
        else None,
        "gamma_max_share": (
            float(cast(float, gamma["gamma_share"].max())) if not gamma.is_empty() else None
        ),
        "funding_missing_rows": int(
            funding.filter(pl.col("funding_rate").is_null()).height if not funding.is_empty() else 0
        ),
        "options_signal_missing_state": int(
            signals.filter(pl.col("signal_state").is_null()).height if not signals.is_empty() else 0
        ),
    }
