"""
Synthetic load test for the NIDS inference pipeline.
Simulates increasing numbers of concurrent flows hitting classify_flow()
and log_detection() directly (no real network traffic needed), to measure
how latency and throughput behave under increasing pressure.

The test doubles the concurrency each phase (10 -> 20 -> 40 -> ...) until
an actual failure occurs (a real exception while processing a flow) or a
safety cap is reached.

Usage (inside the capture-service container):
    python3 tests/load_test_synthetic.py
"""

import random
import sys
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

import numpy as np


sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

import db_writer
import inference_engine as ie

db_writer.init_db()

SEQ_LEN = ie.model.seq_len

# Roughly realistic ranges for TLS packet size / inter-arrival time
SIZE_RANGE = (60, 1500)     # bytes, typical range for TLS packets
IAT_RANGE_MS = (0.5, 300)   # milliseconds, typical gap between packets

# Safety cap to avoid an infinite loop if the system never actually fails
MAX_CONCURRENT_FLOWS = 20000


def build_random_ppi_matrix():
    """Build one synthetic but realistically-shaped PPI matrix."""
    matrix = np.zeros((SEQ_LEN, 3), dtype=np.float64)
    n_packets = random.randint(3, SEQ_LEN)  # realistic flow length (some flows shorter than SEQ_LEN)
    for i in range(n_packets):
        matrix[i, 0] = 0.0 if i == 0 else random.uniform(*IAT_RANGE_MS)
        matrix[i, 1] = random.choice([1, -1])
        matrix[i, 2] = random.uniform(*SIZE_RANGE)
    return matrix


def simulate_one_flow(flow_index):
    """Classify one synthetic flow and log it, returning the measured latency."""
    matrix = build_random_ppi_matrix()
    decision, error, latency_ms = ie.classify_flow(matrix)

    db_writer.log_detection(
        src_ip=f"198.51.100.{flow_index % 255}",  # reserved TEST-NET-2 range (RFC 5737)
        dst_ip="203.0.113.1",
        dst_port=443,
        stage="SYNTHETIC_LOAD_TEST",
        decision=decision,
        reconstruction_error=error,
        threshold=ie.THRESHOLD,
        processing_latency_ms=latency_ms,
        flow_duration_sec=random.uniform(0.1, 5.0),
    )
    return latency_ms


def run_phase(name, n_concurrent):
    print("=" * 70)
    print(f"PHASE: {name}")
    print("=" * 70)

    start = time.perf_counter()
    latencies = []
    failures = 0

    with ThreadPoolExecutor(max_workers=min(n_concurrent, 64)) as executor:
        futures = [executor.submit(simulate_one_flow, i) for i in range(n_concurrent)]
        for future in as_completed(futures):
            try:
                latencies.append(future.result())
            except Exception as e:
                failures += 1
                print(f"  ERROR processing flow: {e}")

    total_time = time.perf_counter() - start
    throughput = n_concurrent / total_time if total_time > 0 else 0

    latencies.sort()
    n = len(latencies)

    print(f"Flows submitted: {n_concurrent} | Succeeded: {n} | Failed: {failures}")
    print(f"Total wall time: {total_time:.3f}s | Throughput: {throughput:.1f} flows/sec")
    if n > 0:
        print(
            f"Per-flow latency (ms) -> min: {latencies[0]:.2f} | "
            f"p90: {latencies[int(n * 0.9)]:.2f} | "
            f"max: {latencies[-1]:.2f}"
        )
    print()

    return {
        "phase": name,
        "n_concurrent": n_concurrent,
        "succeeded": n,
        "failed": failures,
        "total_time_s": round(total_time, 3),
        "throughput_fps": round(throughput, 1),
        "p90_ms": round(latencies[int(n * 0.9)], 2) if n else None,
        "max_ms": round(latencies[-1], 2) if n else None,
    }


def main():
    results = []
    n_concurrent = 10

    while True:
        phase_name = f"{n_concurrent} concurrent flows"
        result = run_phase(phase_name, n_concurrent)
        results.append(result)

        failed = result.get("failed", 0)

        # Stop only on an actual failure (a real exception), not on slowness
        if failed > 0:
            print(f"!!! STOPPING: {failed} flow(s) failed at {n_concurrent} concurrent flows.")
            break

        if n_concurrent >= MAX_CONCURRENT_FLOWS:
            print(f"!!! STOPPING: reached safety cap of {MAX_CONCURRENT_FLOWS} concurrent flows without failure.")
            break

        n_concurrent *= 2  # double the load each phase (10 -> 20 -> 40 -> ...)

    print("=" * 70)
    print("SUMMARY TABLE")
    print("=" * 70)
    header = f"{'Phase':<32}{'Throughput':<14}{'P90(ms)':<12}{'Max(ms)':<12}{'Failed':<8}"
    print(header)
    print("-" * len(header))
    for r in results:
        print(
            f"{r['phase']:<32}{str(r['throughput_fps']) + ' f/s':<14}"
            f"{str(r['p90_ms']):<12}{str(r['max_ms']):<12}"
            f"{str(r['failed']):<8}"
        )


if __name__ == "__main__":
    main()
