"""Utilities: preset loading, validation, synthetic preset bootstrap."""
from __future__ import annotations

import json
from pathlib import Path
from typing import Dict

import numpy as np

from .constants import ARKIT_52_NAMES, NAME_TO_IDX

# The blendshape editor (tools/blendshape-editor.html) uses plural/abstract
# channel names that map to multiple ARKit channels. Without this translation,
# any value saved under one of these keys is silently dropped by the loader.
# Discovered via: presets like 'sadness_L3' had 'eyesLookDown'/'mouthSmile'
# values that never reached the rendered output.
EDITOR_META_CHANNELS = {
    'eyesLookDown':   ('eyeLookDownLeft',   'eyeLookDownRight'),
    'eyesLookUp':     ('eyeLookUpLeft',     'eyeLookUpRight'),
    'eyesLookIn':     ('eyeLookInLeft',     'eyeLookInRight'),
    'eyesLookOut':    ('eyeLookOutLeft',    'eyeLookOutRight'),
    'eyesBlink':      ('eyeBlinkLeft',      'eyeBlinkRight'),
    'eyesWide':       ('eyeWideLeft',       'eyeWideRight'),
    'eyesSquint':     ('eyeSquintLeft',     'eyeSquintRight'),
    'mouthSmile':     ('mouthSmileLeft',    'mouthSmileRight'),
    'mouthFrown':     ('mouthFrownLeft',    'mouthFrownRight'),
    'mouthDimple':    ('mouthDimpleLeft',   'mouthDimpleRight'),
    'mouthPress':     ('mouthPressLeft',    'mouthPressRight'),
    'mouthStretch':   ('mouthStretchLeft',  'mouthStretchRight'),
    'mouthLowerDown': ('mouthLowerDownLeft','mouthLowerDownRight'),
    'mouthUpperUp':   ('mouthUpperUpLeft',  'mouthUpperUpRight'),
    'browDown':       ('browDownLeft',      'browDownRight'),
    'browOuterUp':    ('browOuterUpLeft',   'browOuterUpRight'),
    'cheekSquint':    ('cheekSquintLeft',   'cheekSquintRight'),
    'noseSneer':      ('noseSneerLeft',     'noseSneerRight'),
}


def validate_vad(vad) -> np.ndarray:
    """Ensure VAD is shape (3,), float32, in [-1, 1]."""
    vad = np.asarray(vad, dtype=np.float32)
    if vad.shape != (3,):
        raise ValueError(f"VAD shape must be (3,), got {vad.shape}")
    return np.clip(vad, -1.0, 1.0).astype(np.float32)


def validate_presets(presets: Dict[str, dict]) -> None:
    """Raise if any preset is malformed."""
    for name, data in presets.items():
        if "vad" not in data or "bs" not in data:
            raise ValueError(f"preset {name} missing 'vad' or 'bs'")
        vad = np.asarray(data["vad"])
        bs = np.asarray(data["bs"])
        if vad.shape != (3,):
            raise ValueError(f"{name}: vad shape must be (3,), got {vad.shape}")
        if bs.shape != (52,):
            raise ValueError(f"{name}: bs shape must be (52,), got {bs.shape}")
        if np.any((bs < -1e-4) | (bs > 1 + 1e-4)):
            raise ValueError(f"{name}: bs outside [0, 1]")


def load_presets_from_json(path) -> Dict[str, dict]:
    """Load presets from the JSON format exported by tools/blendshape-editor.html.

    Expected schema:
        {"presets": {"joy_L3": {"emotion": "joy", "intensity": 3,
                                 "values": {"mouthSmileLeft": 0.7, ...}}, ...}}
    or flat dict at top level.

    VAD is read from emotion_vad_anchors.json based on (emotion, intensity).
    """
    path = Path(path)
    raw = json.loads(path.read_text(encoding="utf-8"))
    presets_dict = raw.get("presets", raw)

    # Load anchor VAD table
    project_root = Path(__file__).resolve().parents[2]
    anchor_path = project_root / "data" / "emotion" / "emotion_vad_anchors.json"
    anchors_raw = json.loads(anchor_path.read_text(encoding="utf-8"))["anchors"]
    vad_lookup = {}  # (emotion, level) → [V, A, D]
    for emo, entries in anchors_raw.items():
        for e in entries:
            vad_lookup[(emo, int(e["level"]))] = e["vad"]

    out: Dict[str, dict] = {}
    for name, data in presets_dict.items():
        emotion = data.get("emotion") or name.split("_")[0]
        intensity = int(data.get("intensity", 3))

        # VAD from anchor table (fallback to neutral)
        vad = vad_lookup.get((emotion, intensity), [0.0, 0.0, 0.0])

        # Blendshape values as 52-dim vector. Two-pass loading:
        #   1. Apply explicit ARKit names (eyeLookDownLeft, eyeLookDownRight, …).
        #   2. Apply editor meta-channels (eyesLookDown, mouthSmile, …) but
        #      ONLY when meta value > 0. The editor saves both the meta key
        #      and the explicit L/R keys (often defaulted to 0), so meta-wins
        #      semantics are needed when the user authored only the meta.
        # A meta value of 0 is treated as "unset" — explicit values win, which
        # preserves any asymmetric L/R authoring done via direct per-channel UI.
        values_map = data.get("values") or data.get("blendshapes") or {}
        bs = np.zeros(52, dtype=np.float32)
        for bs_name, v in values_map.items():
            if bs_name in NAME_TO_IDX:
                bs[NAME_TO_IDX[bs_name]] = float(v)
        for bs_name, v in values_map.items():
            if bs_name in EDITOR_META_CHANNELS and float(v) > 0:
                for ark in EDITOR_META_CHANNELS[bs_name]:
                    bs[NAME_TO_IDX[ark]] = float(v)

        out[name] = {"vad": list(vad), "bs": bs}

    validate_presets(out)
    return out


def build_synthetic_presets() -> Dict[str, dict]:
    """Bootstrap presets using the parametric layer on each anchor VAD.

    Use this when no user-authored preset JSON is available. Each preset's
    blendshape vector is computed by running the parametric layer on the
    anchor's VAD coordinate. This ensures anchor-reproducibility trivially.
    """
    # Lazy import to avoid circular
    from .parametric import parametric_layer, apply_conflict_resolution

    project_root = Path(__file__).resolve().parents[2]
    anchor_path = project_root / "data" / "emotion" / "emotion_vad_anchors.json"
    anchors_raw = json.loads(anchor_path.read_text(encoding="utf-8"))["anchors"]

    out: Dict[str, dict] = {}
    for emo, entries in anchors_raw.items():
        for e in entries:
            level = int(e["level"])
            vad = np.asarray(e["vad"], dtype=np.float32)
            bs = parametric_layer(vad)
            bs = apply_conflict_resolution(bs, vad)
            name = f"{emo}_L{level}"
            out[name] = {"vad": vad.tolist(), "bs": bs}

    validate_presets(out)
    return out
