#!/usr/bin/env python3
"""KPI regression gate for solver refactor phases."""

from __future__ import annotations

import argparse
import hashlib
import json
import math
import platform
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional, Set, Tuple

try:
    import yaml
except Exception:  # pragma: no cover - optional dependency for CLI usage
    yaml = None

TARGET_POLICY_CLASS_SLUGS = (
    "switch_heavy",
    "zero_cross",
    "magnetic_nonlinear",
    "closed_loop_control",
)


def _quantile(values: list[float], q: float) -> Optional[float]:
    if not values:
        return None
    ordered = sorted(values)
    if len(ordered) == 1:
        return ordered[0]
    index = (len(ordered) - 1) * q
    lo = int(index)
    hi = min(lo + 1, len(ordered) - 1)
    frac = index - lo
    return ordered[lo] * (1.0 - frac) + ordered[hi] * frac


def _coerce_finite_float(value: Any) -> Optional[float]:
    try:
        numeric = float(value)
    except (TypeError, ValueError):
        return None
    if not math.isfinite(numeric):
        return None
    return numeric


def _normal_cdf(z_value: float) -> float:
    return 0.5 * (1.0 + math.erf(z_value / math.sqrt(2.0)))


def _policy_target_failure_stats(metrics: Dict[str, Any]) -> Optional[Dict[str, float]]:
    case_count = _coerce_finite_float(metrics.get("policy_target_case_count"))
    if case_count is None or case_count <= 0.0:
        case_total = 0.0
        has_any_class_count = False
        for slug in TARGET_POLICY_CLASS_SLUGS:
            class_count = _coerce_finite_float(metrics.get(f"class_{slug}_case_count"))
            if class_count is None or class_count <= 0.0:
                continue
            has_any_class_count = True
            case_total += class_count
        if has_any_class_count and case_total > 0.0:
            case_count = case_total

    failure_rate = _coerce_finite_float(metrics.get("policy_target_terminal_failure_rate"))
    if failure_rate is None:
        pass_rate = _coerce_finite_float(metrics.get("policy_target_pass_rate"))
        if pass_rate is not None:
            failure_rate = max(0.0, min(1.0, 1.0 - pass_rate))
    if case_count is None or case_count <= 0.0 or failure_rate is None:
        return None

    failure_count = _coerce_finite_float(metrics.get("policy_target_terminal_failure_count"))
    if failure_count is None:
        failure_count = max(0.0, min(case_count, failure_rate * case_count))
    else:
        failure_count = max(0.0, min(case_count, failure_count))

    return {
        "case_count": case_count,
        "failure_count": failure_count,
        "failure_rate": max(0.0, min(1.0, failure_rate)),
    }


def evaluate_gate_c_target_failure_drop_significance(
    baseline_metrics: Dict[str, Any],
    current_metrics: Dict[str, Any],
    alpha: float = 0.05,
) -> Dict[str, Any]:
    baseline = _policy_target_failure_stats(baseline_metrics)
    current = _policy_target_failure_stats(current_metrics)
    required = True
    metric_name = "gate_c_target_failure_drop_significance"

    if baseline is None or current is None:
        return {
            "status": "failed",
            "message": (
                "target failure statistics unavailable "
                "(requires policy_target_* and class matrix coverage)"
            ),
            "current": None,
            "baseline": None,
            "direction": "higher_is_better",
            "regression_abs": None,
            "regression_rel": None,
            "max_regression_abs": 0.0,
            "max_regression_rel": None,
            "required": required,
            "metric": metric_name,
            "significance_alpha": alpha,
            "significance_p_value": None,
            "z_score": None,
        }

    baseline_rate = baseline["failure_rate"]
    current_rate = current["failure_rate"]
    baseline_count = baseline["case_count"]
    current_count = current["case_count"]

    significant_drop = False
    message = ""
    p_value: Optional[float] = None
    z_score: Optional[float] = None

    if baseline_rate <= 1e-12:
        significant_drop = current_rate <= baseline_rate + 1e-12
        if significant_drop:
            message = "baseline has zero target failures; non-regression preserved"
        else:
            message = "baseline has zero target failures but current run regressed"
    else:
        if current_rate >= baseline_rate:
            significant_drop = False
            message = "current target failure rate did not improve"
        else:
            pooled_rate = (
                baseline["failure_count"] + current["failure_count"]
            ) / max(baseline_count + current_count, 1.0)
            variance = pooled_rate * (1.0 - pooled_rate) * (
                (1.0 / baseline_count) + (1.0 / current_count)
            )
            if variance <= 0.0:
                significant_drop = current_rate < baseline_rate
                message = (
                    "degenerate variance; accepted strict improvement" if significant_drop
                    else "degenerate variance without strict improvement"
                )
            else:
                z_score = (baseline_rate - current_rate) / math.sqrt(variance)
                p_value = 1.0 - _normal_cdf(z_score)
                significant_drop = p_value <= alpha
                if significant_drop:
                    message = "target failure drop is statistically significant"
                else:
                    message = (
                        f"target failure drop not significant (p={p_value:.3g} > alpha={alpha:.3g})"
                    )

    current_flag = 1.0 if significant_drop else 0.0
    return {
        "status": "passed" if significant_drop else "failed",
        "message": message,
        "current": current_flag,
        "baseline": 1.0,
        "direction": "higher_is_better",
        "regression_abs": 1.0 - current_flag,
        "regression_rel": 1.0 - current_flag,
        "max_regression_abs": 0.0,
        "max_regression_rel": None,
        "required": required,
        "metric": metric_name,
        "significance_alpha": alpha,
        "significance_p_value": p_value,
        "z_score": z_score,
        "baseline_target_failure_rate": baseline_rate,
        "current_target_failure_rate": current_rate,
        "baseline_target_case_count": baseline_count,
        "current_target_case_count": current_count,
    }


def _load_json(path: Path) -> Dict[str, Any]:
    with open(path, "r", encoding="utf-8") as handle:
        return json.load(handle)


def _load_thresholds(path: Path) -> Dict[str, Any]:
    if path.suffix.lower() == ".json":
        return _load_json(path)
    if yaml is None:
        raise RuntimeError("PyYAML is required to load non-JSON thresholds")
    with open(path, "r", encoding="utf-8") as handle:
        return yaml.safe_load(handle)


def _load_phase_budget_metrics(
    phase_budget_path: Optional[Path],
    phase_budget_key: Optional[str],
) -> Dict[str, Dict[str, Any]]:
    if phase_budget_path is None and phase_budget_key is None:
        return {}
    if phase_budget_path is None or phase_budget_key is None:
        raise RuntimeError("phase budget requires both --phase-budget and --phase-key")
    if not phase_budget_path.is_file():
        raise FileNotFoundError(f"phase budget file not found: {phase_budget_path}")
    if yaml is None:
        raise RuntimeError("PyYAML is required to load phase budget YAML files")

    with open(phase_budget_path, "r", encoding="utf-8") as handle:
        payload = yaml.safe_load(handle) or {}
    phases = payload.get("phases")
    if not isinstance(phases, dict):
        raise RuntimeError("phase budget file must define a 'phases' object")

    phase_entry = phases.get(phase_budget_key)
    if not isinstance(phase_entry, dict):
        raise RuntimeError(f"phase budget key not found: {phase_budget_key}")

    metrics = phase_entry.get("metrics")
    if not isinstance(metrics, dict):
        raise RuntimeError(
            f"phase budget '{phase_budget_key}' must define a 'metrics' mapping"
        )

    out: Dict[str, Dict[str, Any]] = {}
    for metric_name, rules in metrics.items():
        if not isinstance(metric_name, str) or not metric_name.strip():
            continue
        if not isinstance(rules, dict):
            continue
        out[metric_name.strip()] = dict(rules)
    return out


def _sanitize_metric_slug(label: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "_", label.strip().lower()).strip("_")
    return slug or "unknown"


def _normalize_matrix_case_keys(cases: Any) -> Set[Tuple[str, str]]:
    case_keys: Set[Tuple[str, str]] = set()
    if not isinstance(cases, list):
        return case_keys

    for entry in cases:
        if not isinstance(entry, dict):
            continue
        benchmark_id_raw = entry.get("benchmark_id")
        if not isinstance(benchmark_id_raw, str) or not benchmark_id_raw.strip():
            continue
        benchmark_id = benchmark_id_raw.strip()

        scenarios_raw = entry.get("scenarios")
        scenarios: list[str] = []
        if isinstance(scenarios_raw, list):
            for item in scenarios_raw:
                if isinstance(item, str) and item.strip():
                    scenarios.append(item.strip())
        elif isinstance(entry.get("scenario"), str) and entry.get("scenario", "").strip():
            scenarios.append(str(entry.get("scenario")).strip())

        if not scenarios:
            scenarios = ["default"]

        for scenario in scenarios:
            case_keys.add((benchmark_id, scenario))

    return case_keys


def _load_class_matrix(path: Optional[Path]) -> Dict[str, Set[Tuple[str, str]]]:
    if path is None:
        return {}
    if not path.is_file():
        raise FileNotFoundError(f"class matrix file not found: {path}")
    if yaml is None:
        raise RuntimeError("PyYAML is required to load class matrix YAML files")

    with open(path, "r", encoding="utf-8") as handle:
        payload = yaml.safe_load(handle) or {}

    classes = payload.get("classes")
    if not isinstance(classes, dict):
        return {}

    matrix: Dict[str, Set[Tuple[str, str]]] = {}
    for class_name, class_entry in classes.items():
        if not isinstance(class_name, str) or not class_name.strip():
            continue
        if not isinstance(class_entry, dict):
            continue
        case_keys = _normalize_matrix_case_keys(class_entry.get("cases"))
        if case_keys:
            matrix[class_name.strip()] = case_keys
    return matrix


def _sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with open(path, "rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _sha256_file_with_normalized_eol(path: Path) -> Optional[str]:
    text_like_suffixes = {
        ".json",
        ".yaml",
        ".yml",
        ".csv",
        ".txt",
        ".md",
    }
    if path.suffix.lower() not in text_like_suffixes:
        return None

    payload = path.read_bytes()
    if b"\x00" in payload:
        return None

    normalized = payload.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
    digest = hashlib.sha256()
    digest.update(normalized)
    return digest.hexdigest()


def _parse_utc_timestamp(raw: Any) -> Optional[datetime]:
    if not isinstance(raw, str) or not raw.strip():
        return None
    normalized = raw.strip().replace("Z", "+00:00")
    try:
        parsed = datetime.fromisoformat(normalized)
    except ValueError:
        return None
    return parsed.astimezone(timezone.utc)


def validate_threshold_policy(payload: Dict[str, Any]) -> Dict[str, Any]:
    errors: list[str] = []
    warnings: list[str] = []
    checks: Dict[str, Any] = {}

    schema = payload.get("schema")
    if schema is not None and schema != "pulsim-kpi-gates-v1":
        errors.append("threshold policy has unsupported schema value")
    checks["schema"] = schema

    version = payload.get("version")
    if version is not None and not isinstance(version, int):
        errors.append("threshold policy version must be an integer when provided")
    checks["version"] = version

    metrics = payload.get("metrics")
    if not isinstance(metrics, dict) or not metrics:
        errors.append("threshold policy must define a non-empty metrics mapping")
        checks["metrics_count"] = 0
    else:
        checks["metrics_count"] = len(metrics)
        for metric_name, rules in metrics.items():
            if not isinstance(metric_name, str) or not metric_name:
                errors.append("threshold metric names must be non-empty strings")
                continue
            if not isinstance(rules, dict):
                errors.append(f"threshold rules for metric '{metric_name}' must be an object")
                continue
            direction = rules.get("direction", "lower_is_better")
            if direction not in ("lower_is_better", "higher_is_better"):
                errors.append(
                    f"threshold metric '{metric_name}' has invalid direction '{direction}'"
                )
            for key in ("max_regression_abs", "max_regression_rel"):
                value = rules.get(key)
                if value is None:
                    continue
                try:
                    numeric = float(value)
                except (TypeError, ValueError):
                    errors.append(
                        f"threshold metric '{metric_name}' field '{key}' must be numeric"
                    )
                    continue
                if numeric < 0.0:
                    errors.append(
                        f"threshold metric '{metric_name}' field '{key}' must be >= 0"
                    )
            if "max_regression_abs" not in rules and "max_regression_rel" not in rules:
                warnings.append(
                    f"threshold metric '{metric_name}' has no regression bound and will never fail"
                )

    return {
        "status": "failed" if errors else "passed",
        "errors": errors,
        "warnings": warnings,
        "checks": checks,
    }


def validate_baseline_provenance(
    baseline_payload: Dict[str, Any],
    baseline_path: Path,
) -> Dict[str, Any]:
    errors: list[str] = []
    warnings: list[str] = []
    checks: Dict[str, Any] = {
        "baseline_path": str(baseline_path),
    }

    schema_version = baseline_payload.get("schema_version")
    if schema_version != "pulsim-kpi-baseline-v1":
        errors.append("baseline schema_version must be 'pulsim-kpi-baseline-v1'")
    checks["schema_version"] = schema_version

    baseline_id = baseline_payload.get("baseline_id")
    if not isinstance(baseline_id, str) or not baseline_id.strip():
        errors.append("baseline_id must be a non-empty string")
    checks["baseline_id"] = baseline_id

    captured_at = baseline_payload.get("captured_at_utc")
    captured_at_ts = _parse_utc_timestamp(captured_at)
    if captured_at_ts is None:
        errors.append("captured_at_utc must be a valid ISO-8601 timestamp")
    checks["captured_at_utc"] = captured_at

    metrics = baseline_payload.get("metrics")
    if not isinstance(metrics, dict) or not metrics:
        errors.append("baseline metrics must be a non-empty object")
        checks["metrics_count"] = 0
    else:
        checks["metrics_count"] = len(metrics)

    environment = baseline_payload.get("environment")
    if not isinstance(environment, dict):
        errors.append("baseline environment fingerprint is required")
        checks["environment_keys"] = []
    else:
        env_keys = sorted(str(key) for key in environment.keys())
        checks["environment_keys"] = env_keys
        for required in ("os", "python"):
            value = environment.get(required)
            if not isinstance(value, str) or not value.strip():
                errors.append(f"baseline environment field '{required}' is required")
        recommended_keys = ("machine_class", "compiler", "cmake", "cxx_flags")
        for recommended in recommended_keys:
            value = environment.get(recommended)
            if not isinstance(value, str) or not value.strip():
                warnings.append(
                    f"baseline environment field '{recommended}' is recommended for stronger provenance"
                )

    manifest_path = baseline_path.with_name("artifact_manifest.json")
    checks["manifest_path"] = str(manifest_path)
    if not manifest_path.is_file():
        errors.append("artifact_manifest.json is required beside kpi_baseline.json")
        return {
            "status": "failed",
            "errors": errors,
            "warnings": warnings,
            "checks": checks,
        }

    manifest_payload = _load_json(manifest_path)
    manifest_schema = manifest_payload.get("schema_version")
    if manifest_schema != "pulsim-kpi-baseline-manifest-v1":
        errors.append("manifest schema_version must be 'pulsim-kpi-baseline-manifest-v1'")
    checks["manifest_schema_version"] = manifest_schema

    manifest_baseline_id = manifest_payload.get("baseline_id")
    if manifest_baseline_id != baseline_id:
        errors.append("baseline_id mismatch between baseline and manifest")
    checks["manifest_baseline_id"] = manifest_baseline_id

    manifest_captured = manifest_payload.get("captured_at_utc")
    manifest_captured_ts = _parse_utc_timestamp(manifest_captured)
    if manifest_captured_ts is None:
        errors.append("manifest captured_at_utc must be a valid ISO-8601 timestamp")
    elif captured_at_ts is not None and manifest_captured_ts != captured_at_ts:
        errors.append("captured_at_utc mismatch between baseline and manifest")
    checks["manifest_captured_at_utc"] = manifest_captured

    files = manifest_payload.get("files")
    if not isinstance(files, list) or not files:
        errors.append("manifest files must be a non-empty array")
        checks["manifest_file_count"] = 0
        return {
            "status": "failed",
            "errors": errors,
            "warnings": warnings,
            "checks": checks,
        }
    checks["manifest_file_count"] = len(files)

    source_bench_results = baseline_payload.get("source_bench_results")
    source_bench_results_resolved: Optional[Path] = None
    if isinstance(source_bench_results, str) and source_bench_results.strip():
        source_bench_results_resolved = _resolve_existing_path(
            source_bench_results.strip(),
            baseline_path,
        )
    checks["source_bench_results"] = source_bench_results
    checks["source_bench_results_resolved"] = (
        str(source_bench_results_resolved)
        if source_bench_results_resolved is not None
        else None
    )

    verified_files = 0
    missing_files = 0
    digest_mismatches = 0
    size_mismatches = 0
    eol_normalization_matches = 0
    bench_results_declared = 0

    for idx, entry in enumerate(files):
        if not isinstance(entry, dict):
            errors.append(f"manifest file entry {idx} must be an object")
            continue
        path_value = entry.get("path")
        sha_value = entry.get("sha256")
        size_value = entry.get("size_bytes")

        if not isinstance(path_value, str) or not path_value.strip():
            errors.append(f"manifest file entry {idx} path must be a non-empty string")
            continue
        if path_value.endswith("benchmarks/results.json"):
            bench_results_declared += 1

        if not isinstance(sha_value, str) or not re.fullmatch(r"[0-9a-f]{64}", sha_value):
            errors.append(f"manifest file entry {idx} has invalid sha256 value")
            continue

        if not isinstance(size_value, int) or size_value < 0:
            errors.append(f"manifest file entry {idx} size_bytes must be a non-negative integer")
            continue

        resolved = _resolve_existing_path(path_value, baseline_path)
        if resolved is None or not resolved.is_file():
            errors.append(f"manifest file entry {idx} does not resolve to an existing file")
            missing_files += 1
            continue

        actual_size = resolved.stat().st_size
        actual_sha = _sha256_file(resolved)
        if actual_size != size_value or actual_sha != sha_value:
            normalized_sha = _sha256_file_with_normalized_eol(resolved)
            if normalized_sha == sha_value:
                warnings.append(
                    f"manifest file entry {idx} newline normalization matched for '{path_value}'"
                )
                eol_normalization_matches += 1
                verified_files += 1
                continue

        if actual_size != size_value:
            errors.append(
                f"manifest file entry {idx} size mismatch for '{path_value}' "
                f"(expected={size_value}, actual={actual_size})"
            )
            size_mismatches += 1
            continue

        if actual_sha != sha_value:
            errors.append(
                f"manifest file entry {idx} sha256 mismatch for '{path_value}'"
            )
            digest_mismatches += 1
            continue

        verified_files += 1

    checks["verified_files"] = verified_files
    checks["missing_files"] = missing_files
    checks["digest_mismatches"] = digest_mismatches
    checks["size_mismatches"] = size_mismatches
    checks["eol_normalization_matches"] = eol_normalization_matches
    checks["bench_results_declared"] = bench_results_declared

    if bench_results_declared == 0:
        if source_bench_results_resolved is None:
            errors.append(
                "baseline must provide a resolvable source_bench_results when "
                "manifest lacks benchmarks/results.json"
            )
        else:
            warnings.append(
                "manifest does not include a benchmarks/results.json artifact entry; "
                "runtime scope uses source_bench_results"
            )

    return {
        "status": "failed" if errors else "passed",
        "errors": errors,
        "warnings": warnings,
        "checks": checks,
    }


def _case_key(row: Dict[str, Any]) -> Optional[Tuple[str, str]]:
    benchmark_id = row.get("benchmark_id")
    scenario = row.get("scenario")
    if not isinstance(benchmark_id, str) or not benchmark_id:
        return None
    if not isinstance(scenario, str) or not scenario:
        return None
    return (benchmark_id, scenario)


def _collect_case_keys(rows: list[Dict[str, Any]]) -> Set[Tuple[str, str]]:
    keys: Set[Tuple[str, str]] = set()
    for row in rows:
        key = _case_key(row)
        if key is not None:
            keys.add(key)
    return keys


def _resolve_existing_path(raw_path: str, baseline_path: Path) -> Optional[Path]:
    candidate = Path(raw_path)
    candidates: list[Path] = []

    if candidate.is_absolute():
        candidates.append(candidate)
    else:
        # Try common roots so CLI works from repo root or nested directories.
        candidates.append(Path.cwd() / candidate)
        candidates.append(baseline_path.parent / candidate)
        candidates.extend(parent / candidate for parent in baseline_path.parents)

    for path in candidates:
        resolved = path.resolve()
        if resolved.exists():
            return resolved
    return None


def _resolve_baseline_bench_results_path(
    baseline_payload: Dict[str, Any],
    baseline_path: Path,
) -> Optional[Path]:
    direct = baseline_payload.get("source_bench_results")
    if isinstance(direct, str) and direct.strip():
        resolved = _resolve_existing_path(direct.strip(), baseline_path)
        if resolved is not None and resolved.is_file():
            return resolved

    source_root = baseline_payload.get("source_artifacts_root")
    if isinstance(source_root, str) and source_root.strip():
        resolved_root = _resolve_existing_path(source_root.strip(), baseline_path)
        if resolved_root is not None:
            root_path = resolved_root if resolved_root.is_dir() else resolved_root.parent
            for suffix in ("benchmarks/results.json", "results.json"):
                candidate = (root_path / suffix).resolve()
                if candidate.is_file():
                    return candidate

    manifest_path = baseline_path.with_name("artifact_manifest.json")
    if manifest_path.is_file():
        manifest_payload = _load_json(manifest_path)
        for entry in manifest_payload.get("files", []):
            if not isinstance(entry, dict):
                continue
            path_value = entry.get("path")
            if not isinstance(path_value, str):
                continue
            if not path_value.endswith("benchmarks/results.json"):
                continue
            resolved = _resolve_existing_path(path_value, baseline_path)
            if resolved is not None and resolved.is_file():
                return resolved

    return None


def resolve_runtime_scope(
    baseline_payload: Dict[str, Any],
    baseline_path: Path,
    bench_results_path: Path,
) -> Tuple[Optional[Set[Tuple[str, str]]], Dict[str, Any]]:
    bench_payload = _load_json(bench_results_path)
    bench_results = bench_payload.get("results", [])
    executed = [
        row
        for row in bench_results
        if row.get("status") in ("passed", "failed") and isinstance(row, dict)
    ]
    executed_keys = _collect_case_keys(executed)

    scope_info: Dict[str, Any] = {
        "mode": "all_executed",
        "reason": "baseline_scope_unavailable",
        "executed_total": len(executed),
        "executed_with_ids": len(executed_keys),
        "baseline_scope_total": 0,
        "comparable_total": len(executed),
        "baseline_results_path": None,
    }

    baseline_results_path = _resolve_baseline_bench_results_path(baseline_payload, baseline_path)
    if baseline_results_path is None:
        return None, scope_info

    baseline_payload_results = _load_json(baseline_results_path)
    baseline_rows = baseline_payload_results.get("results", [])
    baseline_scope = [
        row
        for row in baseline_rows
        if row.get("status") in ("passed", "failed") and isinstance(row, dict)
    ]
    baseline_scope_keys = _collect_case_keys(baseline_scope)

    scope_info["baseline_results_path"] = str(baseline_results_path)
    scope_info["baseline_scope_total"] = len(baseline_scope_keys)

    if not baseline_scope_keys:
        scope_info["reason"] = "baseline_scope_has_no_case_ids"
        return None, scope_info

    comparable_keys = executed_keys.intersection(baseline_scope_keys)
    if comparable_keys:
        scope_info["mode"] = "baseline_intersection"
        scope_info["reason"] = "using_baseline_case_intersection"
        scope_info["comparable_total"] = len(comparable_keys)
        return comparable_keys, scope_info

    scope_info["reason"] = "no_intersection_between_current_and_baseline_cases"
    scope_info["comparable_total"] = len(executed)
    return None, scope_info


def compute_metrics(
    bench_results_path: Path,
    parity_ltspice_results_path: Optional[Path] = None,
    parity_ngspice_results_path: Optional[Path] = None,
    stress_summary_path: Optional[Path] = None,
    runtime_case_filter: Optional[Set[Tuple[str, str]]] = None,
    class_matrix_path: Optional[Path] = None,
) -> Dict[str, Optional[float]]:
    metrics: Dict[str, Optional[float]] = {}

    bench_payload = _load_json(bench_results_path)
    bench_results = bench_payload.get("results", [])
    executed = [
        row
        for row in bench_results
        if isinstance(row, dict) and row.get("status") in ("passed", "failed")
    ]

    passed = sum(1 for row in executed if row.get("status") == "passed")
    failed = sum(1 for row in executed if row.get("status") == "failed")
    denom = passed + failed
    metrics["convergence_success_rate"] = (float(passed) / float(denom)) if denom > 0 else None

    class_matrix = _load_class_matrix(class_matrix_path)

    runtime_rows = executed
    if runtime_case_filter is not None:
        runtime_rows = [
            row for row in executed
            if _case_key(row) in runtime_case_filter
        ]

    runtimes = [
        float(row["runtime_s"])
        for row in runtime_rows
        if row.get("runtime_s") is not None
    ]
    metrics["runtime_p50"] = _quantile(runtimes, 0.50)
    metrics["runtime_p95"] = _quantile(runtimes, 0.95)

    ac_rows: list[Dict[str, Any]] = []
    for row in executed:
        mode = row.get("mode")
        telemetry = row.get("telemetry")
        ac_flag = False
        if isinstance(telemetry, dict):
            value = telemetry.get("ac_sweep_case")
            if value is not None:
                try:
                    ac_flag = float(value) > 0.0
                except (TypeError, ValueError):
                    ac_flag = False
        if mode == "frequency_analysis" or ac_flag:
            ac_rows.append(row)

    ac_runtimes = [
        float(row["runtime_s"])
        for row in ac_rows
        if row.get("runtime_s") is not None
    ]
    metrics["ac_runtime_p95"] = _quantile(ac_runtimes, 0.95)

    state_space_primary_sum = 0.0
    dae_fallback_sum = 0.0
    state_space_total_sum = 0.0
    for row in executed:
        telemetry = row.get("telemetry")
        if not isinstance(telemetry, dict):
            continue
        primary = telemetry.get("state_space_primary_steps")
        fallback = telemetry.get("dae_fallback_steps")
        if primary is None or fallback is None:
            continue
        primary_f = float(primary)
        fallback_f = float(fallback)
        if not (primary_f >= 0.0 and fallback_f >= 0.0):
            continue
        state_space_primary_sum += primary_f
        dae_fallback_sum += fallback_f
        state_space_total_sum += primary_f + fallback_f
    metrics["state_space_primary_ratio"] = (
        state_space_primary_sum / state_space_total_sum
        if state_space_total_sum > 0.0
        else None
    )
    metrics["dae_fallback_ratio"] = (
        dae_fallback_sum / state_space_total_sum
        if state_space_total_sum > 0.0
        else None
    )

    def telemetry_values(metric_name: str, rows: Optional[list[Dict[str, Any]]] = None) -> list[float]:
        source_rows = executed if rows is None else rows
        values: list[float] = []
        for row in source_rows:
            telemetry = row.get("telemetry")
            if not isinstance(telemetry, dict):
                continue
            value = telemetry.get(metric_name)
            if value is None:
                continue
            numeric = float(value)
            if not math.isfinite(numeric):
                continue
            values.append(numeric)
        return values

    def telemetry_mean(metric_name: str, rows: Optional[list[Dict[str, Any]]] = None) -> Optional[float]:
        values = telemetry_values(metric_name, rows=rows)
        if not values:
            return None
        return float(sum(values) / len(values))

    def telemetry_flag(row: Dict[str, Any], key: str) -> bool:
        telemetry = row.get("telemetry")
        if not isinstance(telemetry, dict):
            return False
        value = telemetry.get(key)
        if value is None:
            return False
        try:
            return float(value) > 0.0
        except (TypeError, ValueError):
            return False

    def telemetry_float(row: Dict[str, Any], key: str) -> Optional[float]:
        telemetry = row.get("telemetry")
        if not isinstance(telemetry, dict):
            return None
        value = telemetry.get(key)
        if value is None:
            return None
        try:
            numeric = float(value)
        except (TypeError, ValueError):
            return None
        if not math.isfinite(numeric):
            return None
        return numeric

    typed_schema_keys = (
        "classified_fallback_events",
        "policy_dry_run_events",
        "policy_recommendation_matches",
        "policy_recommendation_mismatches",
        "anti_overfit_violations",
        "anti_overfit_budget_exceeded",
    )

    def has_typed_convergence_schema(row: Dict[str, Any]) -> bool:
        telemetry = row.get("telemetry")
        if not isinstance(telemetry, dict):
            return False
        for key in typed_schema_keys:
            if key not in telemetry:
                return False
            value = telemetry.get(key)
            if value is None:
                return False
            try:
                numeric = float(value)
            except (TypeError, ValueError):
                return False
            if not math.isfinite(numeric):
                return False
        return True

    if executed:
        typed_schema_rows = sum(1 for row in executed if has_typed_convergence_schema(row))
        metrics["typed_convergence_schema_coverage_rate"] = float(typed_schema_rows) / float(
            len(executed)
        )
    else:
        metrics["typed_convergence_schema_coverage_rate"] = None

    module_order_crc_values = telemetry_values("runtime_module_order_crc32")
    if module_order_crc_values:
        histogram: Dict[int, int] = {}
        for value in module_order_crc_values:
            key = int(round(value))
            histogram[key] = histogram.get(key, 0) + 1
        dominant_count = max(histogram.values()) if histogram else 0
        metrics["module_order_mismatch_rate"] = (
            float(len(module_order_crc_values) - dominant_count) / float(len(module_order_crc_values))
            if module_order_crc_values
            else None
        )
    else:
        metrics["module_order_mismatch_rate"] = None
    metrics["module_order_count_match_rate"] = telemetry_mean("runtime_module_count_match")

    output_reallocation_values = telemetry_values("output_reallocation_total")
    metrics["module_output_reallocation_p95"] = _quantile(output_reallocation_values, 0.95)
    metrics["module_output_reallocation_free_rate"] = telemetry_mean("output_reallocation_free")

    metrics["loss_energy_balance_error"] = telemetry_mean("loss_energy_balance_error")
    metrics["thermal_peak_temperature_delta"] = telemetry_mean("thermal_peak_temperature_delta")
    metrics["component_coverage_rate"] = telemetry_mean("component_coverage_rate")
    metrics["component_coverage_gap"] = telemetry_mean("component_coverage_gap")
    metrics["component_loss_summary_consistency_error"] = telemetry_mean(
        "component_loss_summary_consistency_error"
    )
    metrics["component_thermal_summary_consistency_error"] = telemetry_mean(
        "component_thermal_summary_consistency_error"
    )
    classified_fallback_values = telemetry_values("classified_fallback_events")
    metrics["classified_fallback_events_p95"] = _quantile(classified_fallback_values, 0.95)

    policy_dry_run_events_total = float(sum(telemetry_values("policy_dry_run_events")))
    policy_matches_total = float(sum(telemetry_values("policy_recommendation_matches")))
    policy_mismatches_total = float(sum(telemetry_values("policy_recommendation_mismatches")))
    anti_overfit_violations_total = float(sum(telemetry_values("anti_overfit_violations")))

    if policy_dry_run_events_total > 0.0:
        metrics["convergence_policy_match_rate"] = (
            policy_matches_total / policy_dry_run_events_total
        )
        metrics["convergence_policy_mismatch_rate"] = (
            policy_mismatches_total / policy_dry_run_events_total
        )
        metrics["anti_overfit_violation_rate"] = (
            anti_overfit_violations_total / policy_dry_run_events_total
        )
    else:
        metrics["convergence_policy_match_rate"] = None
        metrics["convergence_policy_mismatch_rate"] = None
        metrics["anti_overfit_violation_rate"] = None
    metrics["anti_overfit_budget_exceeded_rate"] = telemetry_mean("anti_overfit_budget_exceeded")

    if class_matrix:
        indexed_rows: Dict[Tuple[str, str], Dict[str, Any]] = {}
        for row in executed:
            key = _case_key(row)
            if key is None:
                continue
            # Deterministic last-write-wins if duplicate case keys appear.
            indexed_rows[key] = row

        class_observed_rows: Dict[str, list[Dict[str, Any]]] = {}
        for class_name, case_keys in class_matrix.items():
            slug = _sanitize_metric_slug(class_name)
            expected_total = float(len(case_keys))
            observed_rows: list[Dict[str, Any]] = []
            observed_keys = 0
            for key in sorted(case_keys):
                row = indexed_rows.get(key)
                if row is None:
                    continue
                observed_keys += 1
                observed_rows.append(row)
            class_observed_rows[class_name] = list(observed_rows)

            metrics[f"class_{slug}_case_count"] = expected_total
            metrics[f"class_{slug}_coverage_rate"] = (
                float(observed_keys) / expected_total if expected_total > 0.0 else None
            )

            if observed_rows:
                passed_rows = sum(1 for row in observed_rows if row.get("status") == "passed")
                metrics[f"class_{slug}_pass_rate"] = float(passed_rows) / float(len(observed_rows))
                class_runtimes = [
                    float(row["runtime_s"])
                    for row in observed_rows
                    if row.get("runtime_s") is not None
                ]
                metrics[f"class_{slug}_runtime_p95"] = _quantile(class_runtimes, 0.95)
                class_newton = telemetry_values("newton_iterations", rows=observed_rows)
                class_rejections = telemetry_values("timestep_rejections", rows=observed_rows)
                metrics[f"class_{slug}_newton_iterations_p95"] = _quantile(class_newton, 0.95)
                metrics[f"class_{slug}_timestep_rejections_p95"] = _quantile(class_rejections, 0.95)
                typed_rows = sum(1 for row in observed_rows if has_typed_convergence_schema(row))
                metrics[f"class_{slug}_typed_schema_coverage_rate"] = float(typed_rows) / float(
                    len(observed_rows)
                )
            else:
                metrics[f"class_{slug}_pass_rate"] = None
                metrics[f"class_{slug}_runtime_p95"] = None
                metrics[f"class_{slug}_newton_iterations_p95"] = None
                metrics[f"class_{slug}_timestep_rejections_p95"] = None
                metrics[f"class_{slug}_typed_schema_coverage_rate"] = None

        def unique_rows_for_classes(class_names: Set[str]) -> list[Dict[str, Any]]:
            selected: Dict[Tuple[str, str], Dict[str, Any]] = {}
            for class_name in class_names:
                for row in class_observed_rows.get(class_name, []):
                    key = _case_key(row)
                    if key is None:
                        continue
                    selected[key] = row
            return list(selected.values())

        target_classes = set(TARGET_POLICY_CLASS_SLUGS)
        stable_classes = {"diode_heavy"}
        target_rows = unique_rows_for_classes(target_classes)
        stable_rows = unique_rows_for_classes(stable_classes)

        if target_rows:
            target_passed = sum(1 for row in target_rows if row.get("status") == "passed")
            target_count = float(len(target_rows))
            target_failed = float(len(target_rows) - target_passed)
            metrics["policy_target_case_count"] = target_count
            metrics["policy_target_terminal_failure_count"] = target_failed
            metrics["policy_target_pass_rate"] = float(target_passed) / target_count
            metrics["policy_target_terminal_failure_rate"] = target_failed / target_count
            target_events = float(sum(telemetry_values("policy_dry_run_events", rows=target_rows)))
            target_matches = float(sum(telemetry_values("policy_recommendation_matches", rows=target_rows)))
            target_mismatches = float(
                sum(telemetry_values("policy_recommendation_mismatches", rows=target_rows))
            )
            if target_events > 0.0:
                metrics["policy_target_match_rate"] = target_matches / target_events
                metrics["policy_target_mismatch_rate"] = target_mismatches / target_events
            else:
                metrics["policy_target_match_rate"] = None
                metrics["policy_target_mismatch_rate"] = 0.0
        else:
            metrics["policy_target_case_count"] = None
            metrics["policy_target_terminal_failure_count"] = None
            metrics["policy_target_terminal_failure_rate"] = None
            metrics["policy_target_pass_rate"] = None
            metrics["policy_target_match_rate"] = None
            metrics["policy_target_mismatch_rate"] = None

        if stable_rows:
            stable_passed = sum(1 for row in stable_rows if row.get("status") == "passed")
            stable_count = float(len(stable_rows))
            stable_failed = float(len(stable_rows) - stable_passed)
            metrics["policy_stable_case_count"] = stable_count
            metrics["policy_stable_terminal_failure_count"] = stable_failed
            metrics["policy_stable_pass_rate"] = float(stable_passed) / stable_count
            metrics["policy_stable_terminal_failure_rate"] = stable_failed / stable_count
            stable_events = float(sum(telemetry_values("policy_dry_run_events", rows=stable_rows)))
            stable_mismatches = float(
                sum(telemetry_values("policy_recommendation_mismatches", rows=stable_rows))
            )
            stable_violations = float(
                sum(telemetry_values("anti_overfit_violations", rows=stable_rows))
            )
            if stable_events > 0.0:
                metrics["policy_stable_mismatch_rate"] = stable_mismatches / stable_events
                metrics["policy_stable_anti_overfit_violation_rate"] = (
                    stable_violations / stable_events
                )
            else:
                metrics["policy_stable_mismatch_rate"] = 0.0
                metrics["policy_stable_anti_overfit_violation_rate"] = 0.0
        else:
            metrics["policy_stable_case_count"] = None
            metrics["policy_stable_terminal_failure_count"] = None
            metrics["policy_stable_terminal_failure_rate"] = None
            metrics["policy_stable_pass_rate"] = None
            metrics["policy_stable_mismatch_rate"] = None
            metrics["policy_stable_anti_overfit_violation_rate"] = None

    metrics["ac_sweep_mag_error"] = telemetry_mean("ac_sweep_mag_error", rows=ac_rows)
    metrics["ac_sweep_phase_error"] = telemetry_mean("ac_sweep_phase_error", rows=ac_rows)

    averaged_pair_rows = [row for row in executed if telemetry_flag(row, "averaged_pair_case")]
    metrics["averaged_pair_case_count"] = float(len(averaged_pair_rows)) if averaged_pair_rows else None

    averaged_pair_fidelity_values: list[float] = []
    pair_runtime_groups: Dict[int, Dict[str, list[float]]] = {}
    for row in averaged_pair_rows:
        if telemetry_flag(row, "averaged_pair_role_averaged"):
            try:
                max_error_value = row.get("max_error")
                if max_error_value is not None:
                    fidelity_error = float(max_error_value)
                    if math.isfinite(fidelity_error):
                        averaged_pair_fidelity_values.append(fidelity_error)
            except (TypeError, ValueError):
                pass

        group_crc = telemetry_float(row, "averaged_pair_group_crc32")
        runtime_value = row.get("runtime_s")
        if group_crc is None or runtime_value is None:
            continue
        try:
            runtime_f = float(runtime_value)
        except (TypeError, ValueError):
            continue
        if not math.isfinite(runtime_f) or runtime_f <= 0.0:
            continue

        group_key = int(round(group_crc))
        group = pair_runtime_groups.setdefault(group_key, {"switching": [], "averaged": []})
        if telemetry_flag(row, "averaged_pair_role_switching"):
            group["switching"].append(runtime_f)
        if telemetry_flag(row, "averaged_pair_role_averaged"):
            group["averaged"].append(runtime_f)

    if averaged_pair_fidelity_values:
        metrics["averaged_pair_fidelity_error"] = float(
            sum(averaged_pair_fidelity_values) / len(averaged_pair_fidelity_values)
        )
    else:
        metrics["averaged_pair_fidelity_error"] = None

    speedup_values: list[float] = []
    for group in pair_runtime_groups.values():
        switching = group["switching"]
        averaged = group["averaged"]
        if not switching or not averaged:
            continue
        switching_mean = float(sum(switching) / len(switching))
        averaged_mean = float(sum(averaged) / len(averaged))
        if averaged_mean <= 0.0:
            continue
        speedup_values.append(switching_mean / averaged_mean)

    metrics["averaged_pair_runtime_speedup_min"] = min(speedup_values) if speedup_values else None
    metrics["averaged_pair_runtime_speedup_mean"] = (
        float(sum(speedup_values) / len(speedup_values)) if speedup_values else None
    )

    metrics["magnetic_sat_error"] = telemetry_mean("magnetic_sat_error")
    metrics["magnetic_hysteresis_cycle_energy_error"] = telemetry_mean(
        "magnetic_hysteresis_cycle_energy_error"
    )

    magnetic_rows = [row for row in executed if telemetry_flag(row, "magnetic_fixture_case")]
    magnetic_runtimes = [
        float(row["runtime_s"])
        for row in magnetic_rows
        if row.get("runtime_s") is not None
    ]
    metrics["magnetic_runtime_p95"] = _quantile(magnetic_runtimes, 0.95)

    magnetic_realloc_values = telemetry_values("output_reallocation_total", rows=magnetic_rows)
    metrics["magnetic_allocation_regression"] = _quantile(magnetic_realloc_values, 0.95)

    trend_rows = [row for row in executed if telemetry_flag(row, "magnetic_fixture_frequency_trend")]
    trend_groups: Dict[int, Dict[str, list[float]]] = {}
    for row in trend_rows:
        group_crc = telemetry_float(row, "magnetic_trend_group_crc32")
        avg_core_loss = telemetry_float(row, "magnetic_avg_core_loss")
        if group_crc is None or avg_core_loss is None:
            continue
        if avg_core_loss < 0.0:
            continue
        group_key = int(round(group_crc))
        group = trend_groups.setdefault(group_key, {"low": [], "high": []})
        if telemetry_flag(row, "magnetic_trend_role_low"):
            group["low"].append(avg_core_loss)
        if telemetry_flag(row, "magnetic_trend_role_high"):
            group["high"].append(avg_core_loss)

    trend_error_values: list[float] = []
    for group in trend_groups.values():
        lows = group["low"]
        highs = group["high"]
        if not lows or not highs:
            continue
        low_mean = float(sum(lows) / len(lows))
        high_mean = float(sum(highs) / len(highs))
        if low_mean <= 0.0:
            continue
        ratio = high_mean / low_mean
        trend_error_values.append(0.0 if ratio > 1.0 else (1.0 - ratio))

    metrics["magnetic_core_loss_trend_error"] = (
        float(sum(trend_error_values) / len(trend_error_values))
        if trend_error_values
        else None
    )

    determinism_rows = [row for row in executed if telemetry_flag(row, "magnetic_determinism_case")]
    determinism_values: list[float] = []
    for row in determinism_rows:
        value: Optional[float]
        raw = row.get("max_error")
        if raw is not None:
            try:
                value = float(raw)
            except (TypeError, ValueError):
                value = telemetry_float(row, "validation_max_error")
        else:
            value = telemetry_float(row, "validation_max_error")
        if value is None or not math.isfinite(value):
            continue
        determinism_values.append(value)

    metrics["magnetic_determinism_drift"] = (
        float(sum(determinism_values) / len(determinism_values))
        if determinism_values
        else None
    )

    def parity_mean_rms(path: Optional[Path]) -> Optional[float]:
        if path is None:
            return None
        payload = _load_json(path)
        values = [
            float(row["rms_error"])
            for row in payload.get("results", [])
            if row.get("status") == "passed" and row.get("rms_error") is not None
        ]
        if not values:
            return None
        return float(sum(values) / len(values))

    metrics["parity_rms_error_ltspice"] = parity_mean_rms(parity_ltspice_results_path)
    metrics["parity_rms_error_ngspice"] = parity_mean_rms(parity_ngspice_results_path)

    if stress_summary_path is not None:
        stress_summary = _load_json(stress_summary_path)
        tiers_failed = stress_summary.get("tiers_failed")
        metrics["stress_tier_failure_count"] = float(tiers_failed) if tiers_failed is not None else None
    else:
        metrics["stress_tier_failure_count"] = None

    return metrics


@dataclass
class MetricComparison:
    name: str
    status: str
    message: str
    current: Optional[float]
    baseline: Optional[float]
    direction: str
    regression_abs: Optional[float]
    regression_rel: Optional[float]
    max_regression_abs: Optional[float]
    max_regression_rel: Optional[float]
    required: bool


def compare_metric(
    name: str,
    current_value: Optional[float],
    baseline_value: Optional[float],
    rules: Dict[str, Any],
) -> MetricComparison:
    direction = str(rules.get("direction", "lower_is_better")).strip()
    required = bool(rules.get("required", True))
    max_reg_abs = rules.get("max_regression_abs")
    max_reg_rel = rules.get("max_regression_rel")
    rel_epsilon = float(rules.get("relative_epsilon", 1e-12))

    max_reg_abs = float(max_reg_abs) if max_reg_abs is not None else None
    max_reg_rel = float(max_reg_rel) if max_reg_rel is not None else None

    if current_value is None:
        status = "failed" if required else "skipped"
        message = "current metric unavailable"
        return MetricComparison(
            name=name,
            status=status,
            message=message,
            current=current_value,
            baseline=baseline_value,
            direction=direction,
            regression_abs=None,
            regression_rel=None,
            max_regression_abs=max_reg_abs,
            max_regression_rel=max_reg_rel,
            required=required,
        )

    if baseline_value is None:
        status = "failed" if required else "skipped"
        message = "baseline metric unavailable"
        return MetricComparison(
            name=name,
            status=status,
            message=message,
            current=current_value,
            baseline=baseline_value,
            direction=direction,
            regression_abs=None,
            regression_rel=None,
            max_regression_abs=max_reg_abs,
            max_regression_rel=max_reg_rel,
            required=required,
        )

    if direction == "higher_is_better":
        regression_abs = max(0.0, baseline_value - current_value)
    elif direction == "lower_is_better":
        regression_abs = max(0.0, current_value - baseline_value)
    else:
        return MetricComparison(
            name=name,
            status="failed",
            message=f"unknown direction '{direction}'",
            current=current_value,
            baseline=baseline_value,
            direction=direction,
            regression_abs=None,
            regression_rel=None,
            max_regression_abs=max_reg_abs,
            max_regression_rel=max_reg_rel,
            required=required,
        )

    # Near-zero baselines are dominated by floating-point noise; use an epsilon
    # guard so tiny absolute deltas do not trigger spurious relative regressions.
    if abs(baseline_value) <= rel_epsilon:
        regression_rel = 0.0 if regression_abs <= rel_epsilon else float("inf")
    else:
        regression_rel = regression_abs / abs(baseline_value)

    failures: list[str] = []
    if max_reg_abs is not None and regression_abs > max_reg_abs:
        failures.append(
            f"regression_abs={regression_abs:.6g} > max_regression_abs={max_reg_abs:.6g}"
        )
    if max_reg_rel is not None and regression_rel > max_reg_rel:
        failures.append(
            f"regression_rel={regression_rel:.6g} > max_regression_rel={max_reg_rel:.6g}"
        )

    if failures:
        status = "failed"
        message = "; ".join(failures)
    else:
        status = "passed"
        message = "within threshold"

    return MetricComparison(
        name=name,
        status=status,
        message=message,
        current=current_value,
        baseline=baseline_value,
        direction=direction,
        regression_abs=regression_abs,
        regression_rel=regression_rel,
        max_regression_abs=max_reg_abs,
        max_regression_rel=max_reg_rel,
        required=required,
    )


def run_gate(
    baseline_path: Path,
    thresholds_path: Path,
    bench_results_path: Path,
    parity_ltspice_results_path: Optional[Path],
    parity_ngspice_results_path: Optional[Path],
    stress_summary_path: Optional[Path],
    strict_provenance: bool = True,
    class_matrix_path: Optional[Path] = None,
    phase_budget_path: Optional[Path] = None,
    phase_budget_key: Optional[str] = None,
) -> Dict[str, Any]:
    baseline_payload = _load_json(baseline_path)
    threshold_payload = _load_thresholds(thresholds_path)
    phase_budget_metrics = _load_phase_budget_metrics(
        phase_budget_path=phase_budget_path,
        phase_budget_key=phase_budget_key,
    )
    if phase_budget_metrics:
        merged_threshold_payload = dict(threshold_payload)
        current_metrics = threshold_payload.get("metrics", {})
        if not isinstance(current_metrics, dict):
            current_metrics = {}
        merged_metrics = dict(current_metrics)
        merged_metrics.update(phase_budget_metrics)
        merged_threshold_payload["metrics"] = merged_metrics
        threshold_payload = merged_threshold_payload
    baseline_provenance = validate_baseline_provenance(
        baseline_payload=baseline_payload,
        baseline_path=baseline_path,
    )
    threshold_provenance = validate_threshold_policy(threshold_payload)
    blocked_by_provenance = (
        baseline_provenance["status"] == "failed" or threshold_provenance["status"] == "failed"
    )

    runtime_case_filter: Optional[Set[Tuple[str, str]]] = None
    runtime_scope: Dict[str, Any] = {
        "mode": "all_executed",
        "reason": "provenance_gate_blocked",
        "executed_total": 0,
        "executed_with_ids": 0,
        "baseline_scope_total": 0,
        "comparable_total": 0,
        "baseline_results_path": None,
    }
    if not blocked_by_provenance or not strict_provenance:
        runtime_case_filter, runtime_scope = resolve_runtime_scope(
            baseline_payload=baseline_payload,
            baseline_path=baseline_path,
            bench_results_path=bench_results_path,
        )

    baseline_metrics = baseline_payload.get("metrics", {})
    if not isinstance(baseline_metrics, dict):
        baseline_metrics = {}
    current_metrics = compute_metrics(
        bench_results_path=bench_results_path,
        parity_ltspice_results_path=parity_ltspice_results_path,
        parity_ngspice_results_path=parity_ngspice_results_path,
        stress_summary_path=stress_summary_path,
        runtime_case_filter=runtime_case_filter,
        class_matrix_path=class_matrix_path,
    )

    metric_rules = threshold_payload.get("metrics", {})
    if not isinstance(metric_rules, dict):
        metric_rules = {}
    comparisons: Dict[str, Dict[str, Any]] = {}

    baseline_env = baseline_payload.get("environment", {})
    if not isinstance(baseline_env, dict):
        baseline_env = {}
    baseline_machine_class_raw = baseline_env.get("machine_class")
    baseline_machine_class = (
        baseline_machine_class_raw.strip().lower()
        if isinstance(baseline_machine_class_raw, str)
        else ""
    )
    current_machine_class = (platform.machine() or "unknown").strip().lower()
    machine_class_match = (
        baseline_machine_class == current_machine_class
        if baseline_machine_class
        else None
    )

    runtime_sensitive_metrics = {
        "runtime_p50",
        "runtime_p95",
        "ac_runtime_p95",
        "averaged_pair_runtime_speedup_mean",
        "averaged_pair_runtime_speedup_min",
    }

    failed_required = 0
    skipped_optional = 0

    if not (blocked_by_provenance and strict_provenance):
        for metric_name, rules in metric_rules.items():
            if metric_name in runtime_sensitive_metrics and machine_class_match is False:
                comparisons[metric_name] = {
                    "status": "skipped",
                    "message": (
                        "runtime KPI skipped due to machine_class mismatch "
                        f"(baseline={baseline_machine_class_raw}, current={current_machine_class})"
                    ),
                    "current": current_metrics.get(metric_name),
                    "baseline": baseline_metrics.get(metric_name),
                    "direction": str((rules or {}).get("direction", "lower_is_better")).strip(),
                    "regression_abs": None,
                    "regression_rel": None,
                    "max_regression_abs": (rules or {}).get("max_regression_abs"),
                    "max_regression_rel": (rules or {}).get("max_regression_rel"),
                    "required": False,
                }
                skipped_optional += 1
                continue

            cmp = compare_metric(
                name=metric_name,
                current_value=current_metrics.get(metric_name),
                baseline_value=baseline_metrics.get(metric_name),
                rules=rules or {},
            )
            comparisons[metric_name] = {
                "status": cmp.status,
                "message": cmp.message,
                "current": cmp.current,
                "baseline": cmp.baseline,
                "direction": cmp.direction,
                "regression_abs": cmp.regression_abs,
                "regression_rel": cmp.regression_rel,
                "max_regression_abs": cmp.max_regression_abs,
                "max_regression_rel": cmp.max_regression_rel,
                "required": cmp.required,
            }
            if cmp.status == "failed" and cmp.required:
                failed_required += 1
            if cmp.status == "skipped" and not cmp.required:
                skipped_optional += 1

        if phase_budget_key == "gate_c":
            gate_c_cmp = evaluate_gate_c_target_failure_drop_significance(
                baseline_metrics=baseline_metrics,
                current_metrics=current_metrics,
            )
            comparisons[gate_c_cmp["metric"]] = gate_c_cmp
            if gate_c_cmp["status"] == "failed" and gate_c_cmp["required"]:
                failed_required += 1

    overall_status = "failed" if failed_required > 0 else "passed"
    if blocked_by_provenance and strict_provenance:
        overall_status = "failed"

    report = {
        "schema_version": "pulsim-kpi-gate-report-v1",
        "evaluated_at_utc": datetime.now(timezone.utc).isoformat(),
        "baseline_id": baseline_payload.get("baseline_id"),
        "class_matrix_path": str(class_matrix_path) if class_matrix_path is not None else None,
        "phase_budget_path": str(phase_budget_path) if phase_budget_path is not None else None,
        "phase_budget_key": phase_budget_key,
        "overall_status": overall_status,
        "failed_required_metrics": failed_required,
        "skipped_optional_metrics": skipped_optional,
        "current_metrics": current_metrics,
        "runtime_scope": runtime_scope,
        "comparisons": comparisons,
        "blocked_by_provenance": blocked_by_provenance and strict_provenance,
        "provenance": {
            "strict_mode": strict_provenance,
            "baseline": baseline_provenance,
            "thresholds": threshold_provenance,
            "environment_compatibility": {
                "baseline_machine_class": baseline_machine_class_raw,
                "current_machine_class": current_machine_class,
                "machine_class_match": machine_class_match,
            },
        },
    }
    return report


def main() -> int:
    parser = argparse.ArgumentParser(description="Enforce KPI regression thresholds")
    parser.add_argument(
        "--baseline",
        type=Path,
        default=Path("benchmarks/kpi_baselines/modular_runtime_phase13_2026-03-07/kpi_baseline.json"),
    )
    parser.add_argument(
        "--thresholds",
        type=Path,
        default=Path("benchmarks/kpi_thresholds.yaml"),
    )
    parser.add_argument("--bench-results", type=Path, required=True)
    parser.add_argument("--parity-ltspice-results", type=Path)
    parser.add_argument("--parity-ngspice-results", type=Path)
    parser.add_argument("--stress-summary", type=Path)
    parser.add_argument(
        "--class-matrix",
        type=Path,
        help="Optional convergence class matrix YAML for per-class KPI derivation",
    )
    parser.add_argument(
        "--phase-budget",
        type=Path,
        help="Optional phase budget YAML with additional KPI threshold rules",
    )
    parser.add_argument(
        "--phase-key",
        help="Phase key in --phase-budget (for example: gate_a, gate_b, gate_c)",
    )
    parser.add_argument("--report-out", type=Path)
    parser.add_argument("--print-report", action="store_true")
    parser.add_argument(
        "--no-strict-provenance",
        action="store_true",
        help="Allow metric comparison even when baseline/threshold provenance checks fail",
    )
    args = parser.parse_args()

    report = run_gate(
        baseline_path=args.baseline,
        thresholds_path=args.thresholds,
        bench_results_path=args.bench_results,
        parity_ltspice_results_path=args.parity_ltspice_results,
        parity_ngspice_results_path=args.parity_ngspice_results,
        stress_summary_path=args.stress_summary,
        strict_provenance=not args.no_strict_provenance,
        class_matrix_path=args.class_matrix,
        phase_budget_path=args.phase_budget,
        phase_budget_key=args.phase_key,
    )

    if args.report_out is not None:
        args.report_out.parent.mkdir(parents=True, exist_ok=True)
        with open(args.report_out, "w", encoding="utf-8") as handle:
            json.dump(report, handle, indent=2, sort_keys=True)
            handle.write("\n")

    if args.print_report:
        print(json.dumps(report, indent=2, sort_keys=True))

    return 1 if report["overall_status"] == "failed" else 0


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