#!/usr/bin/env python3
"""Export a DreamWaQ rsl_rl checkpoint (ActorCritic_DWAQ) to ONNX for MuJoCo.

rsl_rl has NO state preprocessor, so no normalization is needed (unlike SKRL).
The exported model uses the MEAN CENet code (deterministic deploy).

ONNX: inputs observations(1,45) + obs_history(1,5,45) -> actions(1,12)
(matches scripts/dreamwaq_sim2sim_mujoco.py interface)

Usage:
    uv run scripts/export_dreamwaq_rsl_onnx.py            # auto-find latest
    uv run scripts/export_dreamwaq_rsl_onnx.py --checkpoint runs/.../model_700.pt
"""
import argparse, glob, os, sys
import torch
import torch.nn as tnn

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from motrix_rl.dwaq_rsl.actor_critic_dwaq import ActorCritic_DWAQ

PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


class DwaqInfer(tnn.Module):
    """Deterministic inference: CENet mean code + actor."""
    def __init__(self, ac: ActorCritic_DWAQ):
        super().__init__()
        self.encoder = ac.encoder
        self.encode_mean_vel = ac.encode_mean_vel
        self.encode_mean_latent = ac.encode_mean_latent
        self.actor = ac.actor

    def forward(self, obs, obs_history):
        h = self.encoder(obs_history.reshape(obs_history.shape[0], -1))  # (B,225)->(B,64)
        vel = self.encode_mean_vel(h)        # (B,3)  mean velocity estimate
        latent = self.encode_mean_latent(h)  # (B,16) mean latent
        code = torch.cat([vel, latent], dim=-1)   # (B,19) = [vel, latent]
        x = torch.cat([code, obs], dim=-1)        # (B,64) = [code, obs]
        return self.actor(x)                      # (B,12)


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--checkpoint", default=None)
    p.add_argument("--output", default=os.path.join(PROJECT, "exports_go1_dreamwaq", "policy.onnx"))
    p.add_argument("--num-obs", type=int, default=45)
    p.add_argument("--num-priv", type=int, default=235)
    p.add_argument("--num-hist", type=int, default=5)
    p.add_argument("--num-act", type=int, default=12)
    args = p.parse_args()

    if args.checkpoint is None:
        runs = sorted(glob.glob(os.path.join(PROJECT, "runs", "go1-dreamwaq-walk", "rsl_dwaq", "*")),
                      key=os.path.getmtime)
        if not runs:
            print("[ERROR] no rsl_dwaq runs found"); sys.exit(1)
        models = glob.glob(os.path.join(runs[-1], "model_*.pt"))
        args.checkpoint = max(models, key=os.path.getmtime)

    print(f"[rsl-ONNX] checkpoint: {args.checkpoint}")
    cenet_out = 19
    ac = ActorCritic_DWAQ(
        args.num_obs + cenet_out,   # actor in = 64
        args.num_priv,              # critic in = 235
        args.num_act,               # 12
        args.num_hist * args.num_obs,  # cenet in = 225
        cenet_out,                  # 19
    )
    ckpt = torch.load(args.checkpoint, map_location="cpu")
    ac.load_state_dict(ckpt["model_state_dict"])
    ac.eval()

    model = DwaqInfer(ac).eval()
    os.makedirs(os.path.dirname(args.output), exist_ok=True)
    torch.onnx.export(
        model,
        (torch.zeros(1, args.num_obs), torch.zeros(1, args.num_hist, args.num_obs)),
        args.output,
        input_names=["observations", "obs_history"],
        output_names=["actions"],
        opset_version=11,
        dynamic_axes={"observations": {0: "batch"}, "obs_history": {0: "batch"}, "actions": {0: "batch"}},
    )
    print(f"[rsl-ONNX] exported -> {args.output}  (no normalization; mean CENet code)")


if __name__ == "__main__":
    main()
