"""Export tuned motor parameters to MJCF and robot.yaml files.

Reads the original motor.xml and robot.yaml, patches with optimised
parameter values, and writes motor_tuned.xml + robot_tuned.yaml.

Usage:
    python -m src.sysid.motor.export \
        --asset-path assets/motor \
        --result assets/motor/motor_sysid_result.json
"""

from __future__ import annotations

import argparse
import copy
import json
import xml.etree.ElementTree as ET
from pathlib import Path

import structlog
import yaml

log = structlog.get_logger()

_DEFAULT_ASSET = "assets/motor"


def export_tuned_files(
    asset_path: str | Path,
    params: dict[str, float],
) -> tuple[Path, Path]:
    """Write tuned MJCF and robot.yaml files.

    Returns (tuned_mjcf_path, tuned_robot_yaml_path).
    """
    asset_path = Path(asset_path).resolve()

    robot_yaml_path = asset_path / "robot.yaml"
    robot_cfg = yaml.safe_load(robot_yaml_path.read_text())
    mjcf_path = asset_path / robot_cfg["mjcf"]

    # ── Tune MJCF ────────────────────────────────────────────────
    tree = ET.parse(str(mjcf_path))
    root = tree.getroot()

    # Actuator — use average gear for the MJCF model.
    gear_pos = params.get("actuator_gear_pos", params.get("actuator_gear"))
    gear_neg = params.get("actuator_gear_neg", params.get("actuator_gear"))
    gear_avg = None
    if gear_pos is not None and gear_neg is not None:
        gear_avg = (gear_pos + gear_neg) / 2.0
    elif gear_pos is not None:
        gear_avg = gear_pos
    filter_tau = params.get("actuator_filter_tau")
    for act_el in root.iter("general"):
        if act_el.get("name") == "motor":
            if gear_avg is not None:
                act_el.set("gear", str(gear_avg))
            if filter_tau is not None:
                if filter_tau > 0:
                    act_el.set("dyntype", "filter")
                    act_el.set("dynprm", str(filter_tau))
                else:
                    act_el.set("dyntype", "none")

    # Joint — average damping & friction for MJCF (asymmetry in runtime).
    fl_pos = params.get("motor_frictionloss_pos", params.get("motor_frictionloss"))
    fl_neg = params.get("motor_frictionloss_neg", params.get("motor_frictionloss"))
    fl_avg = None
    if fl_pos is not None and fl_neg is not None:
        fl_avg = (fl_pos + fl_neg) / 2.0
    elif fl_pos is not None:
        fl_avg = fl_pos
    damp_pos = params.get("motor_damping_pos", params.get("motor_damping"))
    damp_neg = params.get("motor_damping_neg", params.get("motor_damping"))
    damp_avg = None
    if damp_pos is not None and damp_neg is not None:
        damp_avg = (damp_pos + damp_neg) / 2.0
    elif damp_pos is not None:
        damp_avg = damp_pos
    for jnt in root.iter("joint"):
        if jnt.get("name") == "motor_joint":
            if damp_avg is not None:
                jnt.set("damping", str(damp_avg))
            if "motor_armature" in params:
                jnt.set("armature", str(params["motor_armature"]))
            if fl_avg is not None:
                jnt.set("frictionloss", str(fl_avg))

    # Rotor mass.
    if "rotor_mass" in params:
        for geom in root.iter("geom"):
            if geom.get("name") == "rotor_disk":
                geom.set("mass", str(params["rotor_mass"]))

    # Write tuned MJCF.
    tuned_mjcf_name = mjcf_path.stem + "_tuned" + mjcf_path.suffix
    tuned_mjcf_path = asset_path / tuned_mjcf_name
    ET.indent(tree, space="  ")
    tree.write(str(tuned_mjcf_path), xml_declaration=True, encoding="unicode")
    log.info("tuned_mjcf_written", path=str(tuned_mjcf_path))

    # ── Tune robot.yaml ──────────────────────────────────────────
    tuned_cfg = copy.deepcopy(robot_cfg)
    tuned_cfg["mjcf"] = tuned_mjcf_name

    if tuned_cfg.get("actuators") and len(tuned_cfg["actuators"]) > 0:
        act = tuned_cfg["actuators"][0]
        if gear_avg is not None:
            act["gear"] = round(gear_avg, 6)
        if "actuator_filter_tau" in params:
            act["filter_tau"] = round(params["actuator_filter_tau"], 6)
        if "motor_damping" in params:
            act["damping"] = round(params["motor_damping"], 6)

    if "joints" not in tuned_cfg:
        tuned_cfg["joints"] = {}
    if "motor_joint" not in tuned_cfg["joints"]:
        tuned_cfg["joints"]["motor_joint"] = {}
    mj = tuned_cfg["joints"]["motor_joint"]
    if "motor_armature" in params:
        mj["armature"] = round(params["motor_armature"], 6)
    if fl_avg is not None:
        mj["frictionloss"] = round(fl_avg, 6)

    # Asymmetric / hardware-realism / nonlinear parameters.
    realism = {}
    for key in [
        "actuator_gear_pos", "actuator_gear_neg",
        "motor_damping_pos", "motor_damping_neg",
        "motor_frictionloss_pos", "motor_frictionloss_neg",
        "motor_deadzone_pos", "motor_deadzone_neg",
        "action_bias",
        "viscous_quadratic", "back_emf_gain",
        "stribeck_friction_boost", "stribeck_vel",
        "gearbox_backlash",
    ]:
        if key in params:
            realism[key] = round(params[key], 6)
    if realism:
        tuned_cfg["hardware_realism"] = realism

    tuned_yaml_path = asset_path / "robot_tuned.yaml"
    header = (
        "# Tuned motor config — generated by src.sysid.motor.optimize\n"
        "# Original: robot.yaml\n\n"
    )
    tuned_yaml_path.write_text(
        header + yaml.dump(tuned_cfg, default_flow_style=False, sort_keys=False)
    )
    log.info("tuned_robot_yaml_written", path=str(tuned_yaml_path))

    return tuned_mjcf_path, tuned_yaml_path


# ── CLI ──────────────────────────────────────────────────────────────


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Export tuned motor parameters to MJCF + robot.yaml."
    )
    parser.add_argument("--asset-path", type=str, default=_DEFAULT_ASSET)
    parser.add_argument(
        "--result", type=str, default=None,
        help="Path to motor_sysid_result.json (auto-detected if omitted)",
    )
    args = parser.parse_args()

    asset_path = Path(args.asset_path).resolve()
    if args.result:
        result_path = Path(args.result)
    else:
        result_path = asset_path / "motor_sysid_result.json"

    if not result_path.exists():
        raise FileNotFoundError(f"Result file not found: {result_path}")

    result = json.loads(result_path.read_text())
    params = result["best_params"]

    export_tuned_files(asset_path=args.asset_path, params=params)


if __name__ == "__main__":
    main()
