"""Generate a tiny synthetic session for V1 smoke tests."""

from __future__ import annotations

from pathlib import Path

import numpy as np

from imu_lidar.contracts import ImuSeries, LidarFrame
from imu_lidar.geometry import so3_exp
from imu_lidar.imu_io import save_imu_csv
from imu_lidar.lidar_io import save_lidar_session


def _wall_cloud(rng: np.random.Generator, n: int = 800) -> np.ndarray:
    yz = rng.uniform([-5, -1], [5, 3], size=(n // 3, 2))
    wall_x = np.column_stack([np.full(n // 3, 8.0), yz[:, 0], yz[:, 1]])
    xz = rng.uniform([-5, -1], [5, 3], size=(n // 3, 2))
    wall_y = np.column_stack([xz[:, 0], np.full(n // 3, 6.0), xz[:, 1]])
    xy = rng.uniform([-5, -5], [5, 5], size=(n - 2 * (n // 3), 2))
    ground = np.column_stack([xy[:, 0], xy[:, 1], np.full(xy.shape[0], -1.0)])
    return np.vstack([wall_x, wall_y, ground])


def generate_synthetic_session(
    output_root: Path,
    *,
    delta_t_s: float = 0.17,
    yaw_extrinsic_deg: float = 25.0,
    seed: int = 0,
) -> dict[str, float]:
    """Write IMU CSV + LiDAR frames with known extrinsic rotation and time offset."""

    rng = np.random.default_rng(seed)
    output_root = Path(output_root)
    output_root.mkdir(parents=True, exist_ok=True)

    r_x = so3_exp(np.deg2rad(np.array([2.0, -1.5, yaw_extrinsic_deg])))
    map_points = _wall_cloud(rng)

    lidar_hz = 10.0
    duration = 8.0
    lidar_times = np.arange(0.0, duration, 1.0 / lidar_hz)
    # Non-yaw excitation is required for unique SO(3) hand-eye observability.
    yaw = 0.5 * np.sin(0.8 * lidar_times) + 0.12 * lidar_times
    pitch = 0.18 * np.sin(1.3 * lidar_times + 0.4)
    roll = 0.12 * np.sin(1.7 * lidar_times + 1.0)
    yaw_rate = np.gradient(yaw, lidar_times)
    pitch_rate = np.gradient(pitch, lidar_times)
    roll_rate = np.gradient(roll, lidar_times)

    frames: list[LidarFrame] = []
    for index, (t, yaw_i, pitch_i, roll_i) in enumerate(zip(lidar_times, yaw, pitch, roll)):
        r_wl = so3_exp(np.array([roll_i, pitch_i, yaw_i]))
        t_wl = np.array([0.4 * t, 0.05 * np.sin(0.5 * t), 0.0])
        points = (map_points - t_wl) @ r_wl
        points = points + rng.normal(0.0, 0.01, size=points.shape)
        frames.append(
            LidarFrame(
                frame_id=str(index),
                t_start_s=float(t),
                t_end_s=float(t + 0.08),
                points_xyz=points.astype(float),
            )
        )
    save_lidar_session(output_root / "lidar", frames)

    imu_hz = 100.0
    t_lidar_grid = np.arange(0.0, duration, 1.0 / imu_hz)
    omega_lidar = np.column_stack(
        [
            np.interp(t_lidar_grid, lidar_times, roll_rate),
            np.interp(t_lidar_grid, lidar_times, pitch_rate),
            np.interp(t_lidar_grid, lidar_times, yaw_rate),
        ]
    )
    omega_imu = omega_lidar @ r_x.T

    g_world = np.array([0.0, 0.0, 9.80665])
    acc_rows = []
    for yaw_i, pitch_i, roll_i in zip(
        np.interp(t_lidar_grid, lidar_times, yaw),
        np.interp(t_lidar_grid, lidar_times, pitch),
        np.interp(t_lidar_grid, lidar_times, roll),
    ):
        r_wl = so3_exp(np.array([roll_i, pitch_i, yaw_i]))
        g_in_lidar = r_wl.T @ g_world
        acc_rows.append(r_x @ g_in_lidar)
    acc = np.asarray(acc_rows, dtype=float)

    static_t = np.arange(-1.0, 0.0, 1.0 / imu_hz)
    static_gyro = np.zeros((static_t.size, 3))
    static_acc = np.tile(r_x @ g_world, (static_t.size, 1))

    t_imu = np.concatenate([static_t + delta_t_s, t_lidar_grid + delta_t_s])
    gyro = np.vstack([static_gyro, omega_imu]) + rng.normal(0.0, 0.001, size=(t_imu.size, 3))
    acc_all = np.vstack([static_acc, acc]) + rng.normal(0.0, 0.01, size=(t_imu.size, 3))
    imu = ImuSeries(t_s=t_imu, gyro_rad_s=gyro, acc_m_s2=acc_all)
    save_imu_csv(output_root / "imu.csv", imu)

    return {
        "delta_t_s": float(delta_t_s),
        "yaw_extrinsic_deg": float(yaw_extrinsic_deg),
    }


if __name__ == "__main__":
    import json

    out = Path("examples/synthetic_session")
    meta = generate_synthetic_session(out)
    (out / "meta.json").write_text(json.dumps(meta, indent=2), encoding="utf-8")
    print(f"wrote {out}")
    print(meta)
