from dataclasses import dataclass, field
from typing import List, Optional, Tuple

from sglang.test.nightly_bench_utils import BenchmarkResult
from sglang.test.nightly_utils import NightlyBenchmarkRunner
from sglang.test.test_utils import DEFAULT_URL_FOR_TEST, ModelLaunchSettings


@dataclass
class PerformanceTestParams:
    """Parameters for performance testing."""

    batch_sizes: List[int] = field(default_factory=lambda: [1, 8, 16])
    input_lens: Tuple[int, ...] = (8192,)
    output_lens: Tuple[int, ...] = (512,)
    profile_dir: Optional[str] = None  # None = auto-generate based on is_vlm
    dataset_name: str = "mmmu"  # For VLM perf test
    # MTP/EAGLE speculative decoding: minimum accept length threshold (None = no validation)
    spec_accept_length_threshold: Optional[float] = None


@dataclass
class PerformanceTestResult:
    """Result of a performance test.

    Aggregates metrics across all batch sizes tested for a single model.
    """

    model: str
    passed: bool
    error: Optional[str]
    # Aggregate metrics (from the largest batch size result, or None if failed)
    latency: Optional[float] = None
    input_throughput: Optional[float] = None
    output_throughput: Optional[float] = None
    overall_throughput: Optional[float] = None
    # All individual benchmark results
    benchmark_results: Optional[List[BenchmarkResult]] = None
    # MTP/EAGLE speculative decoding metric
    avg_spec_accept_length: Optional[float] = None


def run_performance_test(
    model: ModelLaunchSettings,
    perf_runner: NightlyBenchmarkRunner,
    batch_sizes: List[int] = None,
    input_lens: Tuple[int, ...] = (4096,),
    output_lens: Tuple[int, ...] = (512,),
    is_vlm: bool = False,
    dataset_name: str = "mmmu",
    spec_accept_length_threshold: Optional[float] = None,
) -> PerformanceTestResult:

    # Set default for mutable argument
    if batch_sizes is None:
        batch_sizes = [1, 8, 16, 64]

    print(f"\n{'='*60}")
    print(f"Running PERFORMANCE test for {model.model_path}")
    print(f"  Variant: {model.variant}")
    print(f"  Batch sizes: {batch_sizes}")
    print(f"  Input lens: {input_lens}")
    print(f"  Output lens: {output_lens}")
    if spec_accept_length_threshold is not None:
        print(f"  Spec accept length threshold: {spec_accept_length_threshold}")
    print(f"{'='*60}\n")

    # Build extra args for benchmarks
    extra_bench_args = ["--trust-remote-code"]
    if is_vlm:
        extra_bench_args.append(f"--dataset-name={dataset_name}")

    try:
        results, success, avg_spec_accept_length = perf_runner.run_benchmark_for_model(
            model_path=model.model_path,
            batch_sizes=batch_sizes,
            input_lens=input_lens,
            output_lens=output_lens,
            other_args=model.extra_args,
            variant=model.variant or "",
            extra_bench_args=extra_bench_args,
            env=model.env,
        )

        if success and results:
            perf_runner.add_report(results, variant=model.variant)
            print(f"✓ Performance test succeeded for {model.model_path}")

            # The cumulative /server_info accept length is reset by the cache
            # flush before the profiling phase, so it can be missing here. Fall
            # back to the per-run accept lengths captured during benchmarking.
            if avg_spec_accept_length is None:
                run_accept_lengths = [
                    r.acc_length
                    for r in results
                    if r.acc_length is not None and r.acc_length > 0
                ]
                if run_accept_lengths:
                    avg_spec_accept_length = sum(run_accept_lengths) / len(
                        run_accept_lengths
                    )

            # Validate speculative decoding accept length if threshold is set
            error_msg = None
            passed = True
            if spec_accept_length_threshold is not None:
                if avg_spec_accept_length is None:
                    error_msg = f"Spec accept length threshold set but no accept length reported"
                    passed = False
                    print(f"✗ {error_msg}")
                elif avg_spec_accept_length < spec_accept_length_threshold:
                    error_msg = (
                        f"Spec accept length {avg_spec_accept_length:.2f} < "
                        f"threshold {spec_accept_length_threshold}"
                    )
                    passed = False
                    print(f"✗ {error_msg}")
                else:
                    print(
                        f"✓ Spec accept length {avg_spec_accept_length:.2f} >= "
                        f"threshold {spec_accept_length_threshold}"
                    )

            # Extract aggregate metrics from the largest batch size result
            largest_batch_result = max(results, key=lambda r: r.batch_size)
            return PerformanceTestResult(
                model=model.model_path,
                passed=passed,
                error=error_msg,
                latency=largest_batch_result.latency,
                input_throughput=largest_batch_result.input_throughput,
                output_throughput=largest_batch_result.output_throughput,
                overall_throughput=largest_batch_result.overall_throughput,
                benchmark_results=results,
                avg_spec_accept_length=avg_spec_accept_length,
            )
        else:
            error_msg = f"Performance test failed for {model.model_path}"
            print(f"✗ {error_msg}")
            return PerformanceTestResult(
                model=model.model_path,
                passed=False,
                error=error_msg,
            )

    except Exception as e:
        error_msg = f"Performance test exception for {model.model_path}: {str(e)}"
        print(f"✗ {error_msg}")
        return PerformanceTestResult(
            model=model.model_path,
            passed=False,
            error=error_msg,
        )


def run_performance_for_models(
    models: List[ModelLaunchSettings],
    profile_dir: str,
    test_name: str,
    base_url: Optional[str] = None,
    batch_sizes: List[int] = None,
    input_lens: Tuple[int, ...] = (4096,),
    output_lens: Tuple[int, ...] = (512,),
    is_vlm: bool = False,
    dataset_name: str = "mmmu",
) -> dict:
    """Run performance tests for multiple models.

    Args:
        models: List of ModelLaunchSettings to test
        profile_dir: Directory for performance profiles
        test_name: Name for the test (used in reports)
        base_url: Server base URL (default: DEFAULT_URL_FOR_TEST)
        batch_sizes: Batch sizes for perf test
        input_lens: Input lengths
        output_lens: Output lengths
        is_vlm: Whether these are VLM models
        dataset_name: Dataset name for VLM benchmarks

    Returns:
        dict with results:
        {
            "all_passed": bool,
            "results": [PerformanceTestResult, ...]
        }
    """
    base_url = base_url or DEFAULT_URL_FOR_TEST

    # Setup performance runner
    perf_runner = NightlyBenchmarkRunner(
        profile_dir=profile_dir,
        test_name=test_name,
        base_url=base_url,
    )
    perf_runner.setup_profile_directory()

    all_results = []
    all_passed = True

    for model in models:
        print("\n" + "=" * 80)
        print(f"PERFORMANCE TEST: {model.model_path}")
        print(f"  TP Size: {model.tp_size}")
        print(f"  Extra Args: {model.extra_args}")
        print("=" * 80)

        result = run_performance_test(
            model=model,
            perf_runner=perf_runner,
            batch_sizes=batch_sizes,
            input_lens=input_lens,
            output_lens=output_lens,
            is_vlm=is_vlm,
            dataset_name=dataset_name,
        )

        all_results.append(result)

        if not result.passed:
            all_passed = False

    # Write performance report
    perf_runner.write_final_report()

    # Print summary
    print("\n" + "=" * 60)
    print(f"Performance Test Summary: {test_name}")
    print("=" * 60)
    for result in all_results:
        status = "PASS" if result.passed else "FAIL"
        throughput_str = (
            f", output: {result.output_throughput:.1f} tok/s"
            if result.output_throughput
            else ""
        )
        print(f"  {result.model}: {status}{throughput_str}")
        if result.error:
            print(f"    Error: {result.error}")

    print("\n" + "=" * 60)
    print(f"OVERALL: {'ALL PASSED' if all_passed else 'SOME FAILED'}")
    print("=" * 60 + "\n")

    return {
        "all_passed": all_passed,
        "results": all_results,
    }
