"""Assembles a synthetic SnapshotStore from a ScenarioConfig.

Orchestrates the full synthetic data generation pipeline:
  1. Trading dates (real NYSE calendar)
  2. Market ETF price paths (SPY, QQQ)
  3. Individual stock price paths (correlated with market)
  4. Macro indicators (VIX, HY spread, SPY/QQQ rolling stats)
  5. Event candidates (parameterized feature distributions)
  6. Event-price coupling (signal-to-noise injection)
  7. SnapshotStore assembly
"""
from __future__ import annotations

import datetime as dt
from typing import Any

import numpy as np

from libs.backtest.scenarios.coupling import couple_events_to_prices
from libs.backtest.scenarios.event_gen import generate_events
from libs.backtest.scenarios.macro_gen import generate_macro_data
from libs.backtest.scenarios.price_gen import generate_market_etf_paths, generate_price_paths
from libs.backtest.scenarios.scenarios import ScenarioConfig
from libs.backtest.snapshot_store import SnapshotStore


# Starting dates for synthetic scenarios (real NYSE calendar)
# Using a date range that's safely after 2020 (no pandemic-era data issues)
_SCENARIO_START_DATE = dt.date(2024, 1, 2)


def _get_trading_dates(n_days: int, start: dt.date = _SCENARIO_START_DATE) -> list[dt.date]:
    """Return n_days consecutive NYSE trading dates starting from start."""
    from libs.backtest.calendar import get_trading_days
    # Request a window of n_days * 1.5 calendar days to account for weekends/holidays
    end_estimate = start + dt.timedelta(days=int(n_days * 1.5) + 30)
    all_days = get_trading_days(start, end_estimate)
    return all_days[:n_days]


def _total_regime_days(scenario: ScenarioConfig) -> int:
    """Sum of all regime durations in the scenario."""
    return sum(r.duration_days for r in scenario.price_regimes)


def build_synthetic_store(
    scenario: ScenarioConfig,
    rng: np.random.Generator | None = None,
) -> SnapshotStore:
    """Assemble a complete synthetic SnapshotStore from a ScenarioConfig.

    The returned store has no connection to real market data. It can be
    passed directly to BacktestRunner without any Oracle API or database.

    Args:
        scenario: Fully specified synthetic market scenario.
        rng: NumPy random generator. If None, uses scenario.seed or random state.

    Returns:
        SnapshotStore ready for BacktestRunner.run().
    """
    if rng is None:
        seed = scenario.seed
        rng = np.random.default_rng(seed)

    # 1. Trading dates
    n_days = _total_regime_days(scenario) + 30  # extra buffer for parking tail
    trading_dates = _get_trading_dates(n_days)

    # 2. Market ETF paths (SPY, QQQ) + market log-returns for macro correlation
    spy_bars, qqq_bars, market_log_rets = generate_market_etf_paths(
        regimes=scenario.price_regimes,
        trading_dates=trading_dates,
        rng=rng,
    )

    # 3. Individual stock paths
    n_sym = scenario.n_symbols
    tickers = [f"SYN{i:03d}" for i in range(n_sym)]

    bars_by_symbol = generate_price_paths(
        n_symbols=n_sym,
        initial_prices=None,
        regimes=scenario.price_regimes,
        trading_dates=trading_dates,
        market_beta_range=(0.5, 1.4),
        rng=rng,
        tickers=tickers,
    )
    assert isinstance(bars_by_symbol, dict), "generate_price_paths must return dict"

    # Merge ETF bars in as well (for parking lookups)
    bars_by_symbol["SPY"] = spy_bars
    bars_by_symbol["QQQ"] = qqq_bars

    # 4. Macro data
    macro_by_date = generate_macro_data(
        spy_bars=spy_bars,
        qqq_bars=qqq_bars,
        vix_config=scenario.vix_config,
        hy_config=scenario.hy_config,
        trading_dates=trading_dates,
        rng=rng,
        market_log_rets=market_log_rets,
    )

    # 5. Event candidates
    candidates_by_exec_date = generate_events(
        trading_dates=trading_dates,
        symbols=tickers,
        dist=scenario.event_distribution,
        rng=rng,
        bars_by_symbol=bars_by_symbol,
    )

    # 5b. Inject macro_vix / macro_hy_spread into each candidate row.
    # selector._row_matches_strategy_engine_filters() reads macro_vix and
    # macro_hy_spread directly from the row dict (not from macro_by_date),
    # so we must populate them here.
    for exec_date, rows in candidates_by_exec_date.items():
        macro = macro_by_date.get(exec_date, {})
        mv = macro.get("macro_vix")
        hy = macro.get("macro_hy_spread")
        for row in rows:
            if mv is not None:
                row["macro_vix"] = mv
            if hy is not None:
                row["macro_hy_spread"] = hy

    # 6. Event-price coupling (signal injection)
    couple_events_to_prices(
        candidates=candidates_by_exec_date,
        bars_by_symbol=bars_by_symbol,
        trading_dates=trading_dates,
        signal_strength=scenario.signal_strength,
        signal_decay_days=scenario.signal_decay_days,
        false_positive_rate=scenario.false_positive_rate,
        rng=rng,
    )

    # 7. Assemble SnapshotStore
    return SnapshotStore(
        candidates_by_exec_date=candidates_by_exec_date,
        bars_by_symbol_date=bars_by_symbol,
        macro_by_date=macro_by_date,
    )
