"""
run_backtests.py
Drives the four BarTimingProbe backtests sequentially via MT5 headless mode.
Usage:
    python run_backtests.py             # runs all four models
    python run_backtests.py et op       # runs only every-tick and open-prices
"""

import subprocess
import time
import shutil
import sys
import re
from pathlib import Path

# ── Configuration ─────────────────────────────────────────────────────────────
TERMINAL_DATA = Path(
    r"C:\Users\Picasso\AppData\Roaming\MetaQuotes\Terminal"
    r"\D0E8209F77C8CF37AD8BF550E51FF075"
)
# FILE_COMMON in the EA writes here (persists across agent restarts)
MT5_FILES = Path(r"C:\Users\Picasso\AppData\Roaming\MetaQuotes\Terminal\Common\Files")
REPORTS_DIR = TERMINAL_DATA / "reports" / "bar_timing"

TERMINAL_CANDIDATES = [
    r"C:\Program Files\MetaTrader 5\terminal64.exe",
    r"C:\Program Files (x86)\MetaTrader 5\terminal64.exe",
    r"C:\MetaTrader 5\terminal64.exe",
]

INI_DIR     = Path(__file__).parent.parent / "ini"
RESULTS_DIR = Path(__file__).parent.parent / "results"
RESULTS_DIR.mkdir(exist_ok=True)
REPORTS_DIR.mkdir(parents=True, exist_ok=True)

MODELS = [
    # Model identity confirmed by tester-log tick counts (EURUSD M15, 2025-05-08 to 2026-05-08, 24751 bars):
    # Model=0 → 18.7M ticks (~757/bar)  → Every Tick (synthesized from M1 OHLC)
    # Model=4 → varies                  → Every Tick Based on Real Ticks (falls back to M=0 if unavailable)
    # Model=1 → 1.47M ticks (~60/bar)   → 1-Min OHLC  (15 M1 bars × 4 OHLC ticks per M15 bar)
    # Model=2 → 24,758 ticks (~1/bar)   → Open Prices Only (1 tick per bar)
    ("et", "probe_every_tick.ini",  "Every Tick (Model=0)"),
    ("rt", "probe_real_ticks.ini",  "Real Ticks (Model=4)"),
    ("m1", "probe_m1_ohlc.ini",     "1-Min OHLC (Model=1)"),
    ("op", "probe_open_prices.ini", "Open Prices Only (Model=2)"),
]

TIMEOUT_S = 7200  # 2 hours max per run
# ──────────────────────────────────────────────────────────────────────────────


def find_terminal() -> Path:
    for p in TERMINAL_CANDIDATES:
        if Path(p).exists():
            return Path(p)
    raise FileNotFoundError(
        "terminal64.exe not found. Update TERMINAL_CANDIDATES in run_backtests.py."
    )


def kill_mt5():
    """Rule 1: kill any running instance before launching."""
    subprocess.run(
        ["taskkill", "/F", "/IM", "terminal64.exe"],
        capture_output=True,
    )
    time.sleep(3)


def patch_ini(ini_path: Path) -> Path:
    """
    Apply headless safety patches to a copy of the INI written to REPORTS_DIR:
      - Rule 3: ensure Expert= ends with .ex5
      - Rule 2: patch Report= to a path inside TERMINAL_DATA
      - Rule 4: ensure Visual=0 and ReplaceReport=1
    Returns path to the patched copy.
    """
    text = ini_path.read_text(encoding="utf-8")

    # Rule 3: strip accidental Experts\ prefix (MT5 adds it automatically), ensure .ex5
    text = re.sub(
        r"^(Expert=)(?:Experts\\)?(.*?)(?:\.ex5)?$",
        lambda m: m.group(1) + m.group(2) + ".ex5",
        text,
        flags=re.MULTILINE,
    )

    # Rule 2: redirect Report= inside terminal data
    stem = ini_path.stem  # e.g. probe_every_tick
    report_path = str(REPORTS_DIR / stem).replace("\\", "\\\\")
    text = re.sub(
        r"^Report=.*$",
        f"Report={report_path}",
        text,
        flags=re.MULTILINE,
    )

    # Rule 4: enforce headless flags
    for key, val in [("Visual", "0"), ("ReplaceReport", "1")]:
        if re.search(rf"^{key}=", text, re.MULTILINE):
            text = re.sub(rf"^{key}=.*$", f"{key}={val}", text, flags=re.MULTILINE)
        else:
            text += f"\n{key}={val}\n"

    patched = REPORTS_DIR / f"{stem}_patched.ini"
    patched.write_text(text, encoding="utf-8")
    return patched


def run_one(label: str, ini_name: str, description: str, terminal: Path) -> bool:
    print(f"\n{'='*64}")
    print(f"  Model: {description}  ({label})")
    print(f"{'='*64}")

    ini_path = INI_DIR / ini_name
    if not ini_path.exists():
        print(f"  ERROR: INI not found: {ini_path}")
        return False

    patched_ini = patch_ini(ini_path)
    out_file = MT5_FILES / f"btp_EURUSD_PERIOD_M15_{label}.csv"

    if out_file.exists():
        out_file.unlink()
        print(f"  Removed previous output: {out_file.name}")

    kill_mt5()

    cmd = [str(terminal), f"/config:{patched_ini}"]
    print(f"  Launching: terminal64.exe /config:{patched_ini.name}")
    t0 = time.time()
    proc = subprocess.Popen(cmd)

    try:
        proc.wait(timeout=TIMEOUT_S)
    except subprocess.TimeoutExpired:
        proc.kill()
        print(f"  ERROR: timed out after {TIMEOUT_S}s")
        return False

    elapsed = time.time() - t0
    print(f"  Terminal exited after {elapsed:.0f}s")

    if out_file.exists():
        dest = RESULTS_DIR / out_file.name
        shutil.copy(out_file, dest)
        sz = out_file.stat().st_size
        rows = sum(1 for _ in out_file.open(encoding="utf-8", errors="ignore")) - 1
        print(f"  Output: {out_file.name}  {sz:,} bytes  ~{rows:,} data rows  → results/")
        return True

    print(f"  WARNING: expected output not found: {out_file}")
    print(f"  Check MQL5 journal for errors.")
    return False


def main():
    requested = set(sys.argv[1:]) if len(sys.argv) > 1 else None
    terminal  = find_terminal()
    print(f"Terminal: {terminal}")

    results = {}
    for label, ini_name, desc in MODELS:
        if requested and label not in requested:
            print(f"\nSkipping {label} ({desc})")
            continue
        ok = run_one(label, ini_name, desc, terminal)
        results[label] = ok

    print("\n" + "="*64)
    print("Summary:")
    for label, ok in results.items():
        status = "OK" if ok else "FAILED / no output"
        print(f"  {label}: {status}")
    print("\nNext step: python analyze_timing.py")


if __name__ == "__main__":
    main()
