#!/usr/bin/env python3
"""运控标定现场数据配置、检查和数据集索引工具。"""

from __future__ import annotations

import csv
import json
import math
import shutil
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml


CONTROL_CSV_CONTRACT_VERSION = 1
CHASSIS_TYPES = {"ackermann", "differential", "single_steer_wheel", "multi_steer_wheel"}
CONTROL_AXES = {"lateral_control", "longitudinal_control", "combined"}
CONTROLLER_ALGORITHMS = {"pid", "mpc", "lqr", "pure_pursuit"}
CONTROL_ROLES = {
    "path_tracking_outer_loop",
    "speed_loop",
    "acceleration_loop",
    "heading_loop",
    "yaw_rate_loop",
    "steering_angle_inner_loop",
    "module_steering_inner_loop",
    "wheel_speed_inner_loop",
    "multi_loop_pid",
}
TASK_TYPES = {
    "trajectory_tracking",
    "velocity_step",
    "acceleration_deceleration",
    "stop_accuracy",
}
PLACEHOLDER_MARKERS = ("replace_with", "measured_on_site", "session_xxx")

CONTROL_EVALUATION_FIELDS = [
    "hardware_timestamp_us",
    "chassis_type",
    "task_type",
    "control_axis",
    "controller_algorithm",
    "control_role",
    "target_x_m",
    "target_y_m",
    "target_yaw_rad",
    "target_velocity_ms",
    "measured_x_m",
    "measured_y_m",
    "measured_yaw_rad",
    "measured_velocity_ms",
    "lateral_error_m",
    "heading_error_rad",
    "speed_error_ms",
    "control_output_steer_rad",
    "control_output_accel_ms2",
    "control_output_brake",
    "saturation_flag",
    "active_job_id",
    "diagnostics_json",
]

REFERENCE_SIGNAL_FIELDS = [
    "reference_timestamp_us",
    "task_type",
    "sequence_index",
    "target_x_m",
    "target_y_m",
    "target_yaw_rad",
    "target_velocity_ms",
    "target_accel_ms2",
    "curvature_1pm",
    "stop_required",
    "active_job_id",
]

CHASSIS_RESPONSE_FIELDS = [
    "hardware_timestamp_us",
    "odom_x_m",
    "odom_y_m",
    "odom_yaw_rad",
    "linear_velocity_ms",
    "angular_velocity_rads",
    "steering_angle_rad",
    "accel_ms2",
    "brake_state",
    "driver_error_code",
    "active_job_id",
]

TRUTH_TRAJECTORY_FIELDS = [
    "hardware_timestamp_us",
    "pose_valid",
    "x_m",
    "y_m",
    "z_m",
    "roll_rad",
    "pitch_rad",
    "yaw_rad",
    "position_stddev_m",
    "yaw_stddev_rad",
    "tracking_loss_ratio",
    "time_sync_offset_ms",
    "quality_score",
    "observed_target_count",
    "reference_source_name",
    "active_job_id",
]

INTEGER_FIELDS_BY_TABLE = {
    "control_evaluation": {"hardware_timestamp_us"},
    "reference_signal": {"reference_timestamp_us", "sequence_index"},
    "chassis_response": {"hardware_timestamp_us", "driver_error_code"},
    "truth_trajectory": {"hardware_timestamp_us", "observed_target_count"},
}

FLOAT_FIELDS_BY_TABLE = {
    "control_evaluation": {
        "target_x_m",
        "target_y_m",
        "target_yaw_rad",
        "target_velocity_ms",
        "measured_x_m",
        "measured_y_m",
        "measured_yaw_rad",
        "measured_velocity_ms",
        "lateral_error_m",
        "heading_error_rad",
        "speed_error_ms",
        "control_output_steer_rad",
        "control_output_accel_ms2",
        "control_output_brake",
    },
    "reference_signal": {
        "target_x_m",
        "target_y_m",
        "target_yaw_rad",
        "target_velocity_ms",
        "target_accel_ms2",
        "curvature_1pm",
    },
    "chassis_response": {
        "odom_x_m",
        "odom_y_m",
        "odom_yaw_rad",
        "linear_velocity_ms",
        "angular_velocity_rads",
        "steering_angle_rad",
        "accel_ms2",
    },
    "truth_trajectory": {
        "x_m",
        "y_m",
        "z_m",
        "roll_rad",
        "pitch_rad",
        "yaw_rad",
        "position_stddev_m",
        "yaw_stddev_rad",
        "tracking_loss_ratio",
        "time_sync_offset_ms",
        "quality_score",
    },
}

BOOLEAN_FIELDS_BY_TABLE = {
    "control_evaluation": {"saturation_flag"},
    "reference_signal": {"stop_required"},
    "truth_trajectory": {"pose_valid"},
}

CSV_CONTRACTS = {
    "control_evaluation": {
        "fields": CONTROL_EVALUATION_FIELDS,
        "json_fields": {"diagnostics_json"},
    },
    "reference_signal": {
        "fields": REFERENCE_SIGNAL_FIELDS,
        "json_fields": set(),
    },
    "chassis_response": {
        "fields": CHASSIS_RESPONSE_FIELDS,
        "json_fields": set(),
    },
    "truth_trajectory": {
        "fields": TRUTH_TRAJECTORY_FIELDS,
        "json_fields": set(),
    },
}


def now_us() -> int:
    return int(time.time() * 1_000_000)


def load_config(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8") as stream:
        data = yaml.safe_load(stream) or {}
    if not isinstance(data, dict):
        raise ValueError(f"配置文件必须是 YAML 字典: {path}")
    return data


def write_yaml(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")


def has_placeholder(value: Any) -> bool:
    return isinstance(value, str) and any(marker in value for marker in PLACEHOLDER_MARKERS)


def relative_to_root(path: Path, root: Path) -> str:
    try:
        return str(path.resolve(strict=False).relative_to(root.resolve(strict=False)))
    except ValueError:
        return str(path.resolve(strict=False))


def resolve_path(root: Path, raw_value: str) -> Path:
    path = Path(raw_value).expanduser()
    if path.is_absolute():
        return path
    return root / path


@dataclass
class ValidationResult:
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    @property
    def ok(self) -> bool:
        return not self.errors

    def extend(self, other: "ValidationResult") -> None:
        self.errors.extend(other.errors)
        self.warnings.extend(other.warnings)


def is_bool_token(value: Any) -> bool:
    return str(value).strip().lower() in {"0", "1", "true", "false"}


def validate_csv_contract(path: Path, table_name: str) -> ValidationResult:
    result = ValidationResult()
    contract = CSV_CONTRACTS.get(table_name)
    if contract is None:
        result.errors.append(f"未知 CSV 协议名称: {table_name}")
        return result
    if not path.exists():
        result.errors.append(f"{table_name} 文件不存在: {path}")
        return result

    required_fields = list(contract["fields"])
    integer_fields = INTEGER_FIELDS_BY_TABLE.get(table_name, set())
    float_fields = FLOAT_FIELDS_BY_TABLE.get(table_name, set())
    boolean_fields = BOOLEAN_FIELDS_BY_TABLE.get(table_name, set())
    json_fields = contract.get("json_fields", set())

    with path.open("r", newline="", encoding="utf-8") as stream:
        reader = csv.DictReader(stream)
        fieldnames = reader.fieldnames or []
        for field_name in required_fields:
            if field_name not in fieldnames:
                result.errors.append(f"{table_name} 缺少必需列: {field_name}")
        if result.errors:
            return result

        row_count = 0
        for row_number, row in enumerate(reader, start=2):
            row_count += 1
            for field_name in integer_fields:
                try:
                    int(str(row.get(field_name, "")).strip())
                except (TypeError, ValueError):
                    result.errors.append(f"{table_name} 第 {row_number} 行 {field_name} 必须是整数。")
            for field_name in float_fields:
                try:
                    float(str(row.get(field_name, "")).strip())
                except (TypeError, ValueError):
                    result.errors.append(f"{table_name} 第 {row_number} 行 {field_name} 必须是浮点数。")
            for field_name in boolean_fields:
                if not is_bool_token(row.get(field_name, "")):
                    result.errors.append(f"{table_name} 第 {row_number} 行 {field_name} 必须是 0/1 或 true/false。")
            for field_name in json_fields:
                try:
                    parsed = json.loads(str(row.get(field_name, "")).strip())
                except json.JSONDecodeError:
                    result.errors.append(f"{table_name} 第 {row_number} 行 {field_name} 必须是合法 JSON。")
                    continue
                if field_name == "diagnostics_json" and not isinstance(parsed, dict):
                    result.errors.append(f"{table_name} 第 {row_number} 行 {field_name} 必须是 JSON 对象。")
            if table_name == "control_evaluation":
                if str(row.get("chassis_type", "")).strip() not in CHASSIS_TYPES:
                    result.errors.append(f"{table_name} 第 {row_number} 行 chassis_type 必须是已支持的底盘类型。")
                row_control_axis = str(row.get("control_axis", "")).strip()
                row_controller_algorithm = str(row.get("controller_algorithm", "")).strip()
                if row_control_axis not in CONTROL_AXES:
                    result.errors.append(f"{table_name} 第 {row_number} 行 control_axis 必须是已支持的控制轴。")
                if row_controller_algorithm not in CONTROLLER_ALGORITHMS:
                    result.errors.append(f"{table_name} 第 {row_number} 行 controller_algorithm 必须是已支持的控制算法。")
                if str(row.get("control_role", "")).strip() not in CONTROL_ROLES:
                    result.errors.append(f"{table_name} 第 {row_number} 行 control_role 必须是已支持的控制回路角色。")
            if table_name in {"control_evaluation", "reference_signal"}:
                if str(row.get("task_type", "")).strip() not in TASK_TYPES:
                    result.errors.append(f"{table_name} 第 {row_number} 行 task_type 必须是已支持的运控评估任务。")
        if row_count == 0:
            result.warnings.append(f"{table_name} 只有表头，没有数据行。")
    return result


def validate_control_dataset_files(paths: dict[str, Path]) -> ValidationResult:
    result = ValidationResult()
    checks = [
        ("control_evaluation", "control_evaluation_data_file"),
        ("reference_signal", "reference_signal_file"),
        ("chassis_response", "chassis_response_file"),
        ("truth_trajectory", "truth_trajectory_file"),
    ]
    for table_name, path_key in checks:
        result.extend(validate_csv_contract(paths[path_key], table_name))
    return result


def validate_config(config: dict[str, Any], config_path: Path | None = None) -> ValidationResult:
    result = ValidationResult()

    csv_contract_version = int(config.get("csv_contract_version", CONTROL_CSV_CONTRACT_VERSION) or 0)
    if csv_contract_version != CONTROL_CSV_CONTRACT_VERSION:
        result.errors.append(f"csv_contract_version 必须为 {CONTROL_CSV_CONTRACT_VERSION}。")

    if str(config.get("chassis_type", "")).strip() not in CHASSIS_TYPES:
        result.errors.append(f"chassis_type 必须是 {sorted(CHASSIS_TYPES)} 之一。")
    if str(config.get("control_axis", "")).strip() not in CONTROL_AXES:
        result.errors.append(f"control_axis 必须是 {sorted(CONTROL_AXES)} 之一。")
    if str(config.get("controller_algorithm", "")).strip() not in CONTROLLER_ALGORITHMS:
        result.errors.append(f"controller_algorithm 必须是 {sorted(CONTROLLER_ALGORITHMS)} 之一。")
    if str(config.get("control_role", "")).strip() not in CONTROL_ROLES:
        result.errors.append(f"control_role 必须是 {sorted(CONTROL_ROLES)} 之一。")

    for field_name in ("session_id", "site_id", "vehicle_id", "session_dir", "dataset_index_path"):
        value = str(config.get(field_name, "")).strip()
        if not value:
            result.errors.append(f"{field_name} 不能为空。")
        elif has_placeholder(value):
            result.errors.append(f"{field_name} 不能保留模板占位符。")

    files = config.get("files", {})
    if not isinstance(files, dict):
        result.errors.append("files 必须是 YAML 字典。")
        files = {}
    for field_name in (
        "control_evaluation_data_file",
        "reference_signal_file",
        "chassis_response_file",
        "truth_trajectory_file",
        "diagnostics_file",
    ):
        value = str(files.get(field_name, "")).strip()
        if not value:
            result.errors.append(f"files.{field_name} 不能为空。")
        elif has_placeholder(value):
            result.errors.append(f"files.{field_name} 不能保留模板占位符。")

    validation = config.get("validation", {}) or {}
    if not isinstance(validation, dict):
        result.errors.append("validation 必须是 YAML 字典。")
        validation = {}
    for field_name in (
        "min_control_evaluation_samples",
        "min_reference_signal_samples",
        "min_chassis_response_samples",
        "min_truth_trajectory_samples",
    ):
        if int(validation.get(field_name, 0) or 0) <= 0:
            result.errors.append(f"validation.{field_name} 必须大于 0。")
    if float(validation.get("max_time_gap_ms", 0.0) or 0.0) <= 0.0:
        result.errors.append("validation.max_time_gap_ms 必须大于 0。")

    if config_path is not None and not config_path.exists():
        result.errors.append(f"配置文件不存在: {config_path}")
    return result


def apply_cli_overrides(config: dict[str, Any], args: Any) -> None:
    for attr_name in ("session_id", "site_id", "vehicle_id", "session_dir", "dataset_index_path"):
        value = getattr(args, attr_name, "")
        if value:
            config[attr_name] = value
    if getattr(args, "control_axis", ""):
        config["control_axis"] = args.control_axis
    if getattr(args, "chassis_type", ""):
        config["chassis_type"] = args.chassis_type
    if getattr(args, "controller_algorithm", ""):
        config["controller_algorithm"] = args.controller_algorithm
    if getattr(args, "control_role", ""):
        config["control_role"] = args.control_role


def read_csv_rows(path: Path) -> list[dict[str, str]]:
    with path.open("r", newline="", encoding="utf-8") as stream:
        return list(csv.DictReader(stream))


def timestamp_us(row: dict[str, Any]) -> int:
    for key in ("hardware_timestamp_us", "reference_timestamp_us", "timestamp_us"):
        value = row.get(key)
        if value not in (None, ""):
            return int(float(value))
    return 0


def numeric(row: dict[str, Any], key: str, default: float = 0.0) -> float:
    value = row.get(key)
    if value in (None, ""):
        return default
    return float(value)


def rms(rows: list[dict[str, str]], key: str) -> float:
    if not rows:
        return 0.0
    return math.sqrt(sum(numeric(row, key) ** 2 for row in rows) / len(rows))


def max_timestamp_gap_ms(rows: list[dict[str, str]]) -> float:
    stamps = sorted(timestamp_us(row) for row in rows if timestamp_us(row) > 0)
    if len(stamps) < 2:
        return 0.0
    return max((b - a) / 1000.0 for a, b in zip(stamps, stamps[1:]))


def boolish(value: Any) -> bool:
    return str(value).strip().lower() in {"1", "true"}


def write_csv(path: Path, fieldnames: list[str], rows: list[dict[str, Any]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", newline="", encoding="utf-8") as stream:
        writer = csv.DictWriter(stream, fieldnames=fieldnames)
        writer.writeheader()
        for row in rows:
            writer.writerow(row)


def copy_or_generate_file(source: str, target: Path, fieldnames: list[str], rows: list[dict[str, Any]]) -> Path:
    target.parent.mkdir(parents=True, exist_ok=True)
    if source:
        shutil.copyfile(Path(source).expanduser().resolve(strict=False), target)
    else:
        write_csv(target, fieldnames, rows)
    return target


def synthetic_rows(
    config: dict[str, Any],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
    synthetic = config.get("synthetic", {}) or {}
    start_us = now_us()
    period_us = int(float(synthetic.get("sample_period_ms", 50.0) or 50.0) * 1000.0)
    duration_sec = float(synthetic.get("duration_sec", 6.0) or 6.0)
    target_speed = float(synthetic.get("target_speed_ms", 0.2) or 0.2)
    lateral_error = float(synthetic.get("lateral_error_m", 0.015) or 0.015)
    heading_error = float(synthetic.get("heading_error_rad", 0.01) or 0.01)
    speed_error = float(synthetic.get("speed_error_ms", 0.02) or 0.02)
    sample_count = max(3, int(duration_sec / max(period_us / 1e6, 1e-6)) + 1)

    task_type = str(synthetic.get("task_type", "trajectory_tracking") or "trajectory_tracking")
    chassis_type = str(config.get("chassis_type", "ackermann") or "ackermann")
    control_axis = str(config.get("control_axis", "combined") or "combined")
    algorithm = str(config.get("controller_algorithm", "mpc") or "mpc")
    control_role = str(config.get("control_role", "path_tracking_outer_loop") or "path_tracking_outer_loop")
    active_job_id = str(config.get("session_id", "session") or "session")

    control_rows: list[dict[str, Any]] = []
    reference_rows: list[dict[str, Any]] = []
    chassis_rows: list[dict[str, Any]] = []
    truth_rows: list[dict[str, Any]] = []

    for index in range(sample_count):
        stamp = start_us + index * period_us
        t_sec = index * period_us / 1e6
        target_x = target_speed * t_sec
        target_y = 0.0
        measured_x = target_x - speed_error * t_sec
        measured_y = lateral_error
        measured_yaw = heading_error
        measured_speed = max(0.0, target_speed - speed_error)

        control_rows.append({
            "hardware_timestamp_us": stamp,
            "chassis_type": chassis_type,
            "task_type": task_type,
            "control_axis": control_axis,
            "controller_algorithm": algorithm,
            "control_role": control_role,
            "target_x_m": target_x,
            "target_y_m": target_y,
            "target_yaw_rad": 0.0,
            "target_velocity_ms": target_speed,
            "measured_x_m": measured_x,
            "measured_y_m": measured_y,
            "measured_yaw_rad": measured_yaw,
            "measured_velocity_ms": measured_speed,
            "lateral_error_m": lateral_error,
            "heading_error_rad": heading_error,
            "speed_error_ms": speed_error,
            "control_output_steer_rad": 0.02,
            "control_output_accel_ms2": 0.0,
            "control_output_brake": 0.0,
            "saturation_flag": 0,
            "active_job_id": active_job_id,
            "diagnostics_json": "{}",
        })
        reference_rows.append({
            "reference_timestamp_us": stamp,
            "task_type": task_type,
            "sequence_index": index,
            "target_x_m": target_x,
            "target_y_m": target_y,
            "target_yaw_rad": 0.0,
            "target_velocity_ms": target_speed,
            "target_accel_ms2": 0.0,
            "curvature_1pm": 0.0,
            "stop_required": 0,
            "active_job_id": active_job_id,
        })
        chassis_rows.append({
            "hardware_timestamp_us": stamp,
            "odom_x_m": measured_x,
            "odom_y_m": measured_y,
            "odom_yaw_rad": measured_yaw,
            "linear_velocity_ms": measured_speed,
            "angular_velocity_rads": 0.0,
            "steering_angle_rad": 0.02,
            "accel_ms2": 0.0,
            "brake_state": 0,
            "driver_error_code": 0,
            "active_job_id": active_job_id,
        })
        truth_rows.append({
            "hardware_timestamp_us": stamp,
            "pose_valid": 1,
            "x_m": measured_x,
            "y_m": measured_y,
            "z_m": 0.0,
            "roll_rad": 0.0,
            "pitch_rad": 0.0,
            "yaw_rad": measured_yaw,
            "position_stddev_m": 0.005,
            "yaw_stddev_rad": 0.002,
            "tracking_loss_ratio": 0.0,
            "time_sync_offset_ms": 0.0,
            "quality_score": 1.0,
            "observed_target_count": 4,
            "reference_source_name": "synthetic_control_smoke",
            "active_job_id": active_job_id,
        })

    return control_rows, reference_rows, chassis_rows, truth_rows


def build_summary(
    control_rows: list[dict[str, str]],
    reference_rows: list[dict[str, str]],
    chassis_rows: list[dict[str, str]],
    truth_rows: list[dict[str, str]],
    config: dict[str, Any],
) -> dict[str, Any]:
    stamps = [
        timestamp_us(row)
        for row in control_rows + reference_rows + chassis_rows + truth_rows
        if timestamp_us(row) > 0
    ]
    start_us = min(stamps) if stamps else now_us()
    end_us = max(stamps) if stamps else start_us + 1
    saturation_count = sum(1 for row in control_rows if boolish(row.get("saturation_flag", "0")))
    return {
        "control_csv_contract_version": CONTROL_CSV_CONTRACT_VERSION,
        "session_id": config.get("session_id", ""),
        "site_id": config.get("site_id", ""),
        "vehicle_id": config.get("vehicle_id", ""),
        "chassis_type": config.get("chassis_type", ""),
        "control_axis": config.get("control_axis", ""),
        "controller_algorithm": config.get("controller_algorithm", ""),
        "control_role": config.get("control_role", ""),
        "data_window_start_timestamp_us": int(start_us),
        "data_window_end_timestamp_us": int(max(end_us, start_us + 1)),
        "control_evaluation_sample_count": len(control_rows),
        "reference_signal_sample_count": len(reference_rows),
        "chassis_response_sample_count": len(chassis_rows),
        "truth_trajectory_sample_count": len(truth_rows),
        "rms_lateral_error_m": rms(control_rows, "lateral_error_m"),
        "rms_heading_error_rad": rms(control_rows, "heading_error_rad"),
        "rms_speed_error_ms": rms(control_rows, "speed_error_ms"),
        "saturation_ratio": saturation_count / len(control_rows) if control_rows else 0.0,
        "max_control_timestamp_gap_ms": max_timestamp_gap_ms(control_rows),
        "max_truth_timestamp_gap_ms": max_timestamp_gap_ms(truth_rows),
    }


def validate_summary(summary: dict[str, Any], config: dict[str, Any]) -> ValidationResult:
    result = ValidationResult()
    validation = config.get("validation", {}) or {}
    checks = [
        ("control_evaluation_sample_count", "min_control_evaluation_samples", "运控评估样本数不足"),
        ("reference_signal_sample_count", "min_reference_signal_samples", "参考信号样本数不足"),
        ("chassis_response_sample_count", "min_chassis_response_samples", "底盘响应样本数不足"),
        ("truth_trajectory_sample_count", "min_truth_trajectory_samples", "真值轨迹样本数不足"),
    ]
    for summary_key, config_key, message in checks:
        if int(summary.get(summary_key, 0)) < int(validation.get(config_key, 0) or 0):
            result.errors.append(f"{message}: {summary.get(summary_key, 0)}")
    max_gap = float(validation.get("max_time_gap_ms", 200.0) or 200.0)
    if float(summary.get("max_control_timestamp_gap_ms", 0.0)) > max_gap:
        result.warnings.append("运控评估数据时间间隔超过配置阈值。")
    if float(summary.get("max_truth_timestamp_gap_ms", 0.0)) > max_gap:
        result.warnings.append("真值轨迹时间间隔超过配置阈值。")
    return result


def build_dataset_index(
    config: dict[str, Any],
    summary: dict[str, Any],
    paths: dict[str, Path],
    config_path: Path,
) -> dict[str, Any]:
    session_dir = Path(str(config["session_dir"])).expanduser().resolve(strict=False)
    return {
        "schema_version": 1,
        "session": {
            "session_id": str(config["session_id"]),
            "site_id": str(config["site_id"]),
            "vehicle_id": str(config["vehicle_id"]),
            "dataset_root": str(session_dir),
            "data_window_start_timestamp_us": int(summary["data_window_start_timestamp_us"]),
            "data_window_end_timestamp_us": int(summary["data_window_end_timestamp_us"]),
        },
        "data_inputs": {
            "control": {
                "control_evaluation_data_files": [
                    relative_to_root(paths["control_evaluation_data_file"], session_dir)
                ],
                "reference_signal_files": [
                    relative_to_root(paths["reference_signal_file"], session_dir)
                ],
                "chassis_response_files": [
                    relative_to_root(paths["chassis_response_file"], session_dir)
                ],
                "truth_trajectory_files": [
                    relative_to_root(paths["truth_trajectory_file"], session_dir)
                ],
            }
        },
        "metadata": {
            "control_csv_contract_version": CONTROL_CSV_CONTRACT_VERSION,
            "control_data_config_file": str(config_path),
            "control_diagnostics_file": relative_to_root(paths["diagnostics_file"], session_dir),
            "control_replay_summary": summary,
        },
    }
