from __future__ import annotations

import argparse
import json
import math
from pathlib import Path

import numpy as np
from scipy.spatial.transform import Rotation


def load(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8-sig"))


def write(path: Path, document: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(document, ensure_ascii=False, indent=2), encoding="utf-8")


def inverse(t: np.ndarray) -> np.ndarray:
    result = np.eye(4)
    result[:3, :3] = t[:3, :3].T
    result[:3, 3] = -result[:3, :3] @ t[:3, 3]
    return result


def delta(a: np.ndarray, b: np.ndarray) -> dict:
    d = inverse(a) @ b
    return {
        "translation_m": float(np.linalg.norm(d[:3, 3])),
        "rotation_deg": float(np.linalg.norm(Rotation.from_matrix(d[:3, :3]).as_rotvec()) * 180.0 / math.pi),
        "delta_matrix_4x4": d.tolist(),
    }


def corrected(raw: dict, backend: str, reference_height: float) -> dict:
    return {
        "schema_version": 1,
        "success": bool(raw["success"]),
        "convention": "T_RTK_lidar maps raw LiDAR points into the RTK navigation frame",
        "equation": "A_RTK_ij X = X B_LiDAR_ij",
        "frames": {
            "RTK": {
                "origin": "GGA positioning reference point; confirm ANT1/reference antenna in receiver configuration",
                "x_axis": "horizontal projection of the rawHeading baseline direction reported by the receiver",
                "y_axis": "left",
                "z_axis": "up",
                "yaw_enu_deg": "90 - rawHeadingDeg",
            },
            "LiDAR": "raw LiDAR sensor frame",
        },
        "backend": backend,
        "measured_lidar_extrinsic_used_as_initial": False,
        "body_heading_offset_used": False,
        "body_antenna_lever_xy_used": False,
        "translation_m": raw["translation_m"],
        "rotation_rpy_deg_xyz": raw["rotation_rpy_deg_xyz"],
        "quaternion_xyzw": raw["quaternion_xyzw"],
        "matrix_4x4": raw["matrix_4x4"],
        "quality": {
            "stations": raw["estimation"]["stations"],
            "pairs": raw["estimation"]["pairs"],
            "residuals": raw["estimation"]["residuals"],
            "weighted_jacobian_condition_number": raw["weighted_jacobian_condition_number"],
            "linearized_one_sigma": raw["linearized_one_sigma"],
            "bootstrap": raw["bootstrap"],
        },
        "z_constraint": {
            "observable_from_planar_AX_XB": False,
            "method": "LiDAR ground planes plus externally supplied RTK reference-point height above ground",
            "rtk_reference_height_above_ground_m": reference_height,
            "warning": "z is conditional on the supplied RTK antenna height; it is not independently identified by planar Ackermann motion",
        },
        "important_limit": "AX residual and bootstrap quantify internal consistency, not independent centimetre-grade absolute certification",
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--result-root", type=Path, required=True)
    parser.add_argument("--reference-height", type=float, required=True)
    args = parser.parse_args()

    def solver_output(directory: str) -> Path:
        raw = args.result_root / directory / "extrinsic_raw.json"
        standard = args.result_root / directory / "extrinsic.json"
        return raw if raw.exists() else standard

    paths = {
        "open3d_gicp": solver_output("open3d_gicp"),
        "small_gicp": solver_output("small_gicp"),
        "consensus": solver_output("consensus"),
    }
    docs = {}
    for backend, path in paths.items():
        document = corrected(load(path), backend, args.reference_height)
        write(path.with_name("extrinsic_rtk_lidar.json"), document)
        docs[backend] = document

    open_t = np.asarray(docs["open3d_gicp"]["matrix_4x4"], float)
    small_t = np.asarray(docs["small_gicp"]["matrix_4x4"], float)
    final = dict(docs["consensus"])
    final["selection"] = {
        "recommended": True,
        "reason": "Uses only motion pairs accepted independently by both Open3D GICP and small_gicp",
        "open3d_vs_small_gicp": delta(open_t, small_t),
    }


    write(args.result_root / "final_T_RTK_lidar.json", final)
    summary = {
        "final": {
            "translation_m": final["translation_m"],
            "rotation_rpy_deg_xyz": final["rotation_rpy_deg_xyz"],
            "pairs": final["quality"]["pairs"],
            "translation_rms_m": final["quality"]["residuals"]["translation_m"]["rms"],
            "rotation_rms_deg": final["quality"]["residuals"]["rotation_deg"]["rms"],
            "condition_number": final["quality"]["weighted_jacobian_condition_number"],
        },
        "backend_difference": delta(open_t, small_t),
    }
    write(args.result_root / "summary.json", summary)
    print(json.dumps(summary, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
