"""Compact chronological validation for the Regime Monitor warning score.

The study calls its outcome a 10% correction, uses the first 70% of sessions to
freeze an 80th-percentile warning threshold, and reports alarm episodes only on
the final 30%.  It is still labelled exploratory while the fixed breadth basket
is reconstructed before its freeze date.
"""

from __future__ import annotations

import json
import logging
from datetime import date, datetime, timedelta, timezone

from sqlalchemy.ext.asyncio import AsyncSession

from app.services import breadth_service, settings_store
from app.services import regime_monitor_service as rms
from app.services.admin_service import update_setting

logger = logging.getLogger(__name__)

KEY_REPORT = "regime_event_study"

EVENT_THRESHOLD_PCT = 10.0
EVENT_COOLDOWN_DAYS = 40
DRAWDOWN_LOOKBACK = 252
HORIZON_DAYS = 20
WARN_PERCENTILE = 80.0
TRAIN_FRACTION = 0.70


def _median(values: list[float]) -> float | None:
    if not values:
        return None
    ordered = sorted(values)
    middle = len(ordered) // 2
    return (
        float(ordered[middle])
        if len(ordered) % 2
        else (ordered[middle - 1] + ordered[middle]) / 2.0
    )


def _percentile(values: list[float], pct: float) -> float | None:
    ordered = sorted(v for v in values if v is not None)
    if not ordered:
        return None
    position = (len(ordered) - 1) * pct / 100.0
    lower = int(position)
    upper = min(lower + 1, len(ordered) - 1)
    return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)


def detect_events(
    closes: list[float],
    dates: list[date],
    threshold_pct: float = EVENT_THRESHOLD_PCT,
    lookback: int = DRAWDOWN_LOOKBACK,
    cooldown: int = EVENT_COOLDOWN_DAYS,
) -> list[dict]:
    """Rising-edge corrections from the trailing 52-week high."""
    events: list[dict] = []
    previous_drawdown = 0.0
    last_event = -10**9
    for index, close in enumerate(closes):
        high = max(closes[max(0, index - lookback + 1): index + 1])
        drawdown = (high - close) / high * 100.0 if high > 0 else 0.0
        if (
            drawdown >= threshold_pct
            and previous_drawdown < threshold_pct
            and index - last_event >= cooldown
        ):
            events.append({
                "date": dates[index].isoformat(),
                "index": index,
                "depth_pct": round(drawdown, 1),
            })
            last_event = index
        previous_drawdown = drawdown
    return events


def alarm_episodes(
    indicator: dict[date, float],
    dates: list[date],
    threshold: float,
    start_index: int = 1,
) -> list[int]:
    """Indices where the warning crosses upward; it must reset below first."""
    alarms: list[int] = []
    was_high = False
    if start_index > 0:
        previous = indicator.get(dates[start_index - 1])
        was_high = previous is not None and previous >= threshold
    for index in range(start_index, len(dates)):
        value = indicator.get(dates[index])
        if value is None:
            continue
        high = value >= threshold
        if high and not was_high:
            alarms.append(index)
        was_high = high
    return alarms


def evaluate_alarms(
    alarm_indices: list[int],
    event_indices: list[int],
    dates: list[date],
    horizon: int = HORIZON_DAYS,
) -> dict:
    """Event recall, episode false alarms, and lead time for one holdout."""
    leads: list[float] = []
    per_event: list[dict] = []
    warned = 0
    for event_index in event_indices:
        matching = [
            alarm for alarm in alarm_indices if 0 < event_index - alarm <= horizon
        ]
        lead = max((event_index - alarm for alarm in matching), default=None)
        if lead is not None:
            warned += 1
            leads.append(float(lead))
        per_event.append({
            "date": dates[event_index].isoformat(),
            "warned": lead is not None,
            "lead_days": lead,
        })

    false_alarms = sum(
        1
        for alarm in alarm_indices
        if not any(0 < event - alarm <= horizon for event in event_indices)
    )
    return {
        "events": len(event_indices),
        "events_warned": warned,
        "events_missed": len(event_indices) - warned,
        "alarm_episodes": len(alarm_indices),
        "false_alarms": false_alarms,
        "median_lead_days": _median(leads),
        "per_event": per_event,
    }


def _warning_series(
    prices: dict[str, rms.Series],
    breadth_divergence: dict[date, float],
    dates: list[date],
    config: dict,
) -> dict[date, float]:
    """Technical Warning score used historically (fundamentals have no PIT history)."""
    tickers = config["tickers"]
    smh_full = prices.get(tickers["leaders"][0], [])
    spy_full = prices.get(tickers["market"], [])
    out: dict[date, float] = {}
    for session in dates:
        divergence = breadth_divergence.get(session)
        relative = rms.p4_relative_strength(
            rms._closes_asof(smh_full, session),
            rms._closes_asof(spy_full, session),
        )
        values: list[tuple[float, float]] = []
        if divergence is not None:
            values.append((divergence, rms.WARNING_WEIGHTS["breadth_divergence"]))
        if relative is not None:
            values.append((relative, rms.WARNING_WEIGHTS["relative_strength"]))
        if values:
            out[session] = round(
                sum(value * weight for value, weight in values)
                / sum(weight for _, weight in values),
                2,
            )
    return out


async def run_event_study(
    db: AsyncSession,
    threshold_pct: float = EVENT_THRESHOLD_PCT,
    horizon: int = HORIZON_DAYS,
) -> dict:
    config = await rms.get_regime_config(db)
    end = date.today()
    start = end - timedelta(days=5 * 365 + 30)
    prices = await rms._fetch_prices(config, start, end)
    leader = config["tickers"]["leaders"][0]
    benchmark = sorted(prices.get(leader, []), key=lambda item: item[0])
    if len(benchmark) < 500:
        return {"available": False, "reason": "insufficient benchmark history"}

    dates = [d for d, _ in benchmark]
    closes = [value for _, value in benchmark]
    breadth, _ = await breadth_service.compute_breadth_details(
        db, config["breadth_basket"], window=200, min_tickers=20
    )
    divergence = breadth_service.compute_divergence_series(breadth, benchmark)
    warning = _warning_series(prices, divergence, dates, config)

    split = max(1, min(len(dates) - 1, int(len(dates) * TRAIN_FRACTION)))
    train_values = [warning[d] for d in dates[:split] if d in warning]
    warn_threshold = _percentile(train_values, WARN_PERCENTILE)
    if warn_threshold is None:
        return {"available": False, "reason": "insufficient warning history"}

    all_events = detect_events(closes, dates, threshold_pct)
    holdout_events = [event["index"] for event in all_events if event["index"] >= split]
    alarms = alarm_episodes(warning, dates, warn_threshold, start_index=split)
    metrics = evaluate_alarms(alarms, holdout_events, dates, horizon)
    holdout_sessions = max(1, len(dates) - split)
    metrics["false_alarms_per_year"] = round(
        metrics["false_alarms"] / (holdout_sessions / 252.0), 2
    )

    basket_asof = date.fromisoformat(config["basket_asof"])
    retrospective = dates[split] < basket_asof
    evaluation = "exploratory" if retrospective else "holdout"
    lead_text = (
        f"median lead {metrics['median_lead_days']:.0f} sessions"
        if metrics["median_lead_days"] is not None
        else "no successful warning lead"
    )
    summary = (
        f"{evaluation.capitalize()} chronological test: warning episodes preceded "
        f"{metrics['events_warned']}/{metrics['events']} 10% corrections; "
        f"{metrics['events_missed']} missed, {metrics['false_alarms_per_year']:.1f} "
        f"false alarms/year, {lead_text}."
    )
    per_event = metrics.pop("per_event")

    report = {
        "available": True,
        "methodology": rms.METHODOLOGY,
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "evaluation": evaluation,
        "summary": summary,
        "params": {
            "benchmark": leader,
            "outcome": "10% correction from trailing 52-week high",
            "event_threshold_pct": threshold_pct,
            "event_cooldown_days": EVENT_COOLDOWN_DAYS,
            "horizon_days": horizon,
            "train_fraction": TRAIN_FRACTION,
            "warn_percentile": WARN_PERCENTILE,
            "warn_threshold": round(warn_threshold, 1),
            "basket_hash": rms._basket_hash(config["breadth_basket"]),
            "basket_asof": config["basket_asof"],
        },
        "sample": {
            "start": dates[0].isoformat(),
            "end": dates[-1].isoformat(),
            "train_end": dates[split - 1].isoformat(),
            "test_start": dates[split].isoformat(),
            "sessions": len(dates),
            "holdout_sessions": holdout_sessions,
        },
        "metrics": metrics,
        "events": per_event,
        "recent_breadth": [
            {"date": d.isoformat(), "breadth": breadth[d], "warning": warning.get(d)}
            for d in dates[-90:]
            if d in breadth
        ],
    }
    logger.info(json.dumps({
        "event": "regime_event_study_complete",
        "evaluation": evaluation,
        "events": metrics["events"],
        "warned": metrics["events_warned"],
        "false_alarms_per_year": metrics["false_alarms_per_year"],
    }))
    return report


async def run_and_store(db: AsyncSession) -> dict:
    report = await run_event_study(db)
    await update_setting(db, KEY_REPORT, json.dumps(report))
    return report


async def get_event_study_report(db: AsyncSession) -> dict | None:
    setting = await settings_store.get_setting(db, KEY_REPORT)
    if setting is None:
        return None
    try:
        report = json.loads(setting.value)
    except (TypeError, ValueError):
        return None
    return report if report.get("methodology") == rms.METHODOLOGY else None
