# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Evaluate trained Go1 policy with specific velocity commands.

Tests three command patterns:
  1. Forward-Backward (前后往返): vx oscillates +1.0 <-> -1.0
  2. Left-Right (左右往返): vy oscillates +1.0 <-> -1.0
  3. Rotation (旋转): wz oscillates +1.0 <-> -1.0

For each pattern, the command toggles direction every N seconds.
Metrics (tracking errors, etc.) are logged to CSV files for analysis.
"""

import logging
import time
from pathlib import Path

import numpy as np
from absl import app, flags

from motrix_envs import registry as env_registry
from motrix_rl import registry, utils
from motrix_rl.skrl.jax import wrap_env
from motrix_rl.skrl.jax.train.ppo import Trainer as SkrlJaxTrainer

logger = logging.getLogger(__name__)

_ENV = flags.DEFINE_string("env", "go1-flat-terrain-walk", "The env to evaluate")
_POLICY = flags.DEFINE_string(
    "policy",
    None,
    "Path to policy checkpoint. Auto-discovers the latest best_agent.pickle if not specified.",
)
_DURATION = flags.DEFINE_float("duration", 4.0, "Seconds per command direction before toggling")
_OUTDIR = flags.DEFINE_string("outdir", None, "Output directory for CSV logs")
_RENDER = flags.DEFINE_bool("render", False, "Enable rendering (may crash on headless)")

# ── Helpers ─────────────────────────────────────────────────────────────


def _find_best_policy(env_name: str) -> Path:
    """Auto-discover the best SKRL policy for the given env."""
    base = Path(f"runs/{env_name}/skrl")
    if not base.exists():
        raise FileNotFoundError(f"No training runs found at {base}")

    runs = sorted([d for d in base.iterdir() if d.is_dir()], key=lambda d: d.stat().st_mtime, reverse=True)
    if not runs:
        raise FileNotFoundError(f"No training runs found at {base}")

    ckpt_dir = runs[0] / "checkpoints"
    best = list(ckpt_dir.glob("best_agent.*"))
    if best:
        return best[0]

    # Fallback: highest timestep
    ckpts = list(ckpt_dir.glob("agent_*.pickle"))
    if not ckpts:
        raise FileNotFoundError(f"No checkpoints found in {ckpt_dir}")

    def _ts(p):
        try:
            return int(p.stem.split("_")[1])
        except (IndexError, ValueError):
            return 0

    return max(ckpts, key=_ts)


def _save_log(outdir: Path, label: str, records: list[dict]) -> None:
    """Save records to a CSV file."""
    outdir.mkdir(parents=True, exist_ok=True)
    import csv

    path = outdir / f"{label}.csv"
    with open(path, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=records[0].keys())
        writer.writeheader()
        writer.writerows(records)
    logger.info(f"Saved {len(records)} records → {path}")


# ── Command generators ──────────────────────────────────────────────────


def _command_forward_backward(t: float, period: float):
    """vx oscillates +1.0 / -1.0, vy=0, wz=0."""
    half = period / 2.0
    phase = (t % period) / half  # 0..1 forward, 1..2 backward
    vx = 1.0 if phase < 1.0 else -1.0
    return np.array([vx, 0.0, 0.0], dtype=np.float32)


def _command_left_right(t: float, period: float):
    """vy oscillates +1.0 / -1.0, vx=0, wz=0."""
    half = period / 2.0
    phase = (t % period) / half
    vy = 1.0 if phase < 1.0 else -1.0
    return np.array([0.0, vy, 0.0], dtype=np.float32)


def _command_rotation(t: float, period: float):
    """wz oscillates +1.0 / -1.0, vx=0, vy=0."""
    half = period / 2.0
    phase = (t % period) / half
    wz = 1.0 if phase < 1.0 else -1.0
    return np.array([0.0, 0.0, wz], dtype=np.float32)


# ── Main evaluation logic ───────────────────────────────────────────────


def _run_pattern(
    trainer: SkrlJaxTrainer,
    policy_path: str,
    pattern_name: str,
    command_fn,
    period: float,
    total_steps: int,
    ctrl_dt: float,
) -> list[dict]:
    """Run one command pattern and return tracking records."""
    logger.info(f"--- {pattern_name} ---")

    rlcfg = trainer._rlcfg
    env = env_registry.make(trainer._env_name, sim_backend=trainer._sim_backend, num_envs=1)
    env = wrap_env(env, enable_render=False)

    # Build fresh agent for this run
    models = trainer._make_model(env, rlcfg)
    ppo_cfg = rlcfg.runner.agent.to_dict()
    from motrix_rl.skrl.jax.train.ppo import _add_runtime_config
    _add_runtime_config(ppo_cfg, env)
    agent = trainer._make_agent(models, env, ppo_cfg, rlcfg.runner.memory)
    agent.load(policy_path)

    obs, info = env.reset()
    state = env._env.state
    records = []

    for step in range(total_steps):
        t = step * ctrl_dt

        # Override command in env state
        cmd = command_fn(t, period)
        state.info["commands"] = cmd.reshape(1, -1)

        # Recompute observation with the new command
        new_obs = env._env._get_obs(state.data, state.info)
        state = state.replace(obs=new_obs)
        obs = new_obs

        # Agent inference
        outputs = agent.act(obs, timestep=0, timesteps=0)
        actions = outputs[-1].get("mean_actions", outputs[0])
        obs, reward, terminated, truncated, info = env.step(actions)
        state = env._env.state

        # Collect tracking data
        lin_vel = env._env.get_local_linvel(state.data)[0]  # [vx, vy, vz] (body frame)
        gyro = env._env.get_gyro(state.data)[0]

        tracking_err_xy = np.linalg.norm(cmd[:2] - lin_vel[:2])
        tracking_err_yaw = abs(cmd[2] - gyro[2])

        records.append({
            "step": step,
            "time": round(t, 3),
            "cmd_vx": round(float(cmd[0]), 4),
            "cmd_vy": round(float(cmd[1]), 4),
            "cmd_wz": round(float(cmd[2]), 4),
            "actual_vx": round(float(lin_vel[0]), 4),
            "actual_vy": round(float(lin_vel[1]), 4),
            "actual_vz": round(float(lin_vel[2]), 4),
            "actual_wz": round(float(gyro[2]), 4),
            "tracking_err_xy": round(float(tracking_err_xy), 6),
            "tracking_err_yaw": round(float(tracking_err_yaw), 6),
            "reward": round(float(reward[0][0]), 6),
        })

        if step % 100 == 0:
            logger.info(
                f"  [{pattern_name}] step {step:5d}/{total_steps}  "
                f"cmd=[{cmd[0]:+.1f},{cmd[1]:+.1f},{cmd[2]:+.1f}]  "
                f"actual_v=[{lin_vel[0]:+.3f},{lin_vel[1]:+.3f},{gyro[2]:+.3f}]  "
                f"track_err_xy={tracking_err_xy:.4f}  "
                f"reward={float(reward[0][0]):.4f}"
            )

    env.close()
    return records


def main(argv):
    env_name = _ENV.value
    ctrl_dt = 0.01  # matches Go1WalkNpEnvCfg.ctrl_dt
    period = _DURATION.value * 2.0  # full cycle: forward + backward

    # Resolve policy path
    if _POLICY.present:
        policy_path = _POLICY.value
    else:
        policy_path = str(_find_best_policy(env_name))
    logger.info(f"Policy: {policy_path}")

    # Build trainer (used to construct models & agent)
    rlcfg = registry.default_rl_cfg(env_name, "skrl", backend="jax")
    trainer = SkrlJaxTrainer(env_name, sim_backend=None, enable_render=False)
    trainer._rlcfg = rlcfg

    # Total steps per pattern = enough full cycles
    cycles = 3
    total_steps = int(cycles * period / ctrl_dt)

    # Output directory
    if _OUTDIR.present:
        outdir = Path(_OUTDIR.value)
    else:
        ts = time.strftime("%y-%m-%d_%H-%M-%S")
        outdir = Path(f"runs/{env_name}/eval_{ts}")
    logger.info(f"Output directory: {outdir}")

    patterns = [
        ("forward_backward", _command_forward_backward),
        ("left_right", _command_left_right),
        ("rotation", _command_rotation),
    ]

    summary = {}
    for label, cmd_fn in patterns:
        records = _run_pattern(
            trainer, policy_path, label, cmd_fn,
            period=period, total_steps=total_steps, ctrl_dt=ctrl_dt,
        )
        _save_log(outdir, label, records)

        # Summary stats (steady-state, skip first 2s for stabilization)
        warmup = int(2.0 / ctrl_dt)
        steady = records[warmup:]
        if steady:
            avg_err = np.mean([r["tracking_err_xy"] for r in steady])
            avg_reward = np.mean([r["reward"] for r in steady])
        else:
            avg_err, avg_reward = float("nan"), float("nan")
        summary[label] = {"avg_tracking_err_xy": avg_err, "avg_reward": avg_reward}
        logger.info(f"  [{label}] steady-state avg tracking_err_xy = {avg_err:.4f}, avg_reward = {avg_reward:.4f}")

    # Print summary
    print("\n" + "=" * 70)
    print("EVALUATION SUMMARY")
    print("=" * 70)
    for label, stats in summary.items():
        print(f"  {label:25s}  track_err_xy={stats['avg_tracking_err_xy']:.4f}  avg_reward={stats['avg_reward']:.4f}")
    print(f"\nDetailed CSV logs saved to: {outdir}")
    print("=" * 70)


if __name__ == "__main__":
    app.run(main)
