"""
Wavelet decomposition endpoints — adapted from InstrumentSimulator's /api/wavelet/analyze
and /api/wavelet/rolling. That project fetches history from a Postgres-cached date-range
store; we don't have that, so history comes from services.data_fetcher.get_historical()
(yfinance period/interval strings by default, or explicit start/end dates when a custom
range is requested). The wavelet math itself (services.wavelet_engine) is an unmodified
port.
"""
from datetime import date, datetime, timedelta
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel

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

# Approximate calendar-day span of each yfinance period string, used to pick a fetch
# window large enough to cover the requested causal (rolling) output range plus the
# lookback padding needed to fill the first walk-forward window.
_PERIOD_TO_DAYS = {
    "5d": 5, "1mo": 30, "3mo": 90, "6mo": 182,
    "1y": 365, "2y": 730, "5y": 1825, "10y": 3650, "max": 3650,
}


def _fetch_history(symbol: str, period: str, interval: str = "1d", start: Optional[str] = None, end: Optional[str] = None, saxo_symbol: Optional[str] = None):
    """Saxo-first when saxo_symbol is given (resolved via the watchlist/Instrument Analysis
    Saxo link), yfinance otherwise or as a silent fallback on any Saxo failure. Wavelets only
    ever consume the close series, so unlike instrument_service's snapshot fetch this doesn't
    need full OHLCV."""
    if saxo_symbol and start is None and end is None:
        # Saxo's Chart API is day-count-from-now only (no explicit date range), so a custom
        # start_date/end_date request can't be served from Saxo — falls through to yfinance.
        try:
            from services.database import get_saxo_catalog_by_symbol
            from services.saxo_client import get_price_history
            entry = get_saxo_catalog_by_symbol(saxo_symbol)
            asset_type = entry["asset_type"] if entry else "FxSpot"
            days = _PERIOD_TO_DAYS.get(period, 365)
            bars = get_price_history(saxo_symbol, asset_type, days=days)
            return [b["close"] for b in bars], [b["date"] for b in bars]
        except Exception as e:
            import logging
            logging.getLogger(__name__).warning(f"[wavelet] Saxo fetch failed for '{saxo_symbol}', falling back to yfinance: {e}")

    from services.data_fetcher import get_historical
    hist = get_historical(symbol, period=period, interval=interval, start=start, end=end)
    values = [h["close"] for h in hist]
    dates = [h["date"] for h in hist]
    return values, dates


def _fetch_padded_history(
    symbol: str, lookback: int, period: str = "1y",
    start_date: Optional[str] = None, end_date: Optional[str] = None,
    future_padding_days: int = 0, saxo_symbol: Optional[str] = None,
):
    """Fetch enough history to cover the requested causal-output range plus a
    `lookback`-sized warmup window before it (mirrors main.py's fetch_start padding).

    Returns (values, dates, cutoff) — `cutoff` is the first date that belongs in the
    causal OUTPUT range; everything in `dates`/`values` before it is lookback padding.

    Two modes:
    - `period` (default): output range = the last `period` worth of days up to today,
      cutoff computed backward from now. The fetch already reaches "today" regardless
      of `future_padding_days` (yfinance can't return data past now), so that argument
      is a no-op here — the reliability endpoint's per-turning-point "not enough real
      future data yet" guard is what actually limits which recent points are testable.
    - `start_date`/`end_date`: output range = that explicit window (e.g. a custom
      backtest over 2013-2019) — fetches from `start_date` minus the lookback padding
      through `end_date` (or through today if `end_date` is omitted). Unlike `period`
      mode, `end_date` is an arbitrary cutoff with real data available past it, so
      `future_padding_days` explicitly extends the fetch past it — needed for the
      reliability endpoint to have real hindsight data for turning points near the end
      of a custom range.
    """
    pad_days = int(lookback * 1.6) + 15
    if start_date:
        fetch_start = (date.fromisoformat(start_date) - timedelta(days=pad_days)).isoformat()
        fetch_end = (date.fromisoformat(end_date) + timedelta(days=future_padding_days)).isoformat() if end_date and future_padding_days else end_date
        values, dates = _fetch_history(symbol, period, start=fetch_start, end=fetch_end, saxo_symbol=saxo_symbol)
        return values, dates, start_date

    out_days = _PERIOD_TO_DAYS.get(period, 365)
    total_days = out_days + pad_days
    fetch_period = next(
        (p for p, d in sorted(_PERIOD_TO_DAYS.items(), key=lambda kv: kv[1]) if d >= total_days),
        "10y",
    )
    values, dates = _fetch_history(symbol, fetch_period, saxo_symbol=saxo_symbol)
    cutoff = (datetime.utcnow() - timedelta(days=out_days)).date().isoformat()
    return values, dates, cutoff


@router.get("/analyze")
def wavelet_analyze(
    symbol: str = Query("SPY"),
    period: str = Query("1y", description="yfinance period: 5d,1mo,3mo,6mo,1y,2y,5y,10y,max — ignored if start_date is set"),
    levels: int = Query(4, ge=2, le=6),
    wavelet: str = Query("gmw", description="gmw, morlet or bump"),
    window_size: int = Query(0, description="0 = single window over the whole range"),
    method: str = Query("cwt", description="cwt (default) or ssq"),
    start_date: Optional[str] = Query(None, description="ISO date (YYYY-MM-DD) — overrides `period` with an explicit custom range"),
    end_date: Optional[str] = Query(None, description="ISO date (YYYY-MM-DD), only used alongside start_date; omit for 'through today'"),
    saxo_symbol: Optional[str] = Query(None, description="Saxo quote symbol — Saxo-first, falls back to `symbol` (yfinance) on any failure"),
):
    from services.wavelet_engine import windowed_band_decompose, band_decompose_ssq

    values, dates = _fetch_history(symbol, period, start=start_date, end=end_date, saxo_symbol=saxo_symbol)
    if len(values) < 32:
        raise HTTPException(400, "Historique insuffisant pour une analyse ondelette (32 points minimum).")

    try:
        if method == "ssq":
            # No windowed/chunked variant for ssq in the source project — analyze the
            # whole fetched range in one pass (matches window_size=0 semantics).
            return band_decompose_ssq(values, dates, num_levels=levels, wavelet=wavelet)
        return windowed_band_decompose(values, dates, window_size=window_size, num_levels=levels, wavelet=wavelet)
    except ValueError as exc:
        raise HTTPException(400, str(exc)) from exc


@router.get("/rolling")
def wavelet_rolling(
    symbol: str = Query("SPY"),
    period: str = Query("1y", description="how much of the causal output range to return — ignored if start_date is set"),
    lookback: int = Query(260, ge=32),
    levels: int = Query(4, ge=2, le=6),
    wavelet: str = Query("gmw"),
    step: int = Query(1, ge=1),
    method: str = Query("cwt", description="cwt (default) or ssq"),
    start_date: Optional[str] = Query(None, description="ISO date (YYYY-MM-DD) — overrides `period`; the causal output starts here"),
    end_date: Optional[str] = Query(None, description="ISO date (YYYY-MM-DD), only used alongside start_date; omit for 'through today'"),
    saxo_symbol: Optional[str] = Query(None, description="Saxo quote symbol — Saxo-first, falls back to `symbol` (yfinance) on any failure"),
):
    """Walk-forward version of /analyze: band values are computed day by day from a
    trailing `lookback`-point window only, so a trade simulation built on this never
    sees data past its own decision date."""
    from services.wavelet_engine import rolling_causal_bands, rolling_causal_bands_ssq

    values, dates, cutoff = _fetch_padded_history(symbol, lookback, period, start_date, end_date, saxo_symbol=saxo_symbol)
    if len(values) < lookback + 32:
        raise HTTPException(400, "Historique insuffisant pour une analyse ondelette (32 points minimum).")

    start_idx = next((i for i, d in enumerate(dates) if d[:10] >= cutoff), None)
    if start_idx is None:
        raise HTTPException(400, "Pas de donnees dans la plage de trading demandee.")

    decomposer = rolling_causal_bands_ssq if method == "ssq" else rolling_causal_bands
    try:
        result = decomposer(
            values, dates,
            start_idx=start_idx, lookback=lookback,
            num_levels=levels, wavelet=wavelet, step=step,
        )
    except ValueError as exc:
        raise HTTPException(400, str(exc)) from exc
    if not result["dates"]:
        raise HTTPException(
            400,
            f"Pas assez d'historique avant {cutoff} pour remplir une fenetre glissante de {lookback} points.",
        )
    return result


@router.get("/reliability")
def wavelet_reliability_endpoint(
    symbol: str = Query("SPY"),
    period: str = Query("1y", description="how much of the causal output range to scan for turning points — ignored if start_date is set"),
    lookback: int = Query(260, ge=32),
    levels: int = Query(4, ge=2, le=6),
    wavelet: str = Query("gmw"),
    step: int = Query(1, ge=1),
    method: str = Query("cwt", description="cwt (default) or ssq"),
    smooth_days: int = Query(3, ge=1, le=10, description="lag used to smooth the slope before flagging a sign-change as a turning point"),
    tolerance_pct: float = Query(0.10, ge=0, le=0.5, description="date-matching tolerance as a fraction of each band's own average cycle length, applied both sides"),
    min_confirm_horizon: int = Query(3, ge=1, le=30, description="floor on the hindsight horizon in days, in case a band's measured cycle length comes out very short"),
    max_future_padding: int = Query(60, ge=10, le=180, description="extra real days fetched beyond the requested range, to cover the slowest band's own (data-driven) confirm horizon"),
    start_date: Optional[str] = Query(None, description="ISO date (YYYY-MM-DD) — overrides `period`; the causal output starts here"),
    end_date: Optional[str] = Query(None, description="ISO date (YYYY-MM-DD), only used alongside start_date; omit for 'through today'"),
    saxo_symbol: Optional[str] = Query(None, description="Saxo quote symbol — Saxo-first, falls back to `symbol` (yfinance) on any failure"),
):
    """For every reversal a live (causal, walk-forward) decomposition would have flagged,
    checks whether redoing the decomposition later still shows the same reversal — a
    per-band reliability score. The hindsight horizon is dynamic per band (each band's own
    historical average peak-to-peak cycle length, not one fixed day count for every band —
    a fixed horizon is meaningless across bands with wildly different natural periods)."""
    from services.wavelet_engine import wavelet_reliability

    # The horizon is now computed per band inside wavelet_reliability (from each band's own
    # measured cycle length), so we don't know it in advance here — pad generously enough to
    # cover even a slow band's cycle instead.
    values, dates, cutoff = _fetch_padded_history(symbol, lookback, period, start_date, end_date, future_padding_days=max_future_padding, saxo_symbol=saxo_symbol)
    if len(values) < lookback + max_future_padding + 32:
        raise HTTPException(400, "Historique insuffisant pour un test de fiabilité (32 points minimum au-delà de la fenêtre + marge).")

    start_idx = next((i for i, d in enumerate(dates) if d[:10] >= cutoff), None)
    if start_idx is None:
        raise HTTPException(400, "Pas de donnees dans la plage de trading demandee.")

    try:
        result = wavelet_reliability(
            values, dates,
            start_idx=start_idx, lookback=lookback,
            num_levels=levels, wavelet=wavelet, method=method, step=step,
            smooth_days=smooth_days, tolerance_pct=tolerance_pct, min_confirm_horizon=min_confirm_horizon,
        )
    except ValueError as exc:
        raise HTTPException(400, str(exc)) from exc
    return result


# ── Watchlist refresh scheduler — Saxo-priced quotes + wavelet recompute, own cadence ──
# ── (services/wavelet_scheduler.py), independent of the once-a-day auto_cycle ─────────

class RefreshSettingsRequest(BaseModel):
    enabled: bool
    refresh_minutes: float


@router.get("/refresh-settings")
def get_refresh_settings():
    from services.wavelet_scheduler import get_settings
    return get_settings()


@router.put("/refresh-settings")
def update_refresh_settings(req: RefreshSettingsRequest):
    from services.wavelet_scheduler import set_settings, get_settings
    set_settings(req.enabled, req.refresh_minutes)
    return get_settings()


@router.post("/refresh-now")
def refresh_now():
    """Manual immediate refresh of the whole Watchlist (Saxo-priced quotes + wavelet
    recompute) — doesn't wait for the periodic poll."""
    from services.wavelet_scheduler import run_refresh_pass
    return {"signal_rows": run_refresh_pass()}


# ── Saved simulation/optimization runs ────────────────────────────────────────

class SimulationCreate(BaseModel):
    name: str
    form: Dict[str, Any] = {}
    results: List[Dict[str, Any]] = []
    excluded_instruments: List[str] = []


class SimulationUpdate(BaseModel):
    name: Optional[str] = None
    form: Optional[Dict[str, Any]] = None
    results: Optional[List[Dict[str, Any]]] = None
    append_results: Optional[List[Dict[str, Any]]] = None
    excluded_instruments: Optional[List[str]] = None


@router.get("/simulations")
def list_simulations():
    from services.database import get_wavelet_simulations
    return get_wavelet_simulations()


@router.post("/simulations")
def create_simulation(payload: SimulationCreate):
    from services.database import save_wavelet_simulation
    return save_wavelet_simulation(payload.name, payload.form, payload.results, payload.excluded_instruments)


@router.get("/simulations/{sim_id}")
def get_simulation(sim_id: str):
    from services.database import get_wavelet_simulation
    sim = get_wavelet_simulation(sim_id)
    if not sim:
        raise HTTPException(404, "Simulation introuvable.")
    return sim


@router.patch("/simulations/{sim_id}")
def patch_simulation(sim_id: str, payload: SimulationUpdate):
    from services.database import update_wavelet_simulation
    sim = update_wavelet_simulation(
        sim_id, name=payload.name, form=payload.form, results=payload.results,
        append_results=payload.append_results, excluded_instruments=payload.excluded_instruments,
    )
    if not sim:
        raise HTTPException(404, "Simulation introuvable.")
    return sim


@router.delete("/simulations/{sim_id}")
def remove_simulation(sim_id: str):
    from services.database import delete_wavelet_simulation
    if not delete_wavelet_simulation(sim_id):
        raise HTTPException(404, "Simulation introuvable.")
    return {"deleted": sim_id}


# ── Watchlist signal scan (populated by the auto-cycle, see services.wavelet_signals) ──

@router.get("/watchlist-signals")
def watchlist_signals():
    from services.database import get_latest_wavelet_state_by_instrument
    return {"signals": get_latest_wavelet_state_by_instrument()}
