# Issue #318: Bulk Parquet population for clean-slate sidecar startup
"""Populate ClickHouse from local Parquet tick cache on empty table.

When the sidecar detects an empty ClickHouse table (0 bars for a symbol),
this module runs repair_direct_parquet.py as a subprocess with 24 parallel
workers. This is 10x faster than inline sequential processing (~3,300
days/min vs ~580 days/min) because it parallelizes at the symbol level.

The subprocess saves processor checkpoints, which the sidecar loads on
return to fill only the last few minutes via fill_from_rest.

Design:
- Delegates to repair_direct_parquet.py --all --execute --workers 24
- 24-worker parallel symbol processing (Aion-safe: sequential per symbol)
- Saves processor checkpoints for seamless sidecar handoff
- Respects symbols.toml as SSoT for symbol + threshold selection
"""

from __future__ import annotations

import logging
import os
import subprocess
import sys
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache

logger = logging.getLogger("opendeviationbar.sidecar")


def _bulk_populate_from_parquet(
    ch_cache: OpenDeviationBarCache,
    symbols: list[str],
    thresholds: list[int],  # noqa: ARG001 — kept for API compat; repair uses registry INLINE-IGNORE-OK
) -> dict[str, int]:
    """Populate ClickHouse for symbols with 0 bars from local Parquet ticks.

    Delegates to repair_direct_parquet.py with 24 parallel workers for
    maximum throughput (~3,300 days/min on bigblack). Returns dict of
    {symbol: bars_written} for symbols that were populated.
    """
    # Find symbols with 0 bars in ClickHouse
    empty_symbols: list[str] = []
    for sym in symbols:
        try:
            result = ch_cache.client.query(
                "SELECT count() FROM opendeviationbar_cache.open_deviation_bars FINAL"
                f" WHERE symbol = '{sym}' AND first_agg_trade_id > 0"
            )
            count = int(result.result_rows[0][0]) if result.result_rows else 0
            if count == 0:
                empty_symbols.append(sym)
        except (OSError, RuntimeError, ConnectionError):
            logger.warning("bulk-populate: count query failed for %s", sym, exc_info=True)

    if not empty_symbols:
        logger.info("bulk-populate: all %d symbols have existing bars — skipping", len(symbols))
        return {}

    logger.info(
        "bulk-populate: %d/%d symbols empty — launching repair_direct_parquet.py (24 workers)",
        len(empty_symbols), len(symbols),
    )

    # Find the repair script
    script_path = _find_repair_script()
    if script_path is None:
        logger.warning("bulk-populate: repair_direct_parquet.py not found — skipping")
        return {}

    # Run repair_direct_parquet.py as subprocess with 24 workers
    python = sys.executable
    workers = min(os.cpu_count() or 8, 24)
    cmd = [python, script_path, "--all", "--execute", f"--workers={workers}"]

    logger.info("bulk-populate: running %s", " ".join(cmd))

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=3600,  # 1 hour max
            check=False,  # handled below — non-zero exit logged with stderr
        )

        if result.returncode == 0:
            logger.info("bulk-populate: repair_direct_parquet.py completed successfully")
        else:
            logger.warning(
                "bulk-populate: repair_direct_parquet.py failed (exit=%d)\nstderr: %s",
                result.returncode, result.stderr[-500:] if result.stderr else "",
            )

        # Log last few lines of stdout for visibility
        if result.stdout:
            for line in result.stdout.strip().split("\n")[-10:]:
                logger.info("bulk-populate: %s", line)

    except subprocess.TimeoutExpired:
        logger.warning("bulk-populate: repair_direct_parquet.py timed out after 1 hour")
    except Exception:
        logger.warning("bulk-populate: subprocess failed", exc_info=True)

    # Query final bar counts per symbol
    results: dict[str, int] = {}
    for sym in empty_symbols:
        try:
            qr = ch_cache.client.query(
                "SELECT count() FROM opendeviationbar_cache.open_deviation_bars FINAL"
                f" WHERE symbol = '{sym}' AND first_agg_trade_id > 0"
            )
            count = int(qr.result_rows[0][0]) if qr.result_rows else 0
            if count > 0:
                results[sym] = count
        except (OSError, RuntimeError, ConnectionError):
            pass

    logger.info(
        "bulk-populate: completed — %d/%d symbols populated, %d total bars",
        len(results), len(empty_symbols), sum(results.values()),
    )
    return results


def _find_repair_script() -> str | None:
    """Find repair_direct_parquet.py relative to the package installation."""
    from pathlib import Path

    # Try relative to this file (development layout)
    pkg_dir = Path(__file__).resolve().parent.parent.parent.parent
    script = pkg_dir / "scripts" / "repair_direct_parquet.py"
    if script.exists():
        return str(script)

    # Try relative to cwd (deployed layout)
    cwd_script = Path.cwd() / "scripts" / "repair_direct_parquet.py"
    if cwd_script.exists():
        return str(cwd_script)

    # Try home directory (bigblack standard location)
    home_script = Path.home() / "opendeviationbar-py" / "scripts" / "repair_direct_parquet.py"
    if home_script.exists():
        return str(home_script)

    return None
