"""Skill Registry — maps audio files to motion commands and callback functions.

A "skill" is a named unit that ties together:
  - An audio clip  (e.g. recordings/audio/intro.wav)
  - A motion file  (e.g. data/motions/wave.jsonl)   — optional
  - A callback     (e.g. "motion.wave_hand")         — resolved at runtime

The registry is persisted in data/skills.json and can be edited via the
dashboard or programmatically through the Brain.

Skill entry schema:
{
    "id":            "intro_greeting",
    "audio_file":    "recordings/audio/intro.wav",
    "motion_file":   "data/motions/right_hand_up.jsonl",
    "callback":      "motion.trigger:wave_hand",
    "sync_mode":     "parallel",        # parallel | audio_first | motion_first
    "enabled":       true,
    "description":   "Wave hand while playing intro audio"
}
"""

from __future__ import annotations

import json
import os
import tempfile
import threading
import uuid
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any

from Project.Sanad.config import SKILLS_FILE
from Project.Sanad.core.logger import get_logger

log = get_logger("skill_registry")


@dataclass
class Skill:
    id: str
    audio_file: str = ""
    motion_file: str = ""
    callback: str = ""
    sync_mode: str = "parallel"
    enabled: bool = True
    description: str = ""
    meta: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Skill:
        known = {f.name for f in cls.__dataclass_fields__.values()}
        filtered = {k: v for k, v in data.items() if k in known}
        return cls(**filtered)


class SkillRegistry:
    """Thread-safe, JSON-backed registry of skills."""

    def __init__(self, path: Path = SKILLS_FILE):
        self._path = path
        self._lock = threading.Lock()
        self._skills: dict[str, Skill] = {}
        self._load()

    # -- persistence --

    def _load(self):
        if not self._path.exists():
            self._skills = {}
            return
        try:
            with open(self._path, "r", encoding="utf-8") as f:
                payload = json.load(f)
            for entry in payload.get("skills", []):
                skill = Skill.from_dict(entry)
                self._skills[skill.id] = skill
            log.info("Loaded %d skills from %s", len(self._skills), self._path)
        except Exception as exc:
            log.warning("Could not load skills: %s", exc)
            # Backup corrupt file rather than silently nuking
            try:
                self._path.rename(self._path.with_suffix(".json.corrupt"))
                log.warning("Backed up corrupt skills to %s.corrupt", self._path)
            except OSError:
                pass
            self._skills = {}

    _VALID_SYNC_MODES = {"parallel", "audio_first", "motion_first"}

    def _save(self):
        self._path.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            "version": 1,
            "total": len(self._skills),
            "skills": [s.to_dict() for s in self._skills.values()],
        }
        # Atomic write: tempfile + os.replace
        fd, tmp = tempfile.mkstemp(
            prefix=f".{self._path.name}.", suffix=".tmp",
            dir=str(self._path.parent),
        )
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as f:
                json.dump(payload, f, ensure_ascii=False, indent=2)
            os.replace(tmp, self._path)
        except Exception:
            try:
                os.unlink(tmp)
            except OSError:
                pass
            raise

    # -- CRUD --

    def list_skills(self) -> list[dict[str, Any]]:
        with self._lock:
            return [s.to_dict() for s in self._skills.values()]

    def get(self, skill_id: str) -> Skill | None:
        with self._lock:
            return self._skills.get(skill_id)

    def add(self, skill: Skill) -> Skill:
        if skill.sync_mode not in self._VALID_SYNC_MODES:
            raise ValueError(
                f"Invalid sync_mode '{skill.sync_mode}' (allowed: {sorted(self._VALID_SYNC_MODES)})"
            )
        with self._lock:
            if not skill.id:
                skill.id = uuid.uuid4().hex[:12]
            elif skill.id in self._skills:
                raise ValueError(f"Skill id already exists: {skill.id}")
            self._skills[skill.id] = skill
            self._save()
            log.info("Added skill %s (%s)", skill.id, skill.description)
            return skill

    def update(self, skill_id: str, updates: dict[str, Any]) -> Skill | None:
        with self._lock:
            existing = self._skills.get(skill_id)
            if existing is None:
                return None
            if "sync_mode" in updates and updates["sync_mode"] not in self._VALID_SYNC_MODES:
                raise ValueError(
                    f"Invalid sync_mode '{updates['sync_mode']}'"
                )
            for key, value in updates.items():
                if hasattr(existing, key) and key != "id":
                    setattr(existing, key, value)
            self._save()
            log.info("Updated skill %s", skill_id)
            return existing

    def delete(self, skill_id: str) -> dict[str, Any] | None:
        with self._lock:
            skill = self._skills.pop(skill_id, None)
            if skill is None:
                return None
            self._save()
            log.info("Deleted skill %s", skill_id)
            return skill.to_dict()

    def find_by_audio(self, audio_file: str) -> list[Skill]:
        """Find all skills linked to a given audio file."""
        with self._lock:
            return [s for s in self._skills.values() if s.audio_file == audio_file and s.enabled]

    def find_by_callback(self, callback: str) -> list[Skill]:
        with self._lock:
            return [s for s in self._skills.values() if s.callback == callback and s.enabled]
