#!/usr/bin/env python3
"""Versioned multi-threshold cache population script.

Replaces ad-hoc ``/tmp/populate_range.py`` on bigblack.  Accepts a list of
thresholds and processes them with a single Parquet read per day (multi-threshold
fast path), reducing I/O by ~4x compared to running each threshold separately.

Usage
-----
Single threshold (backward-compatible):
    uv run --python 3.13 python scripts/populate_range.py \\
        --symbol BTCUSDT --start-date 2025-01-01 --end-date 2025-12-31 \\
        --threshold 250

Multiple thresholds (fast path, single Parquet read per day):
    uv run --python 3.13 python scripts/populate_range.py \\
        --symbol BTCUSDT --start-date 2025-01-01 --end-date 2025-12-31 \\
        --thresholds 250 500 750 1000

With pueue parallelism:
    uv run --python 3.13 python scripts/populate_range.py \\
        --symbol BTCUSDT --start-date 2019-01-01 --end-date 2025-12-31 \\
        --thresholds 250 500 750 1000 \\
        --parallel 8 --pueue-group parquet-process
"""

from __future__ import annotations

import argparse
import logging
import sys
from datetime import UTC, datetime

logger = logging.getLogger(__name__)

_MAX_DAYS = 366  # SSoT-OK: intentional safety guard (matches populate_full_cache.py)


def _setup_logging() -> None:
    from opendeviationbar.logging import setup_service_logging
    setup_service_logging("populate-range")


def _enqueue_pueue(
    cmd: list[str],
    group: str,
    label: str,
) -> None:
    """Enqueue a command in a pueue group (best-effort, non-fatal)."""
    import subprocess
    try:
        subprocess.run(
            ["pueue", "add", "--group", group, "--label", label, "--", *cmd],
            check=True,
        )
        logger.info("Enqueued pueue job [%s]: %s", group, label)
    except (FileNotFoundError, subprocess.CalledProcessError):
        logger.exception("pueue not available or add failed")
        sys.exit(1)


def populate_range(
    symbol: str,
    start_date: str,
    end_date: str,
    thresholds: list[int],
    *,
    force_refresh: bool = False,
    include_microstructure: bool = True,
    ouroboros_mode: str | None = None,
    max_days: int = _MAX_DAYS,
    market: str | None = None,
    notify: bool = True,
) -> dict[int, int] | int:
    """Run populate for a date range with one or more thresholds.

    Uses the multi-threshold fast path when multiple thresholds are given.

    Parameters
    ----------
    symbol : str
        Trading symbol (e.g., "BTCUSDT").
    start_date : str
        Start date (YYYY-MM-DD).
    end_date : str
        End date (YYYY-MM-DD).
    thresholds : list[int]
        One or more threshold values in dbps.
    force_refresh : bool
        Wipe existing cache and checkpoints.
    include_microstructure : bool
        Include microstructure features.
    ouroboros_mode : str | None
        Ouroboros reset mode (default: from env).
    max_days : int
        Maximum date range span in days.
    market : str | None
        Market type override.
    notify : bool
        Send Telegram notifications.

    Returns
    -------
    dict[int, int] | int
        If multiple thresholds: dict of threshold → bars written.
        If single threshold: total bars written (int).
    """
    from opendeviationbar import (
        populate_cache_multi_threshold,
        populate_cache_resumable,
    )

    start_time = datetime.now(tz=UTC)
    logger.info(
        "populate_range: %s thresholds=%s from %s to %s",
        symbol,
        thresholds,
        start_date,
        end_date,
    )

    if len(thresholds) > 1:
        result = populate_cache_multi_threshold(
            symbol,
            start_date,
            end_date,
            thresholds,
            force_refresh=force_refresh,
            include_microstructure=include_microstructure,
            ouroboros_mode=ouroboros_mode,
            max_days=max_days,
            market=market,
            notify=notify,
        )
        total = sum(result.values())
        elapsed = (datetime.now(tz=UTC) - start_time).total_seconds() / 60
        logger.info(
            "Completed %s multi-threshold: %d total bars in %.1f min — %s",
            symbol,
            total,
            elapsed,
            {t: f"{b:,}" for t, b in result.items()},
        )
        return result
    bars = populate_cache_resumable(
        symbol,
        start_date,
        end_date,
        threshold_decimal_bps=thresholds[0],
        force_refresh=force_refresh,
        include_microstructure=include_microstructure,
        ouroboros_mode=ouroboros_mode,
        max_days=max_days,
        market=market,
        notify=notify,
    )
    elapsed = (datetime.now(tz=UTC) - start_time).total_seconds() / 60
    logger.info(
        "Completed %s@%d: %d bars in %.1f min",
        symbol,
        thresholds[0],
        bars,
        elapsed,
    )
    return bars


def main() -> None:
    _setup_logging()

    parser = argparse.ArgumentParser(
        description="Multi-threshold cache population (single Parquet read per day)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Single threshold
  %(prog)s --symbol BTCUSDT --start-date 2025-01-01 --end-date 2025-12-31 --threshold 250

  # Multiple thresholds (fast path)
  %(prog)s --symbol BTCUSDT --start-date 2025-01-01 --end-date 2025-12-31 \\
           --thresholds 250 500 750 1000

  # Enqueue in pueue
  %(prog)s --symbol BTCUSDT --start-date 2019-01-01 --end-date 2025-12-31 \\
           --thresholds 250 500 750 1000 --pueue-group parquet-process

For full historical backfill spanning > 366 days use: mise run kintsugi:catchup
        """,
    )
    parser.add_argument("--symbol", required=True, type=str, help="Trading symbol")
    parser.add_argument(
        "--start-date", required=True, type=str, help="Start date (YYYY-MM-DD)"
    )
    parser.add_argument(
        "--end-date", required=True, type=str, help="End date (YYYY-MM-DD)"
    )

    # Threshold arguments (mutually exclusive)
    threshold_group = parser.add_mutually_exclusive_group(required=True)
    threshold_group.add_argument(
        "--threshold",
        type=int,
        help="Single threshold in dbps (e.g. 250)",
    )
    threshold_group.add_argument(
        "--thresholds",
        type=int,
        nargs="+",
        metavar="THRESHOLD",
        help="One or more thresholds in dbps (e.g. 250 500 750 1000)",
    )

    parser.add_argument(
        "--force-refresh",
        action="store_true",
        help="Wipe existing cache and checkpoints, start fresh",
    )
    parser.add_argument(
        "--no-microstructure",
        action="store_true",
        help="Disable microstructure features (default: enabled)",
    )
    parser.add_argument(
        "--ouroboros",
        type=str,
        default=None,
        choices=["day", "aion"],
        help="Ouroboros reset period (default: auto-detect from per-symbol registry)",
    )
    parser.add_argument(
        "--max-days",
        type=int,
        default=_MAX_DAYS,
        help=f"Maximum date range span in days (default: {_MAX_DAYS})",
    )
    parser.add_argument(
        "--market",
        type=str,
        default=None,
        choices=["spot", "futures-um", "futures-cm", "um", "cm"],
        help="Market type override (default: auto-resolve from symbols.toml)",
    )
    parser.add_argument(
        "--no-notify",
        action="store_true",
        help="Suppress Telegram notifications",
    )
    parser.add_argument(
        "--parallel",
        type=int,
        default=1,
        help="Number of parallel pueue jobs to enqueue (default: 1; only with --pueue-group)",
    )
    parser.add_argument(
        "--pueue-group",
        type=str,
        default=None,
        help=(
            "If set, enqueue the job(s) in this pueue group instead of running directly. "
            "Combined with --parallel to fan out by year."
        ),
    )

    args = parser.parse_args()

    thresholds: list[int] = [args.threshold] if args.threshold else args.thresholds
    include_microstructure = not args.no_microstructure
    notify = not args.no_notify

    # --- pueue mode: enqueue per-year jobs -----------------------------------
    if args.pueue_group:
        from datetime import timezone as _tz
        _parsed_start = datetime.strptime(args.start_date, "%Y-%m-%d").replace(tzinfo=_tz.utc)
        _parsed_end = datetime.strptime(args.end_date, "%Y-%m-%d").replace(tzinfo=_tz.utc)

        # Expand to all years in range
        all_years = list(range(_parsed_start.year, _parsed_end.year + 1))

        python_exe = sys.executable

        for year in all_years:
            year_start = max(args.start_date, f"{year}-01-01")
            year_end = min(args.end_date, f"{year}-12-31")
            label = f"{args.symbol}-{year}-{'_'.join(str(t) for t in thresholds)}"
            cmd = [
                python_exe,
                __file__,
                "--symbol", args.symbol,
                "--start-date", year_start,
                "--end-date", year_end,
                "--thresholds", *[str(t) for t in thresholds],
                "--max-days", str(args.max_days),
            ]
            if args.force_refresh:
                cmd.append("--force-refresh")
            if not include_microstructure:
                cmd.append("--no-microstructure")
            if args.ouroboros:
                cmd.extend(["--ouroboros", args.ouroboros])
            if args.market:
                cmd.extend(["--market", args.market])
            if not notify:
                cmd.append("--no-notify")

            _enqueue_pueue(cmd, group=args.pueue_group, label=label)
        return

    # --- Direct execution ----------------------------------------------------
    # Auto-detect ouroboros mode from per-symbol registry when not specified
    ouroboros = args.ouroboros
    if ouroboros is None:
        try:
            from opendeviationbar.symbol_registry import get_symbol_entries

            entry = get_symbol_entries().get(args.symbol)
            if entry is not None:
                ouroboros = entry.ouroboros_mode.value
        except (ImportError, ValueError, OSError):
            pass  # Falls through to populate_cache_resumable's own default

    try:
        result = populate_range(
            args.symbol,
            args.start_date,
            args.end_date,
            thresholds,
            force_refresh=args.force_refresh,
            include_microstructure=include_microstructure,
            ouroboros_mode=ouroboros,
            max_days=args.max_days,
            market=args.market,
            notify=notify,
        )
        if isinstance(result, dict):
            logger.info(
                "Done: %s total bars — %s",
                sum(result.values()),
                {t: f"{b:,}" for t, b in result.items()},
            )
        else:
            logger.info("Done: %s bars", result)
    except Exception:
        logger.exception("Population failed")
        sys.exit(1)


if __name__ == "__main__":
    main()
