#!/usr/bin/env python3
# ruff: noqa: B007
"""Validate subprocess-per-day memory isolation (Issue #249).

Compares RSS growth between in-process and subprocess modes across
multiple iterations of the Arrow processing pipeline.

Usage:
    python scripts/validate_subprocess_isolation.py
    python scripts/validate_subprocess_isolation.py --iterations 10

Based on: scripts/profile_memory_baseline.py (Issue #248)
"""

from __future__ import annotations

import gc
import multiprocessing
import resource
import sys
import time
from pathlib import Path

FIXTURE_DIR = Path(__file__).parent.parent / "tests" / "fixtures"


def get_rss_mb() -> float:
    """Get current RSS (resident set size) in MB."""
    try:
        import psutil

        return psutil.Process().memory_info().rss / (1024**2)
    except ImportError:
        usage = resource.getrusage(resource.RUSAGE_SELF)
        if sys.platform == "darwin":
            return usage.ru_maxrss / (1024**2)  # macOS: bytes → MB
        return usage.ru_maxrss / 1024  # Linux: KB → MB


def _parse_csv_rows(zip_path: Path, limit: int | None = None) -> list[tuple]:
    """Parse CSV rows from ZIP as tuples."""
    import zipfile

    rows = []
    with zipfile.ZipFile(zip_path) as zf:
        csv_name = zf.namelist()[0]
        with zf.open(csv_name) as f:
            for line in f:
                parts = line.decode().strip().split(",")
                if len(parts) >= 7:
                    rows.append((
                        int(parts[0]),
                        float(parts[1]),
                        float(parts[2]),
                        int(parts[3]),
                        int(parts[4]),
                        int(parts[5]),
                        parts[6].strip().lower() == "true",
                    ))
                    if limit and len(rows) >= limit:
                        break
    return rows


def _process_arrow_iteration(raw_rows: list[tuple]) -> int:
    """Process trades through Arrow pipeline (production path). Returns bar count."""
    import polars as pl
    from opendeviationbar import OpenDeviationBarProcessor

    processor = OpenDeviationBarProcessor(threshold_decimal_bps=250)
    total_bars = 0
    chunk_size = 100_000

    for i in range(0, len(raw_rows), chunk_size):
        chunk = raw_rows[i : i + chunk_size]
        df = pl.DataFrame({
            "agg_trade_id": [r[0] for r in chunk],
            "price": [r[1] for r in chunk],
            "quantity": [r[2] for r in chunk],
            "first_trade_id": [r[3] for r in chunk],
            "last_trade_id": [r[4] for r in chunk],
            "timestamp": [r[5] for r in chunk],
            "is_buyer_maker": [r[6] for r in chunk],
        })
        arrow_batch = df.to_arrow()
        bars_arrow = processor.process_trades_arrow(arrow_batch)
        bars_df = pl.from_arrow(bars_arrow)
        total_bars += len(bars_df)
        del df, arrow_batch, bars_arrow, bars_df

    return total_bars


def _subprocess_target(
    result_queue: multiprocessing.Queue,
    raw_rows: list[tuple],
) -> None:
    """Target for subprocess isolation — all memory reclaimed on exit."""
    bars = _process_arrow_iteration(raw_rows)
    result_queue.put(bars)


def run_in_process(raw_rows: list[tuple], iterations: int) -> list[float]:
    """Process N iterations in-process, return RSS after each."""
    rss_readings = []
    for i in range(iterations):
        _process_arrow_iteration(raw_rows)
        gc.collect()
        rss_readings.append(get_rss_mb())
    return rss_readings


def run_subprocess(raw_rows: list[tuple], iterations: int) -> list[float]:
    """Process N iterations via subprocess, return parent RSS after each."""
    rss_readings = []
    for i in range(iterations):
        q: multiprocessing.Queue = multiprocessing.Queue()
        p = multiprocessing.Process(target=_subprocess_target, args=(q, raw_rows))
        p.start()
        p.join(timeout=120)
        if p.is_alive():
            p.terminate()
            p.join(timeout=5)
        gc.collect()
        rss_readings.append(get_rss_mb())
    return rss_readings


def main() -> int:
    import argparse

    parser = argparse.ArgumentParser(description="Subprocess isolation validation")
    parser.add_argument("--iterations", type=int, default=5, help="Number of iterations")
    parser.add_argument("--symbol", default="BTCUSDT", help="Symbol fixture to use")
    args = parser.parse_args()

    print("=" * 70)
    print("Subprocess-per-Day Memory Isolation Validation (Issue #249)")
    print("=" * 70)

    try:
        from opendeviationbar import __version__

        print(f"Version: {__version__}")
    except ImportError as e:
        print(f"ERROR: {e}")
        return 1

    zip_path = FIXTURE_DIR / f"{args.symbol}-aggTrades-2024-01-01.zip"
    if not zip_path.exists():
        print(f"ERROR: Fixture not found: {zip_path}")
        return 1

    print(f"Fixture: {zip_path.name}")
    print(f"Iterations: {args.iterations}")
    print(f"Initial RSS: {get_rss_mb():.1f} MB")
    print()

    # Pre-parse trades (shared by both modes)
    print("Parsing fixture trades...")
    raw_rows = _parse_csv_rows(zip_path)
    print(f"Parsed {len(raw_rows):,} trades")
    baseline_rss = get_rss_mb()
    print(f"Baseline RSS (after parse): {baseline_rss:.1f} MB")
    print()

    # Mode 1: In-process
    print("-" * 70)
    print("Mode 1: IN-PROCESS (current behavior)")
    print("-" * 70)
    start = time.time()
    in_process_rss = run_in_process(raw_rows, args.iterations)
    in_process_time = time.time() - start
    for i, rss in enumerate(in_process_rss):
        delta = rss - baseline_rss
        print(f"  Iteration {i + 1}: RSS = {rss:.1f} MB  (delta: +{delta:.1f} MB)")
    print(f"  Time: {in_process_time:.2f}s")
    print()

    # Reset for fair comparison
    gc.collect()
    baseline_rss_2 = get_rss_mb()

    # Mode 2: Subprocess isolation
    print("-" * 70)
    print("Mode 2: SUBPROCESS ISOLATION (Chrome model)")
    print("-" * 70)
    start = time.time()
    subprocess_rss = run_subprocess(raw_rows, args.iterations)
    subprocess_time = time.time() - start
    for i, rss in enumerate(subprocess_rss):
        delta = rss - baseline_rss_2
        print(f"  Iteration {i + 1}: RSS = {rss:.1f} MB  (delta: +{delta:.1f} MB)")
    print(f"  Time: {subprocess_time:.2f}s")
    print()

    # Comparison table
    print("=" * 70)
    print("COMPARISON TABLE")
    print("=" * 70)
    print(f"{'Iter':<6} {'In-Process (MB)':<18} {'Subprocess (MB)':<18} {'Saved (MB)':<12}")
    print("-" * 54)
    for i in range(args.iterations):
        ip_rss = in_process_rss[i]
        sp_rss = subprocess_rss[i]
        saved = ip_rss - sp_rss
        print(f"{i + 1:<6} {ip_rss:<18.1f} {sp_rss:<18.1f} {saved:<12.1f}")

    # Summary
    ip_growth = in_process_rss[-1] - in_process_rss[0]
    sp_growth = subprocess_rss[-1] - subprocess_rss[0]
    print()
    print(f"  In-process RSS growth: {ip_growth:+.1f} MB across {args.iterations} iterations")
    print(f"  Subprocess RSS growth: {sp_growth:+.1f} MB across {args.iterations} iterations")
    print(f"  Fragmentation avoided: {ip_growth - sp_growth:.1f} MB")
    print(f"  Overhead: {subprocess_time - in_process_time:+.2f}s ({(subprocess_time / in_process_time - 1) * 100:+.1f}%)")
    print()

    if sp_growth < ip_growth:
        print("  RESULT: Subprocess isolation reduces RSS growth")
    else:
        print("  RESULT: No measurable fragmentation in this test")
        print("  (fragmentation accumulates over hundreds of iterations, not just 5)")

    return 0


if __name__ == "__main__":
    sys.exit(main())
