"""Calibrazione page — distribuzioni storiche dei segnali per tarare le soglie.

Legge dalla tabella ``market_snapshots`` (popolata dal job dedicato cron
``*/15``). Per ogni metrica osservabile mostra:

* istogramma + linea verticale della soglia attuale di config,
* percentili P5/P10/P25/P50/P75/P90/P95,
* percentuale di tick che la soglia attuale avrebbe filtrato.

L'idea è scegliere le soglie sui percentili reali del proprio
ambiente (testnet o mainnet), invece di valori fissati a istinto.
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from pathlib import Path

import pandas as pd
import plotly.graph_objects as go
import streamlit as st

from cerbero_bite.config.loader import load_strategy
from cerbero_bite.core.adaptive_threshold import compute_adaptive_threshold
from cerbero_bite.gui.data_layer import (
    DEFAULT_DB_PATH,
    humanize_dt,
    load_market_snapshots,
)
from cerbero_bite.state.models import MarketSnapshotRecord


def _resolve_db() -> Path:
    return Path(os.environ.get("CERBERO_BITE_GUI_DB", DEFAULT_DB_PATH))


@dataclass(frozen=True)
class MetricSpec:
    """Descrittore della metrica da plottare."""

    field: str
    title: str
    unit: str
    threshold_label: str | None
    threshold_value: float | None
    threshold_direction: str  # "below" o "above"  (filtra se valore è X soglia)


def _metric_specs(strategy: object | None) -> list[MetricSpec]:
    """Costruisce gli spec leggendo le soglie correnti da strategy.yaml."""
    funding_max: float | None = None
    dealer_min: float | None = None
    dvol_min: float | None = None
    if strategy is not None:
        try:
            funding_max = float(strategy.entry.funding_max_abs_annualized)  # type: ignore[attr-defined]
        except Exception:
            funding_max = None
        try:
            dealer_min = float(strategy.entry.dealer_gamma_min)  # type: ignore[attr-defined]
        except Exception:
            dealer_min = None
        try:
            dvol_min = float(strategy.entry.dvol_min)  # type: ignore[attr-defined]
        except Exception:
            dvol_min = None

    specs: list[MetricSpec] = [
        MetricSpec(
            field="dvol",
            title="DVOL",
            unit="%",
            threshold_label=(
                f"DVOL min={dvol_min:.0f}" if dvol_min is not None else None
            ),
            threshold_value=dvol_min,
            threshold_direction="below",
        ),
        MetricSpec(
            field="realized_vol_30d",
            title="Realized vol 30d",
            unit="%",
            threshold_label=None,
            threshold_value=None,
            threshold_direction="below",
        ),
        MetricSpec(
            field="iv_minus_rv",
            title="IV − RV (30d)",
            unit="%",
            threshold_label=None,
            threshold_value=None,
            threshold_direction="below",
        ),
        MetricSpec(
            field="funding_perp_annualized",
            title="Funding perp annualized",
            unit="frazione",
            threshold_label=(
                f"|funding| max={funding_max:.2f}"
                if funding_max is not None
                else None
            ),
            threshold_value=funding_max,
            threshold_direction="above_abs",
        ),
        MetricSpec(
            field="funding_cross_annualized",
            title="Funding cross median annualized",
            unit="frazione",
            threshold_label=None,
            threshold_value=None,
            threshold_direction="above_abs",
        ),
        MetricSpec(
            field="dealer_net_gamma",
            title="Dealer net gamma",
            unit="USD",
            threshold_label=(
                f"min={dealer_min:.0f}"
                if dealer_min is not None
                else None
            ),
            threshold_value=dealer_min,
            threshold_direction="below",
        ),
        MetricSpec(
            field="oi_delta_pct_4h",
            title="OI delta % (4h)",
            unit="%",
            threshold_label=None,
            threshold_value=None,
            threshold_direction="below",
        ),
    ]
    return specs


def _series(records: list[MarketSnapshotRecord], field: str) -> pd.Series:
    values: list[float] = []
    for r in records:
        v = getattr(r, field, None)
        if v is None:
            continue
        try:
            values.append(float(v))
        except (TypeError, ValueError):
            continue
    return pd.Series(values, dtype="float64")


def _percent_blocked(s: pd.Series, spec: MetricSpec) -> float | None:
    if spec.threshold_value is None or s.empty:
        return None
    if spec.threshold_direction == "below":
        return float((s < spec.threshold_value).mean())
    if spec.threshold_direction == "above_abs":
        return float((s.abs() > spec.threshold_value).mean())
    if spec.threshold_direction == "above":
        return float((s > spec.threshold_value).mean())
    return None


def _percentiles_strip(s: pd.Series) -> None:
    if s.empty:
        st.caption("(nessun dato)")
        return
    quantiles = [0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95]
    # Inline markdown compatto: i valori a 4 cifre significative
    # cadono regolarmente fuori dalla colonna se renderizzati con
    # ``st.metric`` (font fisso, label sopra, no shrink). Render
    # orizzontale con font ridotto evita il troncamento.
    parts = [
        f"<b>P{int(q * 100)}</b> {s.quantile(q):.4g}"
        for q in quantiles
    ]
    html = (
        "<div style='font-size:0.85rem;line-height:1.5;"
        "white-space:nowrap;overflow-x:auto;'>"
        + " &nbsp;·&nbsp; ".join(parts)
        + "</div>"
    )
    st.markdown(html, unsafe_allow_html=True)


def _render_metric(spec: MetricSpec, records: list[MarketSnapshotRecord]) -> None:
    s = _series(records, spec.field)
    if s.empty:
        st.subheader(f"{spec.title}")
        st.info(
            f"Nessun valore disponibile per `{spec.field}`. "
            "Avvia il job `market_snapshot` (engine attivo, cron */15) per "
            "popolare la tabella."
        )
        return

    st.subheader(f"{spec.title} ({spec.unit})")

    pct_blocked = _percent_blocked(s, spec)
    cols = st.columns(4)
    cols[0].metric("Tick raccolti", len(s))
    cols[1].metric("Min", f"{s.min():.4g}")
    cols[2].metric("Max", f"{s.max():.4g}")
    cols[3].metric(
        "% bloccato dalla soglia",
        f"{pct_blocked:.0%}" if pct_blocked is not None else "—",
        help=(
            "Frazione di tick che la soglia di config avrebbe filtrato"
            f" se applicata a questa serie ({spec.threshold_direction})."
        ),
    )

    fig = go.Figure()
    fig.add_trace(go.Histogram(x=s, nbinsx=40, opacity=0.85, name="distrib."))
    if spec.threshold_value is not None:
        fig.add_vline(
            x=spec.threshold_value,
            line_dash="dash",
            line_color="red",
            line_width=2,
            annotation_text=spec.threshold_label or f"soglia {spec.threshold_value}",
            annotation_position="top",
        )
        if spec.threshold_direction == "above_abs":
            # Disegna anche il bound negativo per i filtri simmetrici.
            fig.add_vline(
                x=-spec.threshold_value,
                line_dash="dash",
                line_color="red",
                line_width=2,
                annotation_text=None,
            )
    fig.update_layout(
        height=280,
        margin={"l": 10, "r": 10, "t": 30, "b": 10},
        xaxis_title=spec.unit,
        yaxis_title="numero tick",
    )
    st.plotly_chart(fig, use_container_width=True)

    _percentiles_strip(s)


def _render_adaptive_gate_panel(
    strategy: object | None,
    records: list[MarketSnapshotRecord],
) -> None:
    """Pannello informativo sul gate IV-RV adattivo (read-only)."""
    if strategy is None:
        return

    try:
        entry = strategy.entry  # type: ignore[attr-defined]
    except AttributeError:
        return

    if not getattr(entry, "iv_minus_rv_filter_enabled", False):
        st.subheader("🎯 Gate IV-RV adattivo")
        st.info("Gate IV-RV disabilitato nel profilo corrente.")
        st.divider()
        return

    st.subheader("🎯 Gate IV-RV adattivo")

    # records DESC (newest first) → history ASC con NULL/fetch_ok=0 esclusi
    iv_rv_history: list[Decimal] = []
    distinct_days: set[str] = set()
    for r in reversed(records):
        if r.fetch_ok and r.iv_minus_rv is not None:
            iv_rv_history.append(r.iv_minus_rv)
            distinct_days.add(r.timestamp.date().isoformat())

    n_ticks = len(iv_rv_history)
    n_days = len(distinct_days)
    target = int(getattr(entry, "iv_minus_rv_window_target_days", 60))
    min_days = int(getattr(entry, "iv_minus_rv_window_min_days", 30))

    if n_days < 1:
        status = "🟡 Warmup hard (nessun giorno coperto)"
    elif n_days < min_days:
        status = f"🟡 Warmup ({n_days}/{min_days}g — finestra crescente)"
    elif n_days < target:
        status = f"🟢 Attivo (finestra {min_days}g, target {target}g)"
    else:
        status = f"🟢 Attivo (finestra stabile {target}g)"
    st.markdown(f"**Status:** {status} · {n_ticks} tick complessivi")

    # Latest tick
    iv_rv_now: Decimal | None = None
    dvol_now: Decimal | None = None
    latest_ts: datetime | None = None
    for r in records:  # records DESC
        if r.fetch_ok:
            iv_rv_now = r.iv_minus_rv
            dvol_now = r.dvol
            latest_ts = r.timestamp
            break

    adaptive_on = bool(getattr(entry, "iv_minus_rv_adaptive_enabled", False))
    floor = Decimal(str(getattr(entry, "iv_minus_rv_min", "0")))

    if adaptive_on:
        percentile = Decimal(
            str(getattr(entry, "iv_minus_rv_percentile", "0.25"))
        )
        try:
            threshold = compute_adaptive_threshold(
                history=iv_rv_history,
                n_days=n_days,
                percentile=percentile,
                absolute_floor=floor,
            )
        except ValueError as exc:
            st.warning(f"Configurazione gate non valida: {exc}")
            threshold = None

        c1, c2, c3 = st.columns(3)
        pct_label = int(percentile * 100)
        c1.metric(
            f"Soglia P{pct_label} rolling",
            f"{threshold:.2f}" if threshold is not None else "—",
            help="Soglia adattiva = max(percentile, floor)",
        )
        c2.metric(
            "IV-RV ultimo tick",
            f"{iv_rv_now:.2f}" if iv_rv_now is not None else "—",
        )
        c3.metric("Floor assoluto", f"{floor:.2f}")

        if threshold is not None and iv_rv_now is not None:
            verdict = "✅ PASS" if iv_rv_now >= threshold else "❌ SKIP"
            st.markdown(f"**Decisione hypothetical:** {verdict}")
    else:
        st.write(f"Modalità statica: floor = {floor} vol pts")

    if bool(getattr(entry, "vol_of_vol_guard_enabled", False)):
        st.markdown("---")
        st.markdown("**Vol-of-Vol guard**")
        threshold_pt = Decimal(
            str(getattr(entry, "vol_of_vol_threshold_pt", "5"))
        )
        lookback_h = int(getattr(entry, "vol_of_vol_lookback_hours", 24))

        # Find tick closest to latest_ts - lookback hours, tolerance 15 min
        dvol_lookback: Decimal | None = None
        if latest_ts is not None and dvol_now is not None:
            target_ts = latest_ts - timedelta(hours=lookback_h)
            best_delta = timedelta(minutes=15)
            for r in records:
                if not r.fetch_ok or r.dvol is None:
                    continue
                d = abs(r.timestamp - target_ts)
                if d <= best_delta:
                    best_delta = d
                    dvol_lookback = r.dvol

        if dvol_lookback is not None and dvol_now is not None:
            delta = abs(dvol_now - dvol_lookback)
            c1, c2 = st.columns(2)
            c1.metric(f"|ΔDVOL {lookback_h}h|", f"{delta:.2f}")
            c2.metric("Soglia VoV", f"{threshold_pt:.2f}")
            verdict = "✅ PASS" if delta < threshold_pt else "❌ SKIP"
            st.markdown(f"**Verdict:** {verdict}")
        else:
            st.info(f"Lookback {lookback_h}h non disponibile (gap dati).")

    st.divider()


def render() -> None:
    st.title("📐 Calibrazione")
    st.caption(
        "Distribuzioni storiche dei segnali raccolti dal job "
        "`market_snapshot` (cron */15). Usa i percentili reali per "
        "tarare le soglie in `strategy.yaml` invece di valori a istinto."
    )

    db_path = _resolve_db()

    col_a, col_b = st.columns(2)
    asset = col_a.selectbox("Asset", options=["ETH", "BTC"], index=0)
    window = col_b.selectbox(
        "Finestra",
        options=[
            "Tutto lo storico",
            "Ultime 24h",
            "Ultimi 7 giorni",
            "Ultimi 30 giorni",
        ],
        index=0,
    )

    now = datetime.now(UTC)
    start: datetime | None = None
    if window == "Ultime 24h":
        start = now - timedelta(hours=24)
    elif window == "Ultimi 7 giorni":
        start = now - timedelta(days=7)
    elif window == "Ultimi 30 giorni":
        start = now - timedelta(days=30)

    records = load_market_snapshots(
        asset=asset, db_path=db_path, start=start, limit=5000
    )

    if not records:
        st.info(
            "Nessun snapshot disponibile in questa finestra per "
            f"`{asset}`. Avvia l'engine (`cerbero-bite start`) e attendi "
            "almeno un tick del job `market_snapshot` (cron */15)."
        )
        return

    st.caption(
        f"{len(records)} snapshot · primo {humanize_dt(records[-1].timestamp)} "
        f"· ultimo {humanize_dt(records[0].timestamp)}"
    )

    # Conteggio fetch_ok per qualità delle serie
    n_ok = sum(1 for r in records if r.fetch_ok)
    cols = st.columns(3)
    cols[0].metric("Snapshot totali", len(records))
    cols[1].metric("fetch_ok = true", n_ok)
    cols[2].metric(
        "Tasso ok",
        f"{n_ok / len(records):.0%}" if records else "—",
    )
    st.divider()

    # Carica strategy.yaml per leggere le soglie correnti
    try:
        strategy = load_strategy(Path("strategy.yaml"))
    except Exception as exc:
        st.warning(
            f"Impossibile leggere `strategy.yaml`: {type(exc).__name__}: {exc}"
        )
        strategy = None

    specs = _metric_specs(strategy)

    _render_adaptive_gate_panel(strategy, records)

    for spec in specs:
        _render_metric(spec, records)
        st.divider()


render()
