#!/usr/bin/env python3
"""
MT5 Direct Mode Stress Test
Supports concurrent workers, P50/P95 latency, status distribution.
Output: logs/mt5_direct_stress.{timestamp}.json
"""

import argparse
import json
import sys
import time
import threading
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from statistics import median
from typing import Any, Dict, List

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT / "src") not in sys.path:
    sys.path.insert(0, str(ROOT / "src"))

from mt5_execution import dispatch_execution


@dataclass
class Sample:
    signal_id: str
    status: str
    reason: str
    latency_ms: int
    retries: int
    ts: str


@dataclass
class Result:
    total: int = 0
    FILLED: int = 0
    ACKed: int = 0  # Note: dataclass uses ACKed but mock returns ACKed
    ACKed_cnt: int = 0  # counter for ACKed status
    REJECTED: int = 0
    TIMEOUT: int = 0
    RECONCILED: int = 0
    PARTIAL_FILL: int = 0
    RETRY_EXHAUSTED: int = 0
    INIT_FAILED: int = 0
    latencies_ms: List[int] = field(default_factory=list)
    samples: List[Dict] = field(default_factory=list)
    lock: threading.Lock = field(default_factory=threading.Lock)


def _build_signal(i: int, seq: int, symbol: str) -> Dict[str, Any]:
    ts = int(datetime.now(timezone.utc).timestamp())
    return {
        "signal_id": f"stress_{symbol}_{ts}_{seq}",
        "symbol": symbol,
        "action": "OPEN",
        "side": "BUY" if i % 2 == 0 else "SELL",
        "volume_lots": 0.01,
        "sl_price": 3000.0 + (i % 10) * 0.5,
        "tp_price": 3010.0 + (i % 10) * 0.5,
    }


def _worker(args: tuple) -> Sample:
    i, seq, symbol, mode = args
    signal = _build_signal(i, seq, symbol)
    start = time.time()
    try:
        report = dispatch_execution(signal, mode, symbol, None)
    except Exception as exc:
        report = {
            "status": "TIMEOUT",
            "reason": f"EXCEPTION:{type(exc).__name__}:{exc}",
            "retries": 0,
        }
    latency_ms = max(1, int((time.time() - start) * 1000))
    status = str(report.get("status", "REJECTED")).upper()
    return Sample(
        signal_id=signal["signal_id"],
        status=status,
        reason=str(report.get("reason", "")),
        latency_ms=latency_ms,
        retries=int(report.get("retries", 0)),
        ts=datetime.now(timezone.utc).isoformat(),
    )


def _pct(data: List[float], p: float) -> float:
    if not data:
        return 0.0
    s = sorted(data)
    idx = (len(s) - 1) * p / 100.0
    lo = int(idx)
    hi = min(lo + 1, len(s) - 1)
    return round(s[lo] + (s[hi] - s[lo]) * (idx - lo), 1)


def run(args: argparse.Namespace, result: Result) -> None:
    symbol = args.symbol
    mode = args.mode
    tasks = [(i, i, symbol, mode) for i in range(args.iterations)]

    workers = getattr(args, "workers", 1)
    if workers <= 1:
        # Sequential
        for t in tasks:
            s = _worker(t)
            _record(s, result)
    else:
        with ThreadPoolExecutor(max_workers=workers) as pool:
            futures = [pool.submit(_worker, t) for t in tasks]
            for fut in as_completed(futures):
                try:
                    s = fut.result()
                    _record(s, result)
                except Exception as exc:
                    with result.lock:
                        result.total += 1
                        result.TIMEOUT += 1


def _record(s: Sample, result: Result) -> None:
    with result.lock:
        result.total += 1
        attr = s.status
        if hasattr(result, attr):
            setattr(result, attr, getattr(result, attr) + 1)
        result.latencies_ms.append(s.latency_ms)
        if len(result.samples) < 5:
            result.samples.append({
                "signal_id": s.signal_id,
                "status": s.status,
                "reason": s.reason,
                "latency_ms": s.latency_ms,
                "retries": s.retries,
            })


def main() -> int:
    parser = argparse.ArgumentParser(description="MT5 Direct Mode Stress Test")
    parser.add_argument("--symbol", default="XAUUSD")
    parser.add_argument("--iterations", type=int, default=20)
    parser.add_argument("--sleep-ms", type=int, default=100)
    parser.add_argument("--workers", type=int, default=1,
                        help="Concurrent workers (default 1 = sequential)")
    parser.add_argument("--mode", default="direct",
                        choices=["direct", "mock"],
                        help="Execution mode (default direct)")
    parser.add_argument("--output", default="")
    parser.add_argument("--warmup", type=int, default=0,
                        help="Warmup iterations before measuring")
    args = parser.parse_args()

    if args.sleep_ms > 0 and args.workers > 1:
        print("Note: --sleep-ms ignored when --workers > 1", file=sys.stderr)

    # Warmup
    if args.warmup > 0:
        print(f"Warmup: {args.warmup} iterations...")
        warmup_result = Result()
        run(argparse.Namespace(
            symbol=args.symbol, mode=args.mode,
            iterations=args.warmup, workers=1, sleep_ms=0), warmup_result)
        print(f"  warmup done: {warmup_result.total} runs")

    # Main run
    print(f"Starting stress test: {args.iterations} iterations x {max(1, args.workers)} worker(s)")
    result = Result()
    start_time = time.time()
    run(args, result)
    wall_ms = int((time.time() - start_time) * 1000)

    latencies = result.latencies_ms
    summary = {
        "total": result.total,
        "FILLED": result.FILLED,
        "ACKED": result.ACKed,
        "REJECTED": result.REJECTED,
        "TIMEOUT": result.TIMEOUT,
        "RECONCILED": result.RECONCILED,
        "PARTIAL_FILL": result.PARTIAL_FILL,
        "RETRY_EXHAUSTED": result.RETRY_EXHAUSTED,
        "INIT_FAILED": result.INIT_FAILED,
        "success_rate_pct": round(result.FILLED / max(result.total, 1) * 100, 2),
        "throughput_per_sec": round(result.total / max(wall_ms / 1000, 0.001), 2),
        "wall_time_ms": wall_ms,
        "latency_p50_ms": round(_pct([float(x) for x in latencies], 50), 1),
        "latency_p95_ms": round(_pct([float(x) for x in latencies], 95), 1),
        "latency_p99_ms": round(_pct([float(x) for x in latencies], 99), 1),
        "latency_min_ms": min(latencies) if latencies else 0,
        "latency_max_ms": max(latencies) if latencies else 0,
        "latency_avg_ms": round(sum(latencies) / max(len(latencies), 1), 1),
    }

    ts_str = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    output_path = Path(args.output) if args.output else \
                  ROOT / "logs" / f"mt5_direct_stress.{ts_str}.json"
    output_path.parent.mkdir(parents=True, exist_ok=True)

    payload = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "args": {
            "symbol": args.symbol, "iterations": args.iterations,
            "workers": max(1, args.workers), "mode": args.mode,
            "warmup": args.warmup,
        },
        "summary": summary,
        "samples": result.samples,
    }

    output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"\n{'='*50}")
    print(f"  Stress Test Complete")
    print(f"{'='*50}")
    print(f"  Total:    {summary['total']}")
    print(f"  FILLED:   {summary['FILLED']}")
    print(f"  REJECTED: {summary['REJECTED']}")
    print(f"  TIMEOUT:  {summary['TIMEOUT']}")
    print(f"  Success:  {summary['success_rate_pct']}%")
    print(f"  P50:      {summary['latency_p50_ms']}ms")
    print(f"  P95:      {summary['latency_p95_ms']}ms")
    print(f"  Throughput: {summary['throughput_per_sec']}/s")
    print(f"  Report:   {output_path}")
    print(f"{'='*50}")
    print(json.dumps({"ok": True, "output": str(output_path), "summary": summary}, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
