"""Benchmark suite for MCA SDK performance testing.

Measures SDK overhead, throughput, and memory consumption with precise metrics.

Usage:
    python tests/performance/benchmark_sdk.py > benchmarks.txt
"""

import statistics
import sys
import time
import tracemalloc
from pathlib import Path

# Import SDK from installed package (assumes 'pip install -e .' was run)
# Do not use sys.path manipulation - breaks when SDK is properly installed
from mca_sdk import MCAClient


def benchmark_prediction_latency(iterations: int = 1000):
    """Measure SDK overhead for prediction recording.

    Args:
        iterations: Number of predictions to record for latency measurement

    Returns:
        dict: Latency metrics (p50, p95, p99, mean, stdev, min, max) in milliseconds
    """
    print(f"Running prediction latency benchmark ({iterations} iterations)...")

    # NOTE: strict_validation=True reflects production default
    # Benchmarks should represent real-world performance, not best-case scenario
    client = MCAClient(
        service_name="benchmark",
        model_id="benchmark-model-001",
        team_name="performance-team",
        strict_validation=True,  # Use production default for realistic benchmarking
    )

    latencies = []

    # Warmup phase (exclude from measurements)
    for _ in range(100):
        client.record_prediction(latency=0.1)

    # Measurement phase
    for i in range(iterations):
        start = time.perf_counter()
        client.record_prediction(
            latency=0.1, model_version="v1.0", input_size=1024, output_size=512
        )
        sdk_overhead = time.perf_counter() - start
        latencies.append(sdk_overhead * 1000)  # Convert to ms

    client.shutdown()

    # Calculate percentiles
    sorted_latencies = sorted(latencies)
    p50 = sorted_latencies[int(len(sorted_latencies) * 0.50)]
    p95 = sorted_latencies[int(len(sorted_latencies) * 0.95)]
    p99 = sorted_latencies[int(len(sorted_latencies) * 0.99)]

    return {
        "iterations": iterations,
        "p50_ms": p50,
        "p95_ms": p95,
        "p99_ms": p99,
        "mean_ms": statistics.mean(latencies),
        "stdev_ms": statistics.stdev(latencies) if len(latencies) > 1 else 0,
        "min_ms": min(latencies),
        "max_ms": max(latencies),
    }


def benchmark_throughput(duration_seconds: int = 10):
    """Measure predictions per second throughput.

    Args:
        duration_seconds: How long to run the throughput test

    Returns:
        dict: Throughput metrics (predictions/sec, total predictions, elapsed time)
    """
    print(f"Running throughput benchmark ({duration_seconds}s duration)...")

    # NOTE: strict_validation=True reflects production default
    client = MCAClient(
        service_name="benchmark",
        model_id="benchmark-model-001",
        team_name="performance-team",
        strict_validation=True,  # Use production default for realistic benchmarking
    )

    count = 0
    # Use monotonic clock for accurate duration measurement
    start_time = time.perf_counter()
    end_time = start_time + duration_seconds

    while time.perf_counter() < end_time:
        client.record_prediction(
            latency=0.1, model_version="v1.0", input_size=1024, output_size=512
        )
        count += 1

    elapsed = time.perf_counter() - start_time
    client.shutdown()

    return {
        "duration_seconds": elapsed,
        "total_predictions": count,
        "predictions_per_second": count / elapsed,
        "avg_latency_ms": (elapsed / count) * 1000,
    }


def benchmark_memory_usage(iterations: int = 10000):
    """Measure memory consumption under sustained load.

    NOTE: Uses tracemalloc which tracks Python object allocations only.
    Does NOT measure total process RSS (which includes Python VM, shared libraries, etc.)
    For complete memory analysis, use external tools like memory_profiler or psutil.

    Args:
        iterations: Number of predictions to record

    Returns:
        dict: Memory metrics (current MB, peak MB, growth rate)
    """
    print(f"Running memory benchmark ({iterations} iterations)...")
    print("NOTE: tracemalloc tracks Python allocations only, not total process memory (RSS)")

    import gc

    tracemalloc.start()

    # Get baseline
    baseline_current, baseline_peak = tracemalloc.get_traced_memory()

    # NOTE: Use production default validation
    client = MCAClient(
        service_name="benchmark",
        model_id="benchmark-model-001",
        team_name="performance-team",
        strict_validation=True,  # Use production default
    )

    # Warmup
    for _ in range(100):
        client.record_prediction(latency=0.1)

    # Force GC to get clean baseline
    gc.collect()

    # Measure memory after warmup
    warmup_current, warmup_peak = tracemalloc.get_traced_memory()

    # Run full load with periodic GC and sampling
    memory_samples = []
    sample_interval = iterations // 10  # Sample 10 times

    for i in range(iterations):
        client.record_prediction(
            latency=0.1, model_version="v1.0", input_size=1024, output_size=512
        )

        if i > 0 and i % sample_interval == 0:
            # Force GC before sampling to get stable measurements
            gc.collect()
            current, peak = tracemalloc.get_traced_memory()
            memory_samples.append((i, current, peak))

    # Force final GC
    gc.collect()
    client.shutdown()

    # Final snapshot
    final_current, final_peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()

    # Calculate linear regression for memory growth trend
    # More robust than simple first/last comparison (not affected by GC timing)
    if len(memory_samples) >= 3:
        import statistics

        iterations_list = [s[0] for s in memory_samples]
        memory_list = [s[1] for s in memory_samples]

        # Calculate slope (bytes per iteration)
        n = len(memory_samples)
        mean_iter = statistics.mean(iterations_list)
        mean_mem = statistics.mean(memory_list)

        numerator = sum(
            (iterations_list[i] - mean_iter) * (memory_list[i] - mean_mem) for i in range(n)
        )
        denominator = sum((iterations_list[i] - mean_iter) ** 2 for i in range(n))

        slope = numerator / denominator if denominator != 0 else 0

        # Express as growth rate percentage over the entire run
        total_growth = slope * iterations
        growth_rate = (total_growth / warmup_current) * 100 if warmup_current > 0 else 0
    else:
        # Fallback to simple comparison if too few samples
        if memory_samples and len(memory_samples) >= 2:
            first_sample = memory_samples[0][1]
            last_sample = memory_samples[-1][1]
            growth_rate = (
                ((last_sample - first_sample) / first_sample) * 100 if first_sample > 0 else 0
            )
        else:
            growth_rate = 0

    return {
        "iterations": iterations,
        "baseline_mb": baseline_current / 1024 / 1024,
        "warmup_current_mb": warmup_current / 1024 / 1024,
        "warmup_peak_mb": warmup_peak / 1024 / 1024,
        "final_current_mb": final_current / 1024 / 1024,
        "final_peak_mb": final_peak / 1024 / 1024,
        "memory_growth_rate_pct": growth_rate,
        "samples": [(i, c / 1024 / 1024, p / 1024 / 1024) for i, c, p in memory_samples],
        "note": "tracemalloc tracks Python object allocations only, not process RSS",
    }


def benchmark_concurrent_predictions(num_threads: int = 4, predictions_per_thread: int = 1000):
    """Measure SDK performance under concurrent load.

    Args:
        num_threads: Number of concurrent threads
        predictions_per_thread: Predictions per thread

    Returns:
        dict: Concurrent performance metrics
    """
    import threading

    print(
        f"Running concurrency benchmark ({num_threads} threads, {predictions_per_thread} predictions/thread)..."
    )

    client = MCAClient(
        service_name="benchmark",
        model_id="benchmark-model-001",
        team_name="performance-team",
        strict_validation=False,
    )

    def worker(thread_id, latencies_list):
        """Worker function for each thread."""
        thread_latencies = []
        for i in range(predictions_per_thread):
            start = time.perf_counter()
            client.record_prediction(latency=0.1, model_version="v1.0", thread_id=thread_id)
            elapsed = time.perf_counter() - start
            thread_latencies.append(elapsed * 1000)
        latencies_list.extend(thread_latencies)

    all_latencies = []
    threads = []
    start_time = time.time()

    # Create and start threads
    for tid in range(num_threads):
        t = threading.Thread(target=worker, args=(tid, all_latencies))
        threads.append(t)
        t.start()

    # Wait for all threads
    for t in threads:
        t.join()

    elapsed = time.time() - start_time
    client.shutdown()

    total_predictions = num_threads * predictions_per_thread
    sorted_latencies = sorted(all_latencies)
    p50 = sorted_latencies[int(len(sorted_latencies) * 0.50)]
    p95 = sorted_latencies[int(len(sorted_latencies) * 0.95)]
    p99 = sorted_latencies[int(len(sorted_latencies) * 0.99)]

    return {
        "num_threads": num_threads,
        "predictions_per_thread": predictions_per_thread,
        "total_predictions": total_predictions,
        "elapsed_seconds": elapsed,
        "throughput_per_second": total_predictions / elapsed,
        "p50_ms": p50,
        "p95_ms": p95,
        "p99_ms": p99,
        "mean_ms": statistics.mean(all_latencies),
    }


def benchmark_gcp_logging_export(iterations: int = 1000):
    """Measure GCP Cloud Logging export latency.

    Uses mocked Cloud Logging client to measure SDK overhead without actual API calls.

    Args:
        iterations: Number of export operations to measure

    Returns:
        dict: Export latency metrics (p50, p95, p99, mean) in milliseconds
    """
    from unittest.mock import MagicMock, Mock, patch
    from mca_sdk.exporters.gcp_logging import CloudLoggingExporter

    print(f"Running GCP Cloud Logging export benchmark ({iterations} iterations)...")

    # Mock the google-cloud-logging module
    mock_cloud_logging = MagicMock()
    mock_client = Mock()
    mock_logger = Mock()
    mock_client.logger.return_value = mock_logger
    mock_cloud_logging.Client.return_value = mock_client

    latencies = []

    with patch.dict("sys.modules", {"google.cloud.logging": mock_cloud_logging}):
        exporter = CloudLoggingExporter(
            project_id="test-project", log_name="test-log", timeout=30.0
        )

        # Create a realistic log batch (10 records)
        log_records = []
        for i in range(10):
            log_record = Mock()
            log_record.body = Mock(value=f"Test log message {i}")
            log_record.severity_text = "INFO"
            log_record.attributes = {
                "request.id": f"req-{i}",
                "model.version": "1.0.0",
                "prediction.id": f"pred-{i}",
            }
            log_record.resource = Mock(
                attributes={"service.name": "test-service", "model.id": "mdl-001"}
            )
            log_records.append(log_record)

        # Warmup phase
        for _ in range(100):
            exporter.export(log_records)

        # Measurement phase
        for i in range(iterations):
            start = time.perf_counter()
            exporter.export(log_records)
            elapsed = time.perf_counter() - start
            latencies.append(elapsed * 1000)  # Convert to ms

        exporter.shutdown()

    # Calculate percentiles
    sorted_latencies = sorted(latencies)
    p50 = sorted_latencies[int(len(sorted_latencies) * 0.50)]
    p95 = sorted_latencies[int(len(sorted_latencies) * 0.95)]
    p99 = sorted_latencies[int(len(sorted_latencies) * 0.99)]

    return {
        "iterations": iterations,
        "batch_size": 10,
        "p50_ms": p50,
        "p95_ms": p95,
        "p99_ms": p99,
        "mean_ms": statistics.mean(latencies),
        "stdev_ms": statistics.stdev(latencies) if len(latencies) > 1 else 0,
        "min_ms": min(latencies),
        "max_ms": max(latencies),
        "target_overhead_ms": 10.0,  # Target: <10ms per batch
    }


def print_results(title, results):
    """Pretty-print benchmark results."""
    print(f"\n{'=' * 80}")
    print(f"{title}")
    print(f"{'=' * 80}")
    for key, value in results.items():
        if key == "samples":
            print("  Memory Samples:")
            for i, (iteration, current, peak) in enumerate(value, 1):
                print(
                    f"    Sample {i} (iter {iteration}): Current={current:.2f} MB, Peak={peak:.2f} MB"
                )
        elif isinstance(value, float):
            print(f"  {key}: {value:.3f}")
        else:
            print(f"  {key}: {value}")


def main():
    """Run all benchmarks and display results."""
    print("=" * 80)
    print("MCA SDK Performance Benchmark Suite")
    print("=" * 80)
    print()

    # 1. Prediction latency
    latency_results = benchmark_prediction_latency(iterations=1000)
    print_results("1. PREDICTION RECORDING LATENCY", latency_results)

    # 2. Throughput
    throughput_results = benchmark_throughput(duration_seconds=10)
    print_results("2. THROUGHPUT (SUSTAINED LOAD)", throughput_results)

    # 3. Memory usage
    memory_results = benchmark_memory_usage(iterations=10000)
    print_results("3. MEMORY USAGE (SUSTAINED LOAD)", memory_results)

    # 4. Concurrent performance
    concurrent_results = benchmark_concurrent_predictions(
        num_threads=4, predictions_per_thread=1000
    )
    print_results("4. CONCURRENT PERFORMANCE (4 THREADS)", concurrent_results)

    # 5. GCP Cloud Logging export latency
    gcp_logging_results = benchmark_gcp_logging_export(iterations=1000)
    print_results("5. GCP CLOUD LOGGING EXPORT LATENCY", gcp_logging_results)

    # Summary
    print("\n" + "=" * 80)
    print("PERFORMANCE SUMMARY")
    print("=" * 80)
    print(f"  Latency (p95):          {latency_results['p95_ms']:.3f} ms")
    print(f"  Latency (p99):          {latency_results['p99_ms']:.3f} ms")
    print(
        f"  Throughput:             {throughput_results['predictions_per_second']:.0f} predictions/sec"
    )
    print(f"  Peak Memory:            {memory_results['final_peak_mb']:.2f} MB")
    print(f"  Memory Growth Rate:     {memory_results['memory_growth_rate_pct']:.2f}%")
    print(
        f"  Concurrent Throughput:  {concurrent_results['throughput_per_second']:.0f} predictions/sec"
    )
    print(f"  GCP Logging (p95):      {gcp_logging_results['p95_ms']:.3f} ms")
    print("=" * 80)

    # Requirements check
    print("\nREQUIREMENTS CHECK:")
    print("=" * 80)
    print(
        f"  SDK overhead <50ms (p95):     {'✅ PASS' if latency_results['p95_ms'] < 50 else '❌ FAIL'}"
    )
    print(
        f"  SDK overhead <100ms (p99):    {'✅ PASS' if latency_results['p99_ms'] < 100 else '❌ FAIL'}"
    )
    print(
        f"  Throughput >1000 req/s:       {'✅ PASS' if throughput_results['predictions_per_second'] > 1000 else '❌ FAIL'}"
    )
    print(
        f"  Flat memory profile:          {'✅ PASS' if abs(memory_results['memory_growth_rate_pct']) < 10 else '❌ FAIL'}"
    )
    print(
        f"  GCP Logging <10ms (p95):      {'✅ PASS' if gcp_logging_results['p95_ms'] < 10 else '❌ FAIL'}"
    )
    print("=" * 80)


if __name__ == "__main__":
    main()
