#!/usr/bin/env python3
"""DreamWaQ training via the faithful rsl_rl-1.0.2 port (upstream-aligned).

Uses upstream's ActorCritic_DWAQ + PPO (joint VAE training) + OnPolicyRunner,
with MotrixLab's DreamWaQ env. Config matches upstream Go1RoughCfgPPO.

Usage:
    uv run scripts/train_dreamwaq_rsl.py --num-envs 2048 --iterations 3000
"""
import argparse
import os
import sys
import torch

import motrix_envs.locomotion.go1.dreamwaq  # noqa: F401  (registers env)
from motrix_envs import registry as env_registry
from motrix_rl.dwaq_rsl import OnPolicyRunner, DwaqVecEnv


# Upstream Go1RoughCfgPPO (legged_robot_config.py LeggedRobotCfgPPO + Go1 overrides)
TRAIN_CFG = {
    "runner": {
        "policy_class_name": "ActorCritic_DWAQ",
        "algorithm_class_name": "PPO",
        "num_steps_per_env": 24,
        "save_interval": 50,
    },
    "algorithm": {
        "value_loss_coef": 1.0,
        "use_clipped_value_loss": True,
        "clip_param": 0.2,
        "entropy_coef": 0.01,
        "num_learning_epochs": 5,
        "num_mini_batches": 4,
        "learning_rate": 1.0e-3,
        "schedule": "adaptive",
        "gamma": 0.99,
        "lam": 0.95,
        "desired_kl": 0.01,
        "max_grad_norm": 1.0,
    },
    "policy": {
        "init_noise_std": 1.0,
    },
}


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--num-envs", type=int, default=2048)
    p.add_argument("--iterations", type=int, default=3000)
    p.add_argument("--seed", type=int, default=1)
    p.add_argument("--init-noise-std", type=float, default=1.0)
    p.add_argument("--force-std", action="store_true",
                   help="force reset action std to --init-noise-std even when resuming")
    p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
    p.add_argument("--resume", default=None,
                   help="checkpoint .pt to warm-start from (e.g. the flat policy before "
                        "the pyramid-terrain curriculum). Loads model + optimizer; the "
                        "action std comes from the checkpoint, not --init-noise-std.")
    p.add_argument("--level", type=int, default=None,
                   help="force ALL envs to this terrain level (0-9), skip curriculum")
    args = p.parse_args()

    torch.manual_seed(args.seed)
    import numpy as np
    np.random.seed(args.seed)

    TRAIN_CFG["policy"]["init_noise_std"] = args.init_noise_std

    raw_env = env_registry.make("go1-dreamwaq-walk", num_envs=args.num_envs)
    if args.level is not None:
        raw_env._force_level = args.level
        print(f"[DreamWaQ-rsl] forcing ALL envs at terrain level {args.level}")
    env = DwaqVecEnv(raw_env, device=args.device)
    print(f"[DreamWaQ-rsl] {args.num_envs} envs | obs={env.num_obs} priv={env.num_privileged_obs} "
          f"hist={env.num_obs_hist} act={env.num_actions} | device={args.device}")
    print(f"[DreamWaQ-rsl] actor_in={env.num_obs + 19} critic_in={env.num_privileged_obs} "
          f"cenet_in={env.num_obs_hist * env.num_obs}")

    log_dir = os.path.join("runs", "go1-dreamwaq-walk", "rsl_dwaq",
                           __import__("datetime").datetime.now().strftime("%m-%d_%H-%M-%S"))
    os.makedirs(log_dir, exist_ok=True)

    # Save config snapshot for later reference
    import json
    cfg_snapshot = {
        "command_line": sys.argv,
        "kp": raw_env.cfg.control_config.stiffness,
        "kd": raw_env.cfg.control_config.damping,
        "action_scale": raw_env.cfg.control_config.action_scale,
        "rewards": dict(raw_env.cfg.reward_config.scales),
        "sigma": raw_env.cfg.reward_config.tracking_sigma,
        "only_positive": raw_env.cfg.reward_config.only_positive_rewards,
        "force_level": args.level,
        "init_noise_std": TRAIN_CFG["policy"]["init_noise_std"],
        "entropy_coef": TRAIN_CFG["algorithm"]["entropy_coef"],
        "terrain_rows": raw_env.cfg.terrain_rows,
        "terrain_cols": raw_env.cfg.terrain_cols,
        "scene": raw_env.cfg.model_file,
    }
    with open(os.path.join(log_dir, "config.json"), "w") as f:
        json.dump(cfg_snapshot, f, indent=2, default=str)

    runner = OnPolicyRunner(env, TRAIN_CFG, log_dir=log_dir, device=args.device)
    if args.resume:
        runner.load(args.resume)
        runner.current_learning_iteration = 0
        if args.force_std:
            runner.alg.actor_critic.std.data.fill_(args.init_noise_std)
            print(f"[DreamWaQ-rsl] warm-start from {args.resume} "
                  f"(std FORCED to {args.init_noise_std})")
        else:
            print(f"[DreamWaQ-rsl] warm-start from {args.resume} "
                  f"(model+optimizer; std from checkpoint)")
    print(f"[DreamWaQ-rsl] log_dir={log_dir} | training {args.iterations} iterations...")
    runner.learn(args.iterations, init_at_random_ep_len=True)
    print("[DreamWaQ-rsl] done.")


if __name__ == "__main__":
    main()
