#!/usr/bin/env python3
"""收尾真实标定会话，并生成算法服务数据输入参数文件。"""

from __future__ import annotations

import argparse
import importlib.util
import sys
from pathlib import Path
from typing import Any

import yaml

from dataset_index_to_site_data_input import (
    build_ros_params,
    load_yaml,
    validate_output,
)


REPO_ROOT = Path(__file__).resolve().parents[3]
CHASSIS_COMMIT_TOOL = (
    REPO_ROOT
    / "src"
    / "site_deployment"
    / "workshop_chassis_calibration_real"
    / "stage_chassis_parameter_commit.py"
)
CONTROL_COMMIT_TOOL = (
    REPO_ROOT
    / "src"
    / "site_deployment"
    / "workshop_control_calibration_real"
    / "stage_control_parameter_commit.py"
)
SENSOR_COMMIT_TOOL = (
    REPO_ROOT
    / "src"
    / "site_deployment"
    / "workshop_sensor_calibration_real"
    / "stage_sensor_parameter_commit.py"
)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="根据现场 profile 和本次会话数据集索引生成 site_data_input.yaml。"
    )
    parser.add_argument(
        "--site-profile",
        default="src/deployment/profiles/site_template.yaml",
        help="现场部署 profile 路径。",
    )
    parser.add_argument(
        "--session-id",
        help="本次标定会话 ID；未提供 --session-dir 时，会拼到 dataset_root 后面。",
    )
    parser.add_argument(
        "--session-dir",
        help="本次标定会话目录；优先级高于 --session-id。",
    )
    parser.add_argument(
        "--dataset-index",
        help="本次会话 dataset_index.yaml 路径；优先级高于 profile 中的约定。",
    )
    parser.add_argument(
        "-o",
        "--output",
        help="输出的 site_data_input.yaml 路径；默认写入会话目录。",
    )
    parser.add_argument(
        "--no-strict",
        action="store_true",
        help="关闭严格检查；默认要求时间窗口有效并且存在数据文件引用。",
    )
    parser.add_argument(
        "--no-index-reference",
        action="store_true",
        help="不自动把 dataset_index.yaml 写入 sensor 的 synchronized_dataset_files。",
    )
    parser.add_argument(
        "--chassis-estimated-params",
        default="",
        help="底盘算法输出参数 YAML；为空时自动检查会话目录 chassis/estimated_params.yaml。",
    )
    parser.add_argument(
        "--chassis-pending-commit-output",
        default="",
        help="底盘待提交参数包输出路径；为空时写到会话目录 chassis/pending_chassis_commit.yaml。",
    )
    parser.add_argument(
        "--skip-chassis-commit-staging",
        action="store_true",
        help="跳过底盘待提交参数包生成。",
    )
    parser.add_argument(
        "--chassis-commit-reason",
        default="底盘标定算法输出待提交",
        help="写入底盘待提交参数包的提交原因。",
    )
    parser.add_argument(
        "--chassis-operator-id",
        default="",
        help="生成底盘待提交参数包的操作员 ID。",
    )
    parser.add_argument(
        "--previous-chassis-parameter-version",
        default="",
        help="当前车端已生效底盘参数版本，用于回滚引用。",
    )
    parser.add_argument(
        "--chassis-persistent-write",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="最终提交到底盘时是否持久化。",
    )
    parser.add_argument(
        "--chassis-require-manual-approval",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="底盘待提交参数包是否要求人工审批。",
    )
    parser.add_argument(
        "--control-estimated-params",
        default="",
        help="运控算法输出参数 YAML；为空时自动检查会话目录 control/estimated_params.yaml。",
    )
    parser.add_argument(
        "--control-pending-commit-output",
        default="",
        help="运控待提交参数包输出路径；为空时写到会话目录 control/pending_control_commit.yaml。",
    )
    parser.add_argument(
        "--skip-control-commit-staging",
        action="store_true",
        help="跳过运控待提交参数包生成。",
    )
    parser.add_argument(
        "--control-commit-reason",
        default="运控标定算法输出待提交",
        help="写入运控待提交参数包的提交原因。",
    )
    parser.add_argument(
        "--control-operator-id",
        default="",
        help="生成运控待提交参数包的操作员 ID。",
    )
    parser.add_argument(
        "--previous-control-parameter-version",
        default="",
        help="当前车端已生效运控参数版本，用于回滚引用。",
    )
    parser.add_argument(
        "--control-persistent-write",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="最终提交到车端时是否持久化。",
    )
    parser.add_argument(
        "--control-require-manual-approval",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="运控待提交参数包是否要求人工审批。",
    )
    parser.add_argument(
        "--sensor-estimated-params",
        default="",
        help="传感器算法输出参数 YAML；为空时自动检查会话目录 sensor/estimated_params.yaml。",
    )
    parser.add_argument(
        "--sensor-pending-commit-output",
        default="",
        help="传感器待提交参数包输出路径；为空时写到会话目录 sensor/pending_sensor_commit.yaml。",
    )
    parser.add_argument(
        "--skip-sensor-commit-staging",
        action="store_true",
        help="跳过传感器待提交参数包生成。",
    )
    parser.add_argument(
        "--sensor-commit-reason",
        default="传感器标定算法输出待提交",
        help="写入传感器待提交参数包的提交原因。",
    )
    parser.add_argument(
        "--sensor-operator-id",
        default="",
        help="生成传感器待提交参数包的操作员 ID。",
    )
    parser.add_argument(
        "--previous-sensor-parameter-version",
        default="",
        help="当前车端已生效传感器参数版本，用于回滚引用。",
    )
    parser.add_argument(
        "--sensor-persistent-write",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="最终提交到车端时是否持久化。",
    )
    parser.add_argument(
        "--sensor-require-manual-approval",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="传感器待提交参数包是否要求人工审批。",
    )
    return parser.parse_args()


def require_map(value: Any, field_name: str) -> dict[str, Any]:
    if not isinstance(value, dict):
        raise ValueError(f"{field_name} 必须是 YAML map。")
    return value


def resolve_relative(path_value: str, base_dir: Path) -> Path:
    path = Path(path_value).expanduser()
    if path.is_absolute():
        return path
    return (base_dir / path).resolve(strict=False)


def load_site_profile(path: Path) -> dict[str, Any]:
    profile = load_yaml(path)
    return require_map(profile, "site_profile")


def resolve_session_dir(
    profile: dict[str, Any],
    profile_path: Path,
    session_id: str | None,
    session_dir_arg: str | None,
) -> Path:
    if session_dir_arg:
        return resolve_relative(session_dir_arg, Path.cwd())

    data_inputs = require_map(profile.get("algorithm_data_inputs", {}), "algorithm_data_inputs")
    dataset_root_raw = data_inputs.get("dataset_root")
    if not isinstance(dataset_root_raw, str) or not dataset_root_raw:
        raise ValueError("缺少 algorithm_data_inputs.dataset_root。")

    dataset_root = resolve_relative(dataset_root_raw, profile_path.parent)
    if session_id:
        return dataset_root / session_id
    return dataset_root


def resolve_dataset_index(
    profile: dict[str, Any],
    profile_path: Path,
    session_dir: Path,
    dataset_index_arg: str | None,
) -> Path:
    if dataset_index_arg:
        return resolve_relative(dataset_index_arg, Path.cwd())

    data_inputs = require_map(profile.get("algorithm_data_inputs", {}), "algorithm_data_inputs")
    index_name = data_inputs.get("dataset_index_name", "dataset_index.yaml")
    if not isinstance(index_name, str) or not index_name:
        raise ValueError("algorithm_data_inputs.dataset_index_name 必须是非空字符串。")

    index_path = Path(index_name).expanduser()
    if index_path.is_absolute():
        return index_path
    return (session_dir / index_path).resolve(strict=False)


def resolve_output_path(
    profile: dict[str, Any],
    profile_path: Path,
    session_dir: Path,
    session_id: str | None,
    output_arg: str | None,
) -> Path:
    if output_arg:
        return resolve_relative(output_arg, Path.cwd())
    data_inputs = require_map(profile.get("algorithm_data_inputs", {}), "algorithm_data_inputs")
    ros_params_file = data_inputs.get("ros_params_file")
    if isinstance(ros_params_file, str) and ros_params_file:
        output_value = ros_params_file
        if session_id:
            output_value = output_value.replace("{session_id}", session_id)
            output_value = output_value.replace("session_xxx", session_id)
        if "replace_with" not in output_value and "session_xxx" not in output_value:
            return resolve_relative(output_value, profile_path.parent)
    return (session_dir / "site_data_input.yaml").resolve(strict=False)


def default_chassis_estimated_params_path(session_dir: Path) -> Path:
    return (session_dir / "chassis" / "estimated_params.yaml").resolve(strict=False)


def default_control_estimated_params_path(session_dir: Path) -> Path:
    return (session_dir / "control" / "estimated_params.yaml").resolve(strict=False)


def default_sensor_estimated_params_path(session_dir: Path) -> Path:
    return (session_dir / "sensor" / "estimated_params.yaml").resolve(strict=False)


def resolve_optional_path(path_value: str, default_path: Path) -> Path:
    if path_value:
        return resolve_relative(path_value, Path.cwd())
    return default_path


def load_chassis_commit_tool() -> Any:
    spec = importlib.util.spec_from_file_location("stage_chassis_parameter_commit", CHASSIS_COMMIT_TOOL)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"无法加载底盘待提交参数包工具：{CHASSIS_COMMIT_TOOL}")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def load_control_commit_tool() -> Any:
    spec = importlib.util.spec_from_file_location("stage_control_parameter_commit", CONTROL_COMMIT_TOOL)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"无法加载运控待提交参数包工具：{CONTROL_COMMIT_TOOL}")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def load_sensor_commit_tool() -> Any:
    spec = importlib.util.spec_from_file_location("stage_sensor_parameter_commit", SENSOR_COMMIT_TOOL)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"无法加载传感器待提交参数包工具：{SENSOR_COMMIT_TOOL}")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def maybe_stage_chassis_commit(
    args: argparse.Namespace,
    dataset_index_path: Path,
    session_dir: Path,
) -> Path | None:
    if args.skip_chassis_commit_staging:
        return None

    default_estimated_path = default_chassis_estimated_params_path(session_dir)
    estimated_params_path = resolve_optional_path(args.chassis_estimated_params, default_estimated_path)
    explicit_estimated_path = bool(args.chassis_estimated_params)
    if not estimated_params_path.exists():
        if explicit_estimated_path:
            raise FileNotFoundError(f"底盘算法输出参数文件不存在：{estimated_params_path}")
        print(f"未发现底盘算法输出参数，跳过待提交包生成：{estimated_params_path}")
        return None

    chassis_commit_tool = load_chassis_commit_tool()
    index = chassis_commit_tool.load_yaml(dataset_index_path)
    params = chassis_commit_tool.load_yaml(estimated_params_path)
    session = chassis_commit_tool.validate_dataset_index(index)

    stage_args = argparse.Namespace(
        output=args.chassis_pending_commit_output,
        commit_reason=args.chassis_commit_reason,
        operator_id=args.chassis_operator_id,
        previous_parameter_version=args.previous_chassis_parameter_version,
        persistent_write=args.chassis_persistent_write,
        require_manual_approval=args.chassis_require_manual_approval,
        update_dataset_index=True,
    )
    output_path = chassis_commit_tool.resolve_output_path(stage_args, session)
    commit_package = chassis_commit_tool.build_commit_package(
        dataset_index_path,
        estimated_params_path,
        index,
        params,
        stage_args,
        output_path,
    )
    chassis_commit_tool.write_yaml(output_path, commit_package)
    chassis_commit_tool.update_dataset_index(dataset_index_path, index, output_path)
    print(f"已生成底盘待提交参数包：{output_path}")
    print(f"底盘待提交参数版本：{commit_package['commit_request']['parameter_version']}")
    return output_path


def maybe_stage_control_commit(
    args: argparse.Namespace,
    dataset_index_path: Path,
    session_dir: Path,
) -> Path | None:
    if args.skip_control_commit_staging:
        return None

    default_estimated_path = default_control_estimated_params_path(session_dir)
    estimated_params_path = resolve_optional_path(args.control_estimated_params, default_estimated_path)
    explicit_estimated_path = bool(args.control_estimated_params)
    if not estimated_params_path.exists():
        if explicit_estimated_path:
            raise FileNotFoundError(f"运控算法输出参数文件不存在：{estimated_params_path}")
        print(f"未发现运控算法输出参数，跳过待提交包生成：{estimated_params_path}")
        return None

    control_commit_tool = load_control_commit_tool()
    index = control_commit_tool.load_yaml(dataset_index_path)
    params = control_commit_tool.load_yaml(estimated_params_path)
    session = control_commit_tool.validate_dataset_index(index)

    stage_args = argparse.Namespace(
        output=args.control_pending_commit_output,
        commit_reason=args.control_commit_reason,
        operator_id=args.control_operator_id,
        previous_parameter_version=args.previous_control_parameter_version,
        persistent_write=args.control_persistent_write,
        require_manual_approval=args.control_require_manual_approval,
        update_dataset_index=True,
    )
    output_path = control_commit_tool.resolve_output_path(stage_args, session)
    commit_package = control_commit_tool.build_commit_package(
        dataset_index_path,
        estimated_params_path,
        index,
        params,
        stage_args,
        output_path,
    )
    control_commit_tool.write_yaml(output_path, commit_package)
    control_commit_tool.update_dataset_index(dataset_index_path, index, output_path)
    print(f"已生成运控待提交参数包：{output_path}")
    print(f"运控待提交参数版本：{commit_package['commit_request']['parameter_version']}")
    return output_path


def maybe_stage_sensor_commit(
    args: argparse.Namespace,
    dataset_index_path: Path,
    session_dir: Path,
) -> Path | None:
    if args.skip_sensor_commit_staging:
        return None

    default_estimated_path = default_sensor_estimated_params_path(session_dir)
    estimated_params_path = resolve_optional_path(args.sensor_estimated_params, default_estimated_path)
    explicit_estimated_path = bool(args.sensor_estimated_params)
    if not estimated_params_path.exists():
        if explicit_estimated_path:
            raise FileNotFoundError(f"传感器算法输出参数文件不存在：{estimated_params_path}")
        print(f"未发现传感器算法输出参数，跳过待提交包生成：{estimated_params_path}")
        return None

    sensor_commit_tool = load_sensor_commit_tool()
    index = sensor_commit_tool.load_yaml(dataset_index_path)
    params = sensor_commit_tool.load_yaml(estimated_params_path)
    session = sensor_commit_tool.validate_dataset_index(index)

    stage_args = argparse.Namespace(
        output=args.sensor_pending_commit_output,
        commit_reason=args.sensor_commit_reason,
        operator_id=args.sensor_operator_id,
        previous_parameter_version=args.previous_sensor_parameter_version,
        persistent_write=args.sensor_persistent_write,
        require_manual_approval=args.sensor_require_manual_approval,
        update_dataset_index=True,
    )
    output_path = sensor_commit_tool.resolve_output_path(stage_args, session)
    commit_package = sensor_commit_tool.build_commit_package(
        dataset_index_path,
        estimated_params_path,
        index,
        params,
        stage_args,
        output_path,
    )
    sensor_commit_tool.write_yaml(output_path, commit_package)
    sensor_commit_tool.update_dataset_index(dataset_index_path, index, output_path)
    print(f"已生成传感器待提交参数包：{output_path}")
    print(f"传感器待提交参数版本：{commit_package['commit_request']['parameter_version']}")
    return output_path


def main() -> int:
    args = parse_args()
    profile_path = Path(args.site_profile).expanduser().resolve(strict=False)
    profile = load_site_profile(profile_path)
    session_dir = resolve_session_dir(profile, profile_path, args.session_id, args.session_dir)
    dataset_index_path = resolve_dataset_index(profile, profile_path, session_dir, args.dataset_index)
    output_path = resolve_output_path(profile, profile_path, session_dir, args.session_id, args.output)

    index = load_yaml(dataset_index_path)
    output = build_ros_params(
        dataset_index_path,
        index,
        str(session_dir),
        not args.no_index_reference,
    )
    validate_output(output, strict=not args.no_strict)

    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(
        yaml.safe_dump(output, sort_keys=False, allow_unicode=True),
        encoding="utf-8",
    )

    pending_chassis_commit_path = maybe_stage_chassis_commit(args, dataset_index_path, session_dir)
    pending_control_commit_path = maybe_stage_control_commit(args, dataset_index_path, session_dir)
    pending_sensor_commit_path = maybe_stage_sensor_commit(args, dataset_index_path, session_dir)

    print(f"已完成会话数据输入收尾：{output_path}")
    print(f"启动参数：data_input_params_file:={output_path}")
    print(f"启动参数：dataset_index_file:={dataset_index_path}")
    if pending_chassis_commit_path is not None:
        print(f"底盘待提交参数包：{pending_chassis_commit_path}")
    if pending_control_commit_path is not None:
        print(f"运控待提交参数包：{pending_control_commit_path}")
    if pending_sensor_commit_path is not None:
        print(f"传感器待提交参数包：{pending_sensor_commit_path}")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as exc:
        print(f"[错误] {exc}", file=sys.stderr)
        raise SystemExit(1)
