"""Publish one preprocessed solo-dataset GT clip in the E2E turn viewer.

This intentionally bypasses both the VQ codecs and the motion generator so
visual inspection can separate source/preprocessing/retargeting problems from
learned-model errors. Two hard-won rules (2026-07-30, solo seg126 진단):

* rest 테이블은 반드시 그 NPZ를 만든 베이크 템플릿에서 읽는다.
  convert_humanoid는 "템플릿 rest를 벗긴 basis"를 저장하므로, 되입힐 때 다른
  테이블(구 Blender 코퍼스용 kemix_rest_table.npy — hips 90°/어깨 120° 상이)을
  쓰면 제3의 잘못된 포즈가 나온다 (GT_1의 '가슴 앞으로 접힌 팔').
* hips translation을 버리지 않는다. 1인방송 배우는 실제로 걷는다(구간 median
  84cm, 최대 ~4m). root를 고정하면 걷기 회전만 남아 발이 러닝머신처럼 쓸린다
  (FIX_9 vs FIX_10 A/B). 아바타는 걷기·춤을 지원해야 한다는 제품 결정에 따라
  이동을 그대로 싣는다.
* (2026-07-30, seg126 재진단) npz -> write_vrma 재조립을 하지 않는다.
  vrma_writer의 내장 제네릭 스켈레톤에는 upperChest가 없어 고아 노드가 되고
  (휴머노이드 맵 제외 -> 회전 드랍), 어깨·목이 chest 직결로 잘못 연결되어
  three-vrm 리타겟 기준 rest가 kemix 체인(chest->upperChest->어깨/목)과 어긋난다
  — 팔꿈치 72°가 149°로 벌어지는 자세 파괴 (원본/npz FK는 0.1~4° 일치인데
  발행본만 어긋나는 홉 대조로 특정). 그래서 베이크가 템플릿 실제 계층으로 직접
  쓴 processed/vrma 산출물을 body.vrma로 그대로 복사한다. npz는 학습 GT로만
  쓰고, 여기서는 프레임 수/이동 검증과 meta 기록에만 읽는다.
"""
import argparse
import json
import os
import shutil
import sys
import time

import numpy as np

HERE = os.path.dirname(os.path.abspath(__file__))
WH = os.path.abspath(os.path.join(
    HERE, "..", "motion-blender", "experiments", "wave-hands"))
sys.path.insert(0, os.path.join(WH, "..", "..", "lib"))

from vrma_to_npz import FINGER_BONES, _read_glb  # noqa: E402
from face_v3_infer import (                   # noqa: E402
    ARKIT_52_NAMES,
    render_face,
)

# 이 데이터셋의 NPZ가 벗겨낸 바로 그 rest — 되입힐 때도 같은 것을 써야 한다.
DATASET_TEMPLATE = ("/data/mocap-convertor/datasets/solo_20260722_body_v1"
                    "/artifacts/templates/kemix.vrma")

FPS = 30
# 손가락 본 수는 vrma_to_npz가 단일 소스 — 24본/30본 하드코딩 금지. 2026-08-04
# 엄지 확장(44->50본) 때 여기가 44에 묶여 있어 test 클립 발행이 막혔다.
BONES = [
    "hips", "spine", "chest", "upperChest", "neck", "head",
    "leftShoulder", "leftUpperArm", "leftLowerArm", "leftHand",
    "rightShoulder", "rightUpperArm", "rightLowerArm", "rightHand",
    "leftUpperLeg", "leftLowerLeg", "leftFoot",
    "rightUpperLeg", "rightLowerLeg", "rightFoot",
] + list(FINGER_BONES)
NB = len(BONES)


def template_rest(vrma_path):
    """{humanoid bone: [x,y,z,w]} — 베이크 템플릿 노드의 rest 회전 그대로."""
    gltf, _ = _read_glb(vrma_path)
    hb = gltf["extensions"]["VRMC_vrm_animation"]["humanoid"]["humanBones"]
    nodes = gltf["nodes"]
    return {bone: [float(v) for v in nodes[e["node"]].get("rotation", [0, 0, 0, 1])]
            for bone, e in hb.items() if e.get("node") is not None}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--npz", required=True)
    ap.add_argument("--wav", required=True)
    ap.add_argument("--id", required=True)
    ap.add_argument("--emotion", default="neutral")
    ap.add_argument("--template", default=DATASET_TEMPLATE)
    args = ap.parse_args()

    turn_dir = os.path.join(HERE, "outputs", "turns", args.id)
    if os.path.exists(turn_dir):
        raise FileExistsError(
            f"{turn_dir} already exists; choose another id to avoid overwriting")
    os.makedirs(turn_dir)

    started = time.time()
    with np.load(args.npz, allow_pickle=False) as data:
        q_basis = data["vrm_rotations"].astype(np.float64)
        trans = data["trans"].astype(np.float32)
        source_bones = [str(v) for v in data["bone_names"]]
        dataset_id = str(data["dataset_id"])
        split = str(data["split"])
        segment_type = str(data["segment_type"])
        speech_ratio = float(data["speech_ratio"])
    if q_basis.shape[1:] != (NB, 4):
        raise ValueError(f"expected (T,{NB},4) GT rotations, got {q_basis.shape}")
    if source_bones != BONES:
        raise ValueError(f"dataset bone order does not match the E2E {NB}-bone contract")

    # 베이크 산출물 직송 (도크스트링의 write_vrma upperChest 드랍 문제 참조)
    baked_vrma = os.path.normpath(os.path.join(
        os.path.dirname(args.npz), "..", "..", "vrma", split,
        os.path.splitext(os.path.basename(args.npz))[0] + ".vrma"))
    if not os.path.isfile(baked_vrma):
        raise FileNotFoundError(f"baked vrma not found: {baked_vrma}")
    shutil.copyfile(baked_vrma, os.path.join(turn_dir, "body.vrma"))

    t_face_start = time.time()
    blendshapes, face_frames = render_face(
        args.wav, args.emotion, scenario_id=args.id)
    face_s = time.time() - t_face_start
    with open(os.path.join(turn_dir, "face.json"), "w") as f:
        json.dump({
            "scenario_id": args.id,
            "fps": FPS,
            "num_frames": int(face_frames),
            "names": ARKIT_52_NAMES,
            "blendshapes": [
                [round(float(v), 4) for v in row]
                for row in blendshapes
            ],
        }, f)
    shutil.copyfile(args.wav, os.path.join(turn_dir, "audio.wav"))

    meta = {
        "id": args.id,
        "emotion": args.emotion,
        "fps": FPS,
        "face_frames": int(face_frames),
        "body_frames": int(len(q_basis)),
        "face": "face.json",
        "body": "body.vrma",
        "audio": "audio.wav",
        "body_source": "preprocessed_gt_no_codec_no_generator",
        "body_vrma_source": os.path.abspath(baked_vrma),
        "root_translation": "carried",
        "dataset_id": dataset_id,
        "dataset_split": split,
        "source_npz": os.path.abspath(args.npz),
        "segment_type": segment_type,
        "speech_ratio": speech_ratio,
        "timing": {
            "face_s": round(face_s, 2),
            "body_s": 0.0,
            "total_s": round(time.time() - started, 2),
        },
    }
    with open(os.path.join(turn_dir, "meta.json"), "w") as f:
        json.dump(meta, f, ensure_ascii=False, indent=1)
    print(json.dumps(meta, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
