"""
Instrument-level "Curve Regime" — distinct from the global macro regime
(services.data_fetcher.score_macro_scenarios / snapshot.macro_regime, one dominant
scenario for the whole market). This classifies a SINGLE instrument's current price/vol
state into one of 15 canonical regimes (spec below, provided by the user as an analyst
reference table), combining:

  - direction / dynamique: from the instrument's wavelet decomposition — the SAME cache
    services.wavelet_signals writes and the Wavelet tab reads (services.database.
    get_wavelet_decomposition_cache), so this tab can never disagree with the Wavelet tab
    it sits next to — plus the trend/regime signals instrument_service already computes
    (ATR ratio, momentum, MA slopes) for confirmation.
  - volatilite: realized-vol level (ATR vs its own 3-month average) + the direction that
    level has been moving in over the last ~10 sessions (rising/falling fast).
  - structure_iv / liquidite: Saxo options IV/skew/term-structure (services.saxo_iv_engine),
    only when this instrument has a saxo_option_symbol linked (Config -> Instruments
    Watchlist -> "Option") — otherwise both axes are "n/a" and excluded from scoring
    rather than guessed at. Only a single 25-delta skew point + term-structure shape exist
    today (see saxo_iv_engine.py), not a full smile/butterfly curve — so structure_iv only
    distinguishes flat / skew_put / skew_put_fort / degraded, not the finer smile/BF/RR
    categories some regime rows reference; those regimes fall back to their other axes.
  - facteur_dominant: the global macro regime's asset-class bias (already computed per
    snapshot) — shown alongside the match for context, NOT used as a hard scoring
    criterion, since instrument category -> macro asset-class is an approximate mapping.

Cross-asset correlation (the spec table's "Corrélation cross-actifs" column) has no
existing data source anywhere in this codebase (checked: only a portfolio-scoped pairwise
correlation exists, for the Risk radar) — always "n/a" here, not scored. A real version
would need a new rolling-correlation-vs-benchmark fetch; flagged as a known gap rather than
approximated.

This is a transparent rule-based scorer, not a black box: classify_instrument_regime()
returns every candidate regime's per-axis score breakdown, not just the winner, so a wrong
call is inspectable and the thresholds/weights can be tuned without re-deriving the whole
design.
"""
import logging
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# ── Regime reference table (digitized from the user-provided spec) ────────────────────
# Each entry's axis values are what THAT regime expects to see. `None` means "this axis
# doesn't discriminate this regime" (not scored against it). volatilite accepts either a
# level bucket (faible/moyenne/elevee/tres_elevee/explosive) or a trend word
# (hausse/baisse_rapide) — matched against the corresponding computed signal, see
# _score_regime() below.
REGIME_SPECS: List[Dict[str, Any]] = [
    {"key": "bull_trend", "label": "Bull Trend",
     "direction": "up", "dynamique": "progressive", "volatilite": "faible", "structure_iv": "flat",
     "facteur_dominant": "Croissance / BCE",
     "declencheurs": "BCE plus hawkish que Fed, croissance Europe",
     "strategies": "Bull Call Spread, Bull Put Spread", "exemple": "+120 pips, IV stable → +60 à 90%"},
    {"key": "bear_trend", "label": "Bear Trend",
     "direction": "down", "dynamique": "progressive", "volatilite": "faible", "structure_iv": "skew_put",
     "facteur_dominant": "USD / Diff. de taux",
     "declencheurs": "Fed hawkish, différentiel de taux",
     "strategies": "Bear Put Spread", "exemple": "-120 pips → +60 à 90%"},
    {"key": "bull_breakout", "label": "Bull Breakout",
     "direction": "up", "dynamique": "explosive", "volatilite": "elevee", "structure_iv": "smile",
     "facteur_dominant": "Événement",
     "declencheurs": "BCE surprise, cassure technique",
     "strategies": "Long Call, Call Backspread", "exemple": "+150 à 250%"},
    {"key": "bear_breakout", "label": "Bear Breakout",
     "direction": "down", "dynamique": "explosive", "volatilite": "elevee", "structure_iv": "skew_put_fort",
     "facteur_dominant": "Événement",
     "declencheurs": "Fed surprise, cassure, crise localisée",
     "strategies": "Long Put, Put Backspread", "exemple": "+150 à 250%"},
    {"key": "compression", "label": "Compression",
     "direction": "flat", "dynamique": "progressive", "volatilite": "faible", "structure_iv": "flat",
     "facteur_dominant": "Attente",
     "declencheurs": "Vacances, avant FOMC/BCE",
     "strategies": "Calendar, Gamma positif", "exemple": "Préparer une cassure"},
    {"key": "iv_crush", "label": "IV Crush",
     "direction": "flat", "dynamique": "progressive", "volatilite": "tres_elevee", "structure_iv": "flat",
     "facteur_dominant": "Décroissance du risque",
     "declencheurs": "Après BCE/Fed/NFP sans surprise",
     "strategies": "Iron Condor, Butterfly", "exemple": "+30 à 70%"},
    {"key": "whipsaw", "label": "Whipsaw",
     "direction": "flat", "dynamique": "chaotique", "volatilite": "elevee", "structure_iv": "smile",
     "facteur_dominant": "Flux contradictoires",
     "declencheurs": "Faux breakouts, marché sans conviction",
     "strategies": "Petite taille, Gamma contrôlé", "exemple": "Peu de stratégies robustes"},
    {"key": "stress_prolonge", "label": "Stress prolongé",
     "direction": "mixed", "dynamique": "progressive", "volatilite": "elevee", "structure_iv": "skew_put",
     "facteur_dominant": "Inflation / Taux / EM",
     "declencheurs": "Brexit, crise EM, inflation durable",
     "strategies": "Calendar directionnel, Diagonal", "exemple": "+40 à 80%"},
    {"key": "explosion_incertitude", "label": "Explosion d'incertitude",
     "direction": "mixed", "dynamique": "explosive", "volatilite": "explosive", "structure_iv": "skew_put_fort",
     "facteur_dominant": "Géopolitique / Banque centrale",
     "declencheurs": "Guerre, surprise extrême, banques",
     "strategies": "Long Straddle, Reverse Iron Condor", "exemple": "IV 5→8 : +50 à 120%"},
    {"key": "flash_crash", "label": "Flash Crash / Crise de liquidité",
     "direction": "mixed", "dynamique": "explosive", "volatilite": "explosive", "structure_iv": "degradee",
     "facteur_dominant": "Déleveraging",
     "declencheurs": "Vente forcée, défaut, crise systémique",
     "strategies": "Hedges existants uniquement", "exemple": "Priorité = survie et exécution"},
    {"key": "retour_normale", "label": "Retour à la normale",
     "direction": "flat", "dynamique": "progressive", "volatilite": "baisse_rapide", "structure_iv": "flat",
     "facteur_dominant": "Dissipation du risque",
     "declencheurs": "Cessez-le-feu, marché rassuré",
     "strategies": "Vente Vega", "exemple": "+40 à 80%"},
    {"key": "transition_regime", "label": "Transition de régime",
     "direction": "mixed", "dynamique": "accelere", "volatilite": "hausse", "structure_iv": "skew_put",
     "facteur_dominant": "Changement macro",
     "declencheurs": "Brent, COT, Fed/BCE changent progressivement",
     "strategies": "Calendar, Diagonal, structures hybrides", "exemple": "+40 à 90%"},
    {"key": "risk_on", "label": "Risk-On",
     "direction": "up", "dynamique": "progressive", "volatilite": "faible", "structure_iv": "flat",
     "facteur_dominant": "Sentiment",
     "declencheurs": "QE, détente géopolitique",
     "strategies": "Bull Spread", "exemple": "+50 à 80%"},
    {"key": "risk_off", "label": "Risk-Off",
     "direction": "down", "dynamique": "accelere", "volatilite": "elevee", "structure_iv": "skew_put",
     "facteur_dominant": "Sentiment",
     "declencheurs": "Flight to Quality",
     "strategies": "Bear Put, Long Put", "exemple": "+70 à 150%"},
    {"key": "dispersion", "label": "Dispersion / Décorrélation",
     "direction": "mixed", "dynamique": "variable", "volatilite": "moyenne", "structure_iv": None,
     "facteur_dominant": "Rotation sectorielle",
     "declencheurs": "Corrélations qui cassent",
     "strategies": "Paniers, dispersion, relative value",
     "exemple": "Plus adapté au multi-actifs qu'au single instrument"},
]

# Per-axis scoring weight — direction/dynamique/volatilite are the axes we can compute with
# real confidence (wavelets + price data, always available); structure_iv only weighs in
# when options data exists, so it's naturally excluded (not zero-penalized) otherwise.
_AXIS_WEIGHTS = {"direction": 3.0, "dynamique": 2.5, "volatilite": 2.0, "structure_iv": 1.5}


# ── Axis 1+2: direction & dynamique, from the wavelet cache + trend/regime signals ─────

def _wavelet_band_slopes(decomposition: Optional[Dict]) -> List[float]:
    """Last-point slope of each band's series (today vs yesterday) — same math as
    services.wavelet_signals._compute_slope, applied directly to whatever's already in the
    cached decomposition rather than recomputing anything."""
    if not decomposition:
        return []
    bands = decomposition.get("bands") or []
    slopes = []
    for band in bands:
        series = band.get("series") or []
        if len(series) >= 2 and series[-1] is not None and series[-2] is not None:
            slopes.append(float(series[-1]) - float(series[-2]))
    return slopes


def _axis_direction(trend: Dict, regime_signals: Dict, wavelet_slopes: List[float]) -> str:
    """'up'/'down' on a clear, consistent trend; 'flat' when momentum is negligible;
    'mixed' when the fast (wavelet) and slow (momentum/MA) signals disagree — that
    disagreement IS the "?"/"↑ ou ↓" state several regimes in the spec are built around
    (stress prolongé, explosion d'incertitude, transition, flash crash)."""
    mom = trend.get("momentum_1m_pct")
    ma_above = regime_signals.get("ma50_above_ma200")
    if mom is None:
        return "flat"
    slow_dir = "up" if mom > 1.0 else "down" if mom < -1.0 else "flat"
    if wavelet_slopes:
        up_count = sum(1 for s in wavelet_slopes if s > 0)
        down_count = sum(1 for s in wavelet_slopes if s < 0)
        fast_dir = "up" if up_count > down_count else "down" if down_count > up_count else "flat"
        if slow_dir != "flat" and fast_dir != "flat" and slow_dir != fast_dir:
            return "mixed"
    if slow_dir == "flat":
        return "flat"
    if ma_above is not None:
        # MA50/MA200 disagreeing with recent momentum direction = the trend is stalling/
        # reversing under the surface, not a clean trend — treat as mixed too.
        ma_dir = "up" if ma_above else "down"
        if ma_dir != slow_dir:
            return "mixed"
    return slow_dir


def _axis_dynamique(trend: Dict, regime_signals: Dict, wavelet_slopes: List[float]) -> str:
    """progressive (steady) / explosive (sharp acceleration) / chaotique (bands
    disagreeing on direction) / accelere (momentum building) / variable (bands agree on
    neither direction nor magnitude — dispersion-like)."""
    vol_ratio = regime_signals.get("vol_ratio_pct")
    mom_1m = trend.get("momentum_1m_pct")
    mom_3m = trend.get("momentum_3m_pct")

    if wavelet_slopes and len(wavelet_slopes) >= 2:
        signs = [1 if s > 0 else -1 if s < 0 else 0 for s in wavelet_slopes]
        disagreement = len(set(signs)) > 1 and 0 not in signs  # genuine split, not just noise near zero
    else:
        disagreement = False

    if vol_ratio is not None and vol_ratio > 180:
        return "explosive"
    if disagreement and vol_ratio is not None and vol_ratio > 110:
        return "chaotique"
    if disagreement:
        return "variable"
    if mom_1m is not None and mom_3m is not None and mom_3m != 0:
        # This month's pace vs the trailing 3-month pace — accelerating if 1m alone is
        # already running hotter than the average of the last 3.
        monthly_avg_3m = mom_3m / 3
        if abs(mom_1m) > abs(monthly_avg_3m) * 1.8 and abs(mom_1m) > 3:
            return "accelere"
    return "progressive"


# ── Axis 3: volatilite level + trend, from ATR ratio + the realized-vol series ─────────

def _axis_volatilite(trend: Dict, regime_signals: Dict, vol_series: List[Dict]) -> Dict[str, Optional[str]]:
    """Returns {"level": ..., "trend": ...} — level is the primary bucket regimes are
    matched against; trend (hausse/baisse_rapide/stable) only matters for the couple of
    regimes defined by a MOVE in vol rather than its absolute level (Retour à la normale,
    Transition de régime)."""
    vol_ratio = regime_signals.get("vol_ratio_pct")  # ATR14 / its own 3-month average, %
    level = None
    if vol_ratio is not None:
        if vol_ratio > 200:
            level = "explosive"
        elif vol_ratio > 160:
            level = "tres_elevee"
        elif vol_ratio > 115:
            level = "elevee"
        elif vol_ratio >= 85:
            level = "moyenne"
        else:
            level = "faible"

    trend_word = None
    values = [p.get("value") for p in (vol_series or []) if p.get("value") is not None]
    if len(values) >= 10:
        recent = values[-1]
        baseline = sum(values[-10:-3]) / len(values[-10:-3]) if len(values[-10:-3]) else None
        if baseline and baseline > 0:
            change_pct = (recent - baseline) / baseline * 100
            if change_pct <= -25:
                trend_word = "baisse_rapide"
            elif change_pct >= 30:
                trend_word = "hausse"
            else:
                trend_word = "stable"
    return {"level": level, "trend": trend_word}


# ── Axis 4+5: structure_iv / liquidite, from Saxo options data when linked ─────────────

def _axis_options(watchlist_row: Optional[Dict]) -> Dict[str, Any]:
    """None when this instrument has no saxo_option_symbol link — the caller excludes
    structure_iv from scoring entirely in that case rather than defaulting to 'flat'."""
    if not watchlist_row or not watchlist_row.get("saxo_option_symbol"):
        return {"structure_iv": None, "liquidite": None, "iv_snapshot": None}
    try:
        from services.saxo_iv_engine import get_saxo_iv_snapshot
        snap = get_saxo_iv_snapshot(watchlist_row["saxo_option_symbol"])
    except Exception as e:
        logger.warning(f"[curve_regime] IV snapshot failed for '{watchlist_row.get('saxo_option_symbol')}': {e}")
        return {"structure_iv": None, "liquidite": None, "iv_snapshot": None}

    if snap.get("iv_current_pct") is None:
        # No usable Saxo option data at all (empty/stale chain) — this itself is the
        # "surface cassée" signal, not just an absent one.
        return {"structure_iv": "degradee", "liquidite": "degradee", "iv_snapshot": snap}

    skew_pct = (snap.get("skew") or {}).get("skew_pct")
    structure_iv = "flat"
    if skew_pct is not None:
        if skew_pct > 4:
            structure_iv = "skew_put_fort"
        elif skew_pct > 1.2:
            structure_iv = "skew_put"
        elif skew_pct < -1.2:
            structure_iv = "smile"  # calls bid up relative to puts — treated as the closest bucket we can score

    liquidite = "faible" if (snap.get("history_days") or 0) < 5 else "normal"

    return {"structure_iv": structure_iv, "liquidite": liquidite, "iv_snapshot": snap}


# ── Scoring ──────────────────────────────────────────────────────────────────────────

def _score_regime(spec: Dict, profile: Dict) -> Dict[str, Any]:
    axis_hits: Dict[str, bool] = {}
    total_weight = 0.0
    earned = 0.0

    for axis in ("direction", "dynamique"):
        expected = spec.get(axis)
        observed = profile.get(axis)
        if expected is None or observed is None:
            continue
        w = _AXIS_WEIGHTS[axis]
        total_weight += w
        hit = observed == expected
        if hit:
            earned += w
        axis_hits[axis] = hit

    # volatilite: match level OR trend, whichever the spec entry is actually about
    expected_vol = spec.get("volatilite")
    vol = profile.get("volatilite") or {}
    if expected_vol is not None:
        w = _AXIS_WEIGHTS["volatilite"]
        total_weight += w
        observed_vol = vol.get("trend") if expected_vol in ("hausse", "baisse_rapide") else vol.get("level")
        hit = observed_vol == expected_vol
        if hit:
            earned += w
        axis_hits["volatilite"] = hit

    expected_iv = spec.get("structure_iv")
    observed_iv = profile.get("structure_iv")
    if expected_iv is not None and observed_iv is not None:
        w = _AXIS_WEIGHTS["structure_iv"]
        total_weight += w
        hit = observed_iv == expected_iv
        if hit:
            earned += w
        axis_hits["structure_iv"] = hit

    score = (earned / total_weight) if total_weight > 0 else 0.0
    return {"key": spec["key"], "label": spec["label"], "score": round(score, 3), "axis_hits": axis_hits}


def classify_instrument_regime(instrument_id: str, snapshot: Dict) -> Dict[str, Any]:
    """Main entry point — called by routers/instruments.py with the already-built snapshot
    (indicators/regime/trend/macro_regime) so this doesn't refetch price history a second
    time. Returns the top match, full ranked scores, the computed axis profile (for the UI
    to show its work), and the matched regime's reference info (déclencheurs/stratégies)."""
    from services.instrument_service import resolve_watchlist_ticker
    from services.database import get_wavelet_decomposition_cache, get_instruments_watchlist

    watchlist_ticker = resolve_watchlist_ticker(instrument_id)
    cached = get_wavelet_decomposition_cache(watchlist_ticker)
    decomposition = cached.get("decomposition") if cached else None
    wavelet_slopes = _wavelet_band_slopes(decomposition)

    watchlist_row = next(
        (r for r in get_instruments_watchlist() if r["ticker"].upper() == watchlist_ticker.upper()), None
    )

    regime_signals = (snapshot.get("regime") or {}).get("signals") or {}
    trend = snapshot.get("trend") or {}
    vol_series = (snapshot.get("indicators") or {}).get("volatility") or []

    direction = _axis_direction(trend, regime_signals, wavelet_slopes)
    dynamique = _axis_dynamique(trend, regime_signals, wavelet_slopes)
    volatilite = _axis_volatilite(trend, regime_signals, vol_series)
    options_axes = _axis_options(watchlist_row)

    profile = {
        "direction": direction,
        "dynamique": dynamique,
        "volatilite": volatilite,
        "structure_iv": options_axes["structure_iv"],
        "liquidite": options_axes["liquidite"],
        "correlation": None,  # no data source — always n/a, see module docstring
    }

    scored = sorted(
        (_score_regime(spec, profile) for spec in REGIME_SPECS),
        key=lambda s: -s["score"],
    )
    top = scored[0] if scored else None
    top_spec = next((s for s in REGIME_SPECS if s["key"] == (top or {}).get("key")), None)

    macro_regime = snapshot.get("macro_regime") or {}
    config_category = (snapshot.get("instrument") or {}).get("category")
    facteur_dominant_bias = _macro_bias_for_category(macro_regime.get("asset_bias") or {}, config_category)

    return {
        "instrument_id": instrument_id,
        "watchlist_ticker": watchlist_ticker,
        "profile": profile,
        "top_match": {
            "key": top_spec["key"], "label": top_spec["label"], "score": top["score"],
            "axis_hits": top["axis_hits"],
            "facteur_dominant_ref": top_spec["facteur_dominant"],
            "declencheurs": top_spec["declencheurs"],
            "strategies": top_spec["strategies"],
            "exemple": top_spec["exemple"],
        } if top_spec else None,
        "ranked": scored,
        "macro_regime_label": macro_regime.get("label"),
        "facteur_dominant_bias": facteur_dominant_bias,
        "iv_snapshot": options_axes.get("iv_snapshot"),
        "has_options_data": options_axes["structure_iv"] is not None,
    }


# instruments.json's own category vocabulary -> macro asset_bias's asset-class keys
_CATEGORY_TO_ASSET_BIAS_KEY = {
    "fx": "forex", "energy": "energy", "metal": "metals",
    "equity_index": "indices", "equity_intl": "indices", "stock": "equities",
}


def _macro_bias_for_category(asset_bias: Dict[str, str], category: Optional[str]) -> Optional[str]:
    key = _CATEGORY_TO_ASSET_BIAS_KEY.get(category or "")
    return asset_bias.get(key) if key else None


async def refresh_watchlist_curve_regimes() -> int:
    """Classify every Cockpit Watchlist instrument and cache the result — called from
    services.wavelet_scheduler's periodic refresh pass, same cadence as the wavelet cache,
    so the Dashboard's Curve Regime card never needs a live classify call per page view and
    always agrees with what Instrument Analysis's Curve Regime tab would compute right now.
    Auto quick-adds a Watchlist ticker into the Instrument Analysis catalog first if it
    isn't there yet (services.instrument_service.quick_add_instrument_from_watchlist) —
    classify_instrument_regime needs a full snapshot (price history + indicators), which
    only exists for catalog-registered instruments. Returns the number classified."""
    from services.database import get_instruments_watchlist, save_curve_regime_cache
    from services.instrument_service import quick_add_instrument_from_watchlist, get_snapshot

    count = 0
    for item in get_instruments_watchlist():
        ticker = item["ticker"]
        try:
            added = quick_add_instrument_from_watchlist(ticker)
            snapshot = await get_snapshot(added["id"], period="1y", interval="1d")
            if "error" in snapshot:
                logger.warning(f"[curve_regime] Skipping '{ticker}': snapshot error: {snapshot['error']}")
                continue
            classification = classify_instrument_regime(added["id"], snapshot)
            save_curve_regime_cache(ticker, classification)
            count += 1
        except Exception as e:
            logger.warning(f"[curve_regime] Skipping '{ticker}': {e}")
            continue
    logger.info(f"[curve_regime] Refreshed {count}/{len(get_instruments_watchlist())} Watchlist instruments")
    return count
