#!/usr/bin/env python3
"""NEXT Athlete OS · RC49 special prop binding.

Builds on RC48's green expansion. This pass targets the held special cases:
med-ball/throw object binding, box prop anchors and buddy hamstring knee pivot.
It keeps wrong-family placeholders gated rather than promoting them as real
exercise movement truth.
"""
from __future__ import annotations

import json
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "assets" / "data" / "image-admin"
QA = ROOT / "qa_screenshots"
SEQ_PATH = DATA / "pose-atlas-exercise-sequences.json"
PACKS_PATH = DATA / "pose-atlas-packs.json"
OUT_DATA = DATA / "rc49_special_prop_binding.json"
OUT_REPORT = QA / "rc49_special_prop_binding_report.json"
BACKUP_SEQ = DATA / "pose-atlas-exercise-sequences.rc48-before-rc49.backup.json"
BACKUP_PACKS = DATA / "pose-atlas-packs.rc48-before-rc49.backup.json"

RC49_VERSION = "v97-rc49-special-prop-binding"
NOW = datetime.now(timezone.utc).isoformat(timespec="seconds")


def load_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def save_json(path: Path, data: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def limb(anchor: str, *, role: str, confidence: float = 0.99, source: str = "rc49-special-prop-binding", object_anchor: str | None = None) -> Dict[str, Any]:
    out: Dict[str, Any] = {
        "schema": "rc49-special-prop-intent-v1",
        "primaryAnchor": anchor,
        "anchorSource": source,
        "movementRole": role,
        "confidence": confidence,
        "linkedToSpriteLandmarks": True,
        "coordinateSpace": "sprite-percent-unflipped",
        "runtimeTransformPolicy": "skeleton and sprite share the same anchor + flip transform",
        "note": "Exercise/prop movement truth controls anchor and object binding. Pose/sheet names are not authoritative.",
    }
    if object_anchor:
        out["objectAnchor"] = object_anchor
    return out


def prop_binding(kind: str, *, anchor: str, object_anchor: str | None = None, phase: str = "", review: str = "", gate: str = "") -> Dict[str, Any]:
    out: Dict[str, Any] = {
        "schema": "rc49-prop-binding-v1",
        "kind": kind,
        "worldAnchor": anchor,
        "phase": phase,
        "runtimeGate": gate,
        "reviewNote": review,
        "poseNameAuthority": False,
    }
    if object_anchor:
        out["objectAnchor"] = object_anchor
    return out


def tag_seq(seq: Dict[str, Any], status: str, decision: str, *, risk: str = "moderate", gate: str = "special-prop-bound") -> None:
    seq["rc49SpecialPropBinding"] = {
        "version": RC49_VERSION,
        "status": status,
        "decision": decision,
        "riskTier": risk,
        "runtimeGate": gate,
        "source": "RC48 green expansion + AI physio movement truth + MediaPipe candidate geometry",
        "poseNameAuthority": False,
        "updatedAt": NOW,
    }
    seq["latestMovementGate"] = gate


def rewrite_entry(entry: Dict[str, Any], *, phase: str, path_t: float, contact: str, anchor: str, role: str, prop: Dict[str, Any] | None = None, object_anchor: str | None = None) -> None:
    entry["phase"] = phase
    entry["pathT"] = round(float(path_t), 3)
    entry["contact"] = contact
    entry["anchorLandmark"] = anchor
    entry["limbIntent"] = limb(anchor, role=role, object_anchor=object_anchor)
    if prop:
        entry["propBinding"] = prop
    entry["rc49Frame"] = {
        "phase": phase,
        "pathT": entry["pathT"],
        "contact": contact,
        "anchorLandmark": anchor,
        "objectAnchor": object_anchor,
        "movementRole": role,
        "reviewNote": "Special prop/partner pass. Runtime activation only where object/anchor identity is safe.",
    }


def spread(n: int, preset: List[float] | None = None) -> List[float]:
    if preset and len(preset) == n:
        return preset
    if n <= 1:
        return [0.0]
    return [i / (n - 1) for i in range(n)]


def apply_sequence_updates(sequences: Dict[str, Any]) -> List[Dict[str, Any]]:
    changes: List[Dict[str, Any]] = []

    def change(key: str, action: str, impact: str, gate: str) -> None:
        changes.append({"exerciseKey": key, "action": action, "runtimeImpact": impact, "runtimeGate": gate})

    # Med-ball slam is a true sequence in the current source atlas. Promote it to prop-bound
    # with feet as world anchor and hands as object anchor. MediaPipe activation is done in packs.
    seq = sequences.get("med-ball-slam")
    if seq:
        phases = [
            "ready-ball-high", "load-hinge", "accelerate-down", "slam-contact", "follow-through",
            "recover-low", "lift-from-floor", "return-overhead", "reset-ready",
        ]
        roles = [
            "ball held high; feet planted", "hinge/load with ball tracked between hands", "hands accelerate ball down",
            "slam/contact phase; feet stay world anchor", "follow-through without foot drift", "low recovery",
            "lift ball from floor", "return ball high", "stable reset",
        ]
        contacts = ["grounded", "grounded", "grounded", "grounded/object-contact", "grounded", "grounded", "grounded", "grounded", "grounded"]
        poses = seq.get("poses", [])
        ts = spread(len(poses), [0, .10, .23, .36, .50, .63, .75, .88, 1] if len(poses) == 9 else None)
        for i, entry in enumerate(poses):
            phase = phases[i] if i < len(phases) else f"medball-slam-{i+1}"
            rewrite_entry(
                entry,
                phase=phase,
                path_t=ts[i],
                contact=contacts[i] if i < len(contacts) else "grounded",
                anchor="footCenter",
                object_anchor="handCenter",
                role=roles[i] if i < len(roles) else "med-ball slam prop-bound keyframe",
                prop=prop_binding("medball", anchor="footCenter", object_anchor="handCenter", phase=phase, review="Ball is bound to handCenter; feet remain the world anchor.", gate="special-prop-active"),
            )
        seq.setdefault("motionProfile", {}).update({
            "family": "hinge_slam_lift",
            "anchor": "feet-with-medball-hand-binding",
            "objectAnchor": "handCenter",
            "x0": 50,
            "x1": 50,
            "loopMs": 2250,
            "ghostCount": 0,
            "ghostMode": "none",
            "debug": True,
        })
        seq["propModel"] = {
            "schema": "rc49-prop-model-v1",
            "kind": "medball",
            "worldAnchor": "footCenter",
            "objectAnchor": "handCenter",
            "activationPolicy": "MediaPipe may activate runtime landmarks for the athlete; the ball remains prop-bound to hands, not independently detected.",
        }
        tag_seq(seq, "special-prop-active", "Med-ball slam is now bound by feet + hand/object intent; no pose-name authority.", risk="moderate", gate="special-prop-active")
        change("med-ball-slam", "medball object binding promoted", "runtime landmarks activated only where hand/foot confidence passes gate", "special-prop-active")

    # Box variants keep sprite/prop visual/manual, but get explicit prop anchors and frame semantics.
    box_specs = {
        "box-jump": {
            "phases": ["stand-before-box", "load-before-box", "takeoff-to-box", "airborne-to-box", "land-on-box", "stand-on-box"],
            "contacts": ["grounded", "ground/load", "takeoff", "airborne", "box-land/reset", "box-grounded"],
            "preset": [0, .12, .28, .52, .82, 1],
            "decision": "Box prop is explicitly modelled. MediaPipe runtime remains held because full-body/prop detections vary by athlete/frame.",
        },
        "seated-box-jump": {
            "phases": ["seated-ready", "countermovement-from-box", "takeoff-from-box", "airborne", "land-reset"],
            "contacts": ["box-seated", "box-load", "takeoff", "airborne", "land/reset"],
            "preset": [0, .18, .38, .62, 1],
            "decision": "Seated box jump gets box-seat/landing prop semantics; MediaPipe runtime remains manual/held.",
        },
    }
    for key, cfg in box_specs.items():
        seq = sequences.get(key)
        if not seq:
            continue
        poses = seq.get("poses", [])
        ts = spread(len(poses), cfg["preset"] if len(cfg["preset"]) == len(poses) else None)
        for i, entry in enumerate(poses):
            phase = cfg["phases"][i] if i < len(cfg["phases"]) else f"{key}-{i+1}"
            rewrite_entry(
                entry,
                phase=phase,
                path_t=ts[i],
                contact=cfg["contacts"][i] if i < len(cfg["contacts"]) else "grounded",
                anchor="footCenter",
                role="foot pair relative to fixed box prop; do not infer prop from foot-only MediaPipe",
                prop=prop_binding("box", anchor="footCenter", phase=phase, review="Box is a fixed visual prop/landing target; athlete footCenter is bound relative to it.", gate="manual-prop-locked"),
            )
        seq.setdefault("motionProfile", {}).update({"anchor": "foot-pair-box-prop-lock", "ghostCount": 0, "ghostMode": "none", "debug": True})
        seq["propModel"] = {
            "schema": "rc49-prop-model-v1",
            "kind": "box",
            "worldAnchor": "stageBox",
            "athleteAnchor": "footCenter",
            "activationPolicy": "Manual prop lock. Do not activate full MediaPipe runtime until box/foot contact is visually reviewed.",
        }
        tag_seq(seq, "manual-prop-locked", cfg["decision"], risk="moderate-high", gate="manual-prop-locked")
        change(key, "box prop semantics added", "no new MediaPipe runtime activation; anchor/fas is now explicit", "manual-prop-locked")

    # Buddy hamstring curl: fixed knee pivot; partner/contact is a non-MediaPipe prop.
    seq = sequences.get("buddy-hamstring-curls")
    if seq:
        phases = ["kneeling-start-partner-lock", "eccentric-lower-from-knees", "catch/return-from-knees", "reset-knee-pivot"]
        contacts = ["fixed-pivot", "fixed-pivot", "fixed-pivot", "fixed-pivot"]
        poses = seq.get("poses", [])
        ts = spread(len(poses), [0, .34, .72, 1] if len(poses) == 4 else None)
        for i, entry in enumerate(poses):
            phase = phases[i] if i < len(phases) else f"buddy-hamstring-{i+1}"
            rewrite_entry(
                entry,
                phase=phase,
                path_t=ts[i],
                contact=contacts[i] if i < len(contacts) else "fixed-pivot",
                anchor="kneeCenter",
                role="knees are the pivot; partner/ankle lock is manual and must not be inferred from MediaPipe",
                prop=prop_binding("partner-ankle-lock", anchor="kneeCenter", phase=phase, review="Partner/ankle lock treated as fixed support; MediaPipe runtime remains inactive.", gate="manual-knee-pivot-locked"),
            )
        seq.setdefault("motionProfile", {}).update({"family": "fixed_knee_pivot_sequence", "anchor": "knee-pivot-partner-lock", "x0": 50, "x1": 50, "loopMs": 2100, "ghostCount": 0, "ghostMode": "none", "debug": True})
        seq["propModel"] = {
            "schema": "rc49-prop-model-v1",
            "kind": "partner-ankle-lock",
            "worldAnchor": "partnerFeet/ankleHold",
            "athleteAnchor": "kneeCenter",
            "activationPolicy": "Manual knee pivot. MediaPipe candidate is retained only as visual aid, not runtime truth.",
        }
        tag_seq(seq, "manual-knee-pivot-locked", "Buddy hamstring curl locked to knee pivot + partner support semantics; no MediaPipe promotion.", risk="high", gate="manual-knee-pivot-locked")
        change("buddy-hamstring-curls", "partner/knee pivot semantics added", "no new MediaPipe runtime activation; manual knee pivot retained", "manual-knee-pivot-locked")

    # Wrong-family throw placeholders remain gated, but now have clear true-movement requirements for future image selection.
    throw_specs = {
        "chest-pass": {
            "kind": "medball/chest-pass",
            "requirements": ["ball starts at chest", "hands push straight forward", "feet/hips stay stable", "no overhead slam or floor contact"],
            "decision": "Current images are still med-ball-slam placeholders. Keep hidden/held from true runtime expansion until matching chest-pass candidates exist.",
        },
        "rotational-throw": {
            "kind": "medball/rotational-throw",
            "requirements": ["hip/shoulder rotation visible", "ball travels across body", "rear foot pivots", "no vertical slam pattern"],
            "decision": "Current images are still med-ball-slam placeholders. Keep review-hold until true rotational throw sequence exists.",
        },
    }
    for key, cfg in throw_specs.items():
        seq = sequences.get(key)
        if not seq:
            continue
        poses = seq.get("poses", [])
        ts = spread(len(poses))
        for i, entry in enumerate(poses):
            rewrite_entry(
                entry,
                phase=f"placeholder-{key}-{i+1}",
                path_t=ts[i],
                contact="review-hold",
                anchor="footCenter",
                object_anchor="handCenter",
                role="placeholder only; not exercise movement truth",
                prop=prop_binding(cfg["kind"], anchor="footCenter", object_anchor="handCenter", phase="placeholder", review="Wrong-family candidate; use only in review lab, not as approved movement.", gate="review-hold-needs-true-pose-set"),
            )
        seq["trueMovementRequirements"] = cfg["requirements"]
        seq["propModel"] = {
            "schema": "rc49-prop-model-v1",
            "kind": cfg["kind"],
            "worldAnchor": "footCenter",
            "objectAnchor": "handCenter",
            "activationPolicy": "Review hold. Needs new/source-matched pose candidates before runtime promotion.",
        }
        tag_seq(seq, "review-hold-needs-true-pose-set", cfg["decision"], risk="high", gate="review-hold-needs-true-pose-set")
        change(key, "throw placeholder gated with true-movement requirements", "runtime remains held; lab documents what future candidates must match", "review-hold-needs-true-pose-set")

    # Broad jump remains a non-prop special: keep it explicitly gated/manual because one frame was below green threshold.
    seq = sequences.get("broad-jump")
    if seq:
        seq["rc49SpecialPropBinding"] = {
            "version": RC49_VERSION,
            "status": "manual-review-retained",
            "decision": "Broad jump remains manual foot-pair review until landing/flight sequence is visually checked.",
            "riskTier": "moderate",
            "runtimeGate": "manual-review-retained",
            "source": "RC48 gate carried forward",
            "poseNameAuthority": False,
            "updatedAt": NOW,
        }
        seq["latestMovementGate"] = "manual-review-retained"
        change("broad-jump", "manual review retained", "no new activation", "manual-review-retained")

    return changes


def activate_mediapipe_for_medball(packs: Dict[str, Any]) -> Dict[str, Any]:
    activated: List[Dict[str, Any]] = []
    skipped: List[Dict[str, Any]] = []
    medball_pose_keys = {f"sheet_10_medball_throw_slam__pose{i:02d}" for i in [1,2,3,4,5,6,8,10,12]}
    for pose in packs.get("poses", []):
        pk = pose.get("poseKey")
        if pk not in medball_pose_keys:
            continue
        for athlete_id, athlete in (pose.get("athletes") or {}).items():
            sk = (((athlete.get("metrics") or {}).get("skeleton")) or {})
            cand = sk.get("runtimeLandmarksMediapipeCandidate") or {}
            foot = cand.get("footCenter") or {}
            hand = cand.get("handCenter") or {}
            hip = cand.get("hipCenter") or {}
            min_conf = min(float(foot.get("confidence", 0) or 0), float(hand.get("confidence", 0) or 0), float(hip.get("confidence", 0) or 0))
            if min_conf >= 0.72:
                if "runtimeLandmarksAlphaFallback" not in sk:
                    sk["runtimeLandmarksAlphaFallback"] = deepcopy(sk.get("runtimeLandmarks", {}))
                sk["runtimeLandmarks"] = deepcopy(cand)
                sk["runtimeLandmarkSource"] = "mediapipe-rc49-medball-prop-bound"
                sk["mediaPipeRuntimeActive"] = True
                sk["mediaPipeRuntimeGate"] = {
                    "version": RC49_VERSION,
                    "exercise": "med-ball-slam",
                    "policy": "active-special-prop-bound",
                    "minConfidence": round(min_conf, 3),
                    "reason": "Foot/hand/hip landmarks pass med-ball slam prop-binding threshold; ball remains bound to handCenter, not independently detected.",
                }
                activated.append({"poseKey": pk, "athleteId": athlete_id, "minConfidence": round(min_conf, 3)})
            else:
                sk["mediaPipeRuntimeActive"] = False
                sk["mediaPipeRuntimeGate"] = {
                    "version": RC49_VERSION,
                    "exercise": "med-ball-slam",
                    "policy": "held-low-confidence-special-prop",
                    "minConfidence": round(min_conf, 3),
                }
                skipped.append({"poseKey": pk, "athleteId": athlete_id, "minConfidence": round(min_conf, 3)})
    return {"activated": activated, "skipped": skipped, "targetPoseKeys": sorted(medball_pose_keys)}


def main() -> None:
    QA.mkdir(exist_ok=True)
    seq_data = load_json(SEQ_PATH)
    packs = load_json(PACKS_PATH)
    if not BACKUP_SEQ.exists():
        save_json(BACKUP_SEQ, seq_data)
    if not BACKUP_PACKS.exists():
        save_json(BACKUP_PACKS, packs)

    seq_data["version"] = RC49_VERSION
    seq_data["limbIntentSchema"] = "rc49-special-prop-intent-v1"
    changes = apply_sequence_updates(seq_data.get("sequences", {}))
    activation = activate_mediapipe_for_medball(packs)

    report = {
        "version": RC49_VERSION,
        "createdAt": NOW,
        "principle": "Special props/partner/object movements are promoted only when the movement truth and prop binding are explicit. Wrong-family placeholders remain held.",
        "sequenceChanges": len(changes),
        "changes": changes,
        "medballMediaPipeActivation": {
            "activated": len(activation["activated"]),
            "skipped": len(activation["skipped"]),
            "items": activation["activated"],
            "skippedItems": activation["skipped"],
        },
        "heldOrManual": [
            "box-jump",
            "seated-box-jump",
            "buddy-hamstring-curls",
            "chest-pass",
            "rotational-throw",
            "broad-jump",
        ],
        "runtimePolicy": {
            "newRuntimeActivation": "med-ball-slam only, confidence-gated",
            "manualPropLocked": ["box-jump", "seated-box-jump", "buddy-hamstring-curls"],
            "reviewHold": ["chest-pass", "rotational-throw"],
            "noImageGeneration": True,
        },
    }
    seq_data["rc49SpecialPropBinding"] = report

    save_json(SEQ_PATH, seq_data)
    save_json(PACKS_PATH, packs)
    save_json(OUT_DATA, report)
    save_json(OUT_REPORT, report)
    print(json.dumps({"ok": True, "version": RC49_VERSION, "sequenceChanges": len(changes), "medballActivated": len(activation["activated"]), "medballSkipped": len(activation["skipped"])}, indent=2))


if __name__ == "__main__":
    main()
