"""
Instrument Dashboard Router.
Exposes per-instrument snapshot (price, indicators, regime, trend, events) and AI narrative.
"""
import json
import math
from datetime import datetime, timedelta, date as date_type
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from typing import List, Dict, Any, Optional

from services.instrument_service import (
    get_all_instruments,
    get_instrument,
    get_snapshot,
    get_narrative,
    update_instrument_drivers,
    update_instrument_saxo_link,
    quick_add_instrument_from_watchlist,
)

class DriverUpdate(BaseModel):
    drivers: List[Dict[str, Any]]


class SaxoLinkBody(BaseModel):
    saxo_symbol: Optional[str] = None


class QuickAddBody(BaseModel):
    ticker: str

router = APIRouter(prefix="/api/instruments", tags=["instruments"])


@router.get("", response_model=List[Dict[str, Any]])
def list_instruments() -> List[Dict[str, Any]]:
    """
    Return all instrument configurations (no price data).
    """
    return get_all_instruments()


@router.post("/quick-add")
def quick_add(body: QuickAddBody) -> Dict[str, Any]:
    """Bring a Cockpit Watchlist ticker into Instrument Analysis on the fly, copying over
    its saxo_quote_symbol link automatically — see instrument_service.quick_add_instrument_from_watchlist."""
    try:
        return quick_add_instrument_from_watchlist(body.ticker)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))


@router.get("/{instrument_id}/snapshot")
async def instrument_snapshot(
    instrument_id: str,
    period: str = Query(default="1y", description="yfinance period string (e.g. 1y, 6mo, 3mo)"),
    interval: str = Query(default="1d", description="yfinance interval string (e.g. 1d, 1wk)"),
) -> Dict[str, Any]:
    """
    Full snapshot for a single instrument: price data, indicators, regime, trend, events.
    """
    config = get_instrument(instrument_id)
    if not config:
        raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")

    snapshot = await get_snapshot(instrument_id, period=period, interval=interval)

    if "error" in snapshot:
        raise HTTPException(status_code=500, detail=snapshot["error"])

    return snapshot


@router.get("/{instrument_id}/wavelet-cache")
def wavelet_cache(instrument_id: str) -> Dict[str, Any]:
    """Latest full band decomposition for this instrument, from the same per-cycle scan
    that feeds the Dashboard's Wavelets Signal card (services.wavelet_signals.
    scan_watchlist_wavelet_signals) — NOT a live recompute. Instrument Analysis's Wavelet
    tab reads this by default so opening it never triggers its own analysis and always
    agrees with the Signal card; "Start analyse" is still there for an on-demand live run
    with custom parameters, which does NOT overwrite this cache.

    Looked up by the Cockpit Watchlist ticker the cache is keyed on, resolved via
    instrument_service.resolve_watchlist_ticker — handles both the yfinance-suffix case
    (EURUSD=X -> EURUSD) and the alias case where the catalog id and the Watchlist ticker
    share no common substring at all (GC=F -> GOLD, ^GSPC -> SP500)."""
    from services.instrument_service import resolve_watchlist_ticker
    from services.database import get_wavelet_decomposition_cache
    cached = get_wavelet_decomposition_cache(resolve_watchlist_ticker(instrument_id))
    if not cached or not cached.get("decomposition"):
        raise HTTPException(status_code=404, detail=f"Aucune décomposition wavelet en cache pour '{instrument_id}'")
    return {
        "computed_at": cached["computed_at"],
        "method": cached["method"],
        "wavelet": cached["wavelet"],
        "num_levels": cached["num_levels"],
        "lookback_days": cached["lookback_days"],
        **cached["decomposition"],
    }


@router.get("/{instrument_id}/curve-regime")
async def curve_regime(
    instrument_id: str,
    period: str = Query(default="1y"),
    interval: str = Query(default="1d"),
) -> Dict[str, Any]:
    """Instrument-level regime classification (Direction/Dynamique/Volatilité/Structure IV,
    matched against the 15-regime reference table) — distinct from the global macro regime
    already shown elsewhere. See services.curve_regime module docstring for the full
    methodology and its data-availability caveats (no cross-asset correlation source,
    structure_iv only available when this instrument's options chain is Saxo-linked)."""
    config = get_instrument(instrument_id)
    if not config:
        raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")

    snapshot = await get_snapshot(instrument_id, period=period, interval=interval)
    if "error" in snapshot:
        raise HTTPException(status_code=500, detail=snapshot["error"])

    from services.curve_regime import classify_instrument_regime
    return classify_instrument_regime(instrument_id, snapshot)


@router.post("/{instrument_id}/narrative")
async def generate_narrative(
    instrument_id: str,
    force: bool = Query(default=False, description="Force regeneration, bypass cache"),
) -> Dict[str, Any]:
    """
    Generate (or return cached) a French AI narrative for the instrument.
    """
    config = get_instrument(instrument_id)
    if not config:
        raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")

    narrative = await get_narrative(instrument_id, force=force)

    return {
        "instrument_id": instrument_id.upper(),
        "instrument_name": config.get("name", instrument_id),
        "narrative": narrative,
    }


@router.put("/{instrument_id}/drivers")
def update_drivers(instrument_id: str, body: DriverUpdate) -> Dict[str, Any]:
    """
    Persist updated drivers (label, weight, keywords) for an instrument to instruments.json.
    """
    config = get_instrument(instrument_id)
    if not config:
        raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")

    try:
        update_instrument_drivers(instrument_id, body.drivers)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

    return {"ok": True, "instrument_id": instrument_id.upper(), "drivers_count": len(body.drivers)}


@router.put("/{instrument_id}/saxo-link")
def set_saxo_link(instrument_id: str, body: SaxoLinkBody) -> Dict[str, Any]:
    """Link this instrument to the Saxo symbol used to price it (chart, indicators, regime,
    wavelets) instead of yfinance — or pass null to unlink. Mirrors
    routers/instruments_watchlist.py's /{ticker}/saxo-quote-link, but persisted in
    instruments.json (this catalog's own store) rather than the instruments_watchlist table."""
    config = get_instrument(instrument_id)
    if not config:
        raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")

    saxo_symbol = (body.saxo_symbol or "").strip().upper() or None
    try:
        update_instrument_saxo_link(instrument_id, saxo_symbol)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

    return {"instrument_id": instrument_id.upper(), "saxo_quote_symbol": saxo_symbol}


# ── Instrument mult (pips → price conversion) ─────────────────────────────────

_INST_MULT: Dict[str, int] = {"EURUSD": 10000, "GBPUSD": 10000, "USDJPY": 100, "AUDUSD": 10000}

def _get_mult(inst: str) -> int:
    return _INST_MULT.get(inst.upper(), 10)


def _decay(days_after: int, absorption_days: int, decay_type: str) -> float:
    """Decay factor ∈ [0,1] for a given number of days after the event."""
    if days_after < 0:
        return 0.0
    if decay_type == "step":
        return 1.0 if days_after <= absorption_days else 0.0
    elif decay_type == "linear":
        return max(0.0, 1.0 - days_after / max(absorption_days, 1))
    else:  # exp — 3 time-constants reach ~5% at absorption_days
        lam = 3.0 / max(absorption_days, 1)
        return math.exp(-lam * days_after)


@router.get("/{instrument_id}/theoretical-curve")
def get_theoretical_curve(
    instrument_id: str,
    period: str = Query("1y"),
) -> List[Dict[str, Any]]:
    """
    Courbe théorique composite : pour chaque jour calendaire de la période,
    somme des impacts décroissants des analyses causales stockées.

    Retourne [{date, cumulative_pips, contributions: [{template_name, event_name, event_date, pips, decay_factor}]}]
    """
    from services.database import get_conn

    period_lookback: Dict[str, int] = {
        "5d": 7, "1mo": 35, "3mo": 95, "6mo": 190,
        "1y": 370, "2y": 740, "5y": 1830,
    }
    lookback = period_lookback.get(period, 370)

    date_to   = datetime.utcnow().date()
    date_from = date_to - timedelta(days=lookback)
    # Fetch events that started before date_from too — they may still be decaying into the window
    extended_from = date_from - timedelta(days=90)

    inst_upper = instrument_id.upper()

    conn = get_conn()
    try:
        rows = conn.execute("""
            SELECT a.id,
                   a.prediction_json,
                   a.activation_score,
                   e.start_date  AS event_date,
                   e.name        AS event_name,
                   t.name        AS template_name,
                   t.calibration_json
            FROM   causal_event_analyses a
            JOIN   market_events          e ON e.id = a.market_event_id
            JOIN   causal_graph_templates t ON t.id = a.template_id
            WHERE  a.instrument = ?
              AND  e.start_date >= ?
              AND  e.start_date <= ?
            ORDER  BY e.start_date
        """, (inst_upper, str(extended_from), str(date_to))).fetchall()
    finally:
        conn.close()

    # ── Build calendar-day series ─────────────────────────────────────────────
    all_dates: List[str] = []
    cur = date_from
    while cur <= date_to:
        all_dates.append(str(cur))
        cur += timedelta(days=1)

    curve: Dict[str, Dict] = {
        d: {"cumulative_pips": 0.0, "contributions": []} for d in all_dates
    }

    for row in rows:
        r = dict(row)
        try:
            predictions = json.loads(r["prediction_json"] or "{}")
            calib       = json.loads(r["calibration_json"] or "{}")
        except Exception:
            continue

        # Extract predicted pips for this instrument from node_values dict
        inst_lower = inst_upper.lower()
        predicted_pips: Optional[float] = None
        if inst_lower in predictions:
            predicted_pips = float(predictions[inst_lower])
        else:
            for k, v in predictions.items():
                if inst_lower in k.lower():
                    try:
                        predicted_pips = float(v)
                        break
                    except (TypeError, ValueError):
                        pass

        if predicted_pips is None or predicted_pips == 0:
            continue

        absorption_days: int  = max(1, int(calib.get("absorption_days", 7)))
        dtype:           str  = str(calib.get("decay_type", "exp"))
        event_date_str:  str  = r["event_date"][:10]

        try:
            event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date()
        except ValueError:
            continue

        for d in all_dates:
            cal_date   = datetime.strptime(d, "%Y-%m-%d").date()
            days_after = (cal_date - event_date).days
            df         = _decay(days_after, absorption_days, dtype)
            if df < 0.01:
                continue
            contribution = round(predicted_pips * df, 2)
            curve[d]["cumulative_pips"] += contribution
            curve[d]["contributions"].append({
                "template_name": r["template_name"],
                "event_name":    r["event_name"],
                "event_date":    event_date_str,
                "pips":          contribution,
                "decay_factor":  round(df, 3),
            })

    # Round totals and strip empty-contribution days at the edges
    result = []
    for d in all_dates:
        entry = curve[d]
        entry["cumulative_pips"] = round(entry["cumulative_pips"], 1)
        result.append({"date": d, **entry})

    return result


# ── Libellés lisibles par catégorie ───────────────────────────────────────────
_CAT_LABELS: Dict[str, str] = {
    "central_bank":   "Banque Centrale",
    "monetary_shock": "Surprise Macro",
    "geopolitical":   "Géopolitique",
    "commodity":      "Commodités",
    "growth_shock":   "Croissance",
    "trade_policy":   "Commerce / Tarifs",
    "credit_stress":  "Stress Crédit",
    "sentiment":      "Sentiment & Position.",
    "technical":      "Technique",
    "positioning":    "Flux Institutionnels",
    "unclassified":   "Non Classifié",
}


@router.get("/{instrument_id}/factor-state")
def get_factor_state(
    instrument_id: str,
    at_date: Optional[str] = Query(None, description="YYYY-MM-DD (défaut: aujourd'hui)"),
) -> Dict[str, Any]:
    """
    Pression nette actuelle sur l'instrument : somme de toutes les contributions
    d'events actifs pondérées par leur courbe de dissipation.

    Retourne une décomposition par catégorie causale (Banque Centrale, Surprise Macro…)
    avec détail par event, ainsi que le NET en pips et la direction.
    """
    from services.database import get_conn

    try:
        ref_date = date_type.fromisoformat(at_date) if at_date else datetime.utcnow().date()
    except ValueError:
        ref_date = datetime.utcnow().date()

    # Cherche les analyses pour cet instrument dans les 180 jours précédents
    extended_from = ref_date - timedelta(days=180)
    inst_upper = instrument_id.upper()

    conn = get_conn()
    try:
        rows = conn.execute("""
            SELECT a.prediction_json,
                   e.start_date   AS event_date,
                   e.name         AS event_name,
                   e.sub_type     AS event_sub_type,
                   e.end_date     AS event_end_date,
                   t.name         AS template_name,
                   t.category     AS category,
                   t.calibration_json
            FROM   causal_event_analyses a
            JOIN   market_events          e ON e.id = a.market_event_id
            JOIN   causal_graph_templates t ON t.id = a.template_id
            WHERE  a.instrument = ?
              AND  e.start_date >= ?
              AND  e.start_date <= ?
            ORDER  BY e.start_date DESC
        """, (inst_upper, str(extended_from), str(ref_date))).fetchall()
    finally:
        conn.close()

    inst_lower = inst_upper.lower()
    by_category: Dict[str, Dict] = {}
    seen_events: set = set()  # évite les doublons (même event × multi-analyse)

    for row in rows:
        r = dict(row)
        event_key = (r["event_name"], r["event_date"])
        if event_key in seen_events:
            continue
        seen_events.add(event_key)

        try:
            predictions = json.loads(r["prediction_json"] or "{}")
            calib       = json.loads(r["calibration_json"] or "{}")
        except Exception:
            continue

        # Pips prédits pour cet instrument (cherche node_id == inst_lower ou contenant)
        pips_full: Optional[float] = None
        if inst_lower in predictions:
            pips_full = float(predictions[inst_lower])
        else:
            for k, v in predictions.items():
                if inst_lower in k.lower():
                    try:
                        pips_full = float(v)
                        break
                    except (TypeError, ValueError):
                        pass

        if pips_full is None or pips_full == 0:
            continue

        absorption_days = max(1, int(calib.get("absorption_days", 7)))
        decay_type      = str(calib.get("decay_type", "exp"))

        try:
            ev_date = date_type.fromisoformat(r["event_date"][:10])
        except ValueError:
            continue

        # Pour les guidance events : end_date = meeting date → absorption dynamique
        ev_end = r.get("event_end_date")
        if ev_end and r.get("event_sub_type", "").startswith("rate_guidance"):
            try:
                meeting = date_type.fromisoformat(ev_end[:10])
                absorption_days = max(1, (meeting - ev_date).days)
                decay_type = "linear"  # anticipation linéaire jusqu'à la réunion
            except ValueError:
                pass

        days_elapsed = (ref_date - ev_date).days
        df = _decay(days_elapsed, absorption_days, decay_type)
        if df < 0.01:
            continue

        current_pips = round(pips_full * df, 1)
        cat = r["category"]

        if cat not in by_category:
            by_category[cat] = {
                "label":         _CAT_LABELS.get(cat, cat),
                "pips":          0.0,
                "contributions": [],
            }

        by_category[cat]["pips"]    += current_pips
        by_category[cat]["contributions"].append({
            "event_name":      r["event_name"],
            "event_date":      r["event_date"][:10],
            "template_name":   r["template_name"],
            "pips_full":       round(pips_full, 1),
            "days_elapsed":    days_elapsed,
            "absorption_days": absorption_days,
            "decay_pct":       round(df * 100),
            "pips_current":    current_pips,
        })

    # Arrondi + tri par |pips| décroissant
    for v in by_category.values():
        v["pips"] = round(v["pips"], 1)
        v["contributions"].sort(key=lambda c: abs(c["pips_current"]), reverse=True)

    categories = sorted(by_category.values(), key=lambda x: abs(x["pips"]), reverse=True)
    net_pips = round(sum(v["pips"] for v in by_category.values()), 1)

    direction = "neutral"
    if net_pips > 5:
        direction = "bullish"
    elif net_pips < -5:
        direction = "bearish"

    return {
        "instrument": inst_upper,
        "at_date":    str(ref_date),
        "net_pips":   net_pips,
        "direction":  direction,
        "categories": categories,
        "n_events":   len(seen_events),
    }
