"""Recognition state file — atomic JSON I/O shared by parent + child.

The dashboard (parent process) writes this file on every toggle / face
gallery change; the Gemini child (`gemini/script.py`) polls it at 1 Hz
to flip its in-memory flags without a session restart.

Format (data/.recognition_state.json):
    {
        "vision_enabled":      bool,
        "face_rec_enabled":    bool,
        "gallery_version":     int,   # bumped on every face CRUD
        "zone_rec_enabled":    bool,  # N3 — zones/places knowledge toggle
        "zones_version":       int,   # bumped on every zone/place CRUD
        "nav_target_zone_id":  int,   # active "go here" destination (0 = none)
        "nav_target_place_id": int,
        "movement_enabled":    bool   # N2 — Gemini-driven locomotion gate
    }
"""

from __future__ import annotations

import json
import os
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path


@dataclass
class RecognitionState:
    vision_enabled: bool = False
    face_rec_enabled: bool = False
    gallery_version: int = 0
    # N3 — zones/places knowledge (zone → place → linked faces)
    zone_rec_enabled: bool = False
    zones_version: int = 0
    # "Go here" destination — the active place the robot should head to.
    # 0/0 = no destination set. Actual locomotion is wired by N2.
    nav_target_zone_id: int = 0
    nav_target_place_id: int = 0
    # N2 — Gemini-driven locomotion enable gate (default OFF for safety)
    movement_enabled: bool = False
    # Auto-record every conversation turn to data/recordings/ (default ON to
    # match historical behavior). Toggled live from the Live Gemini panel; the
    # child syncs TurnRecorder.enabled to this without a session restart.
    record_enabled: bool = True


def read(path: Path) -> RecognitionState:
    """Return the persisted state, or a default if missing/corrupt."""
    try:
        raw = json.loads(Path(path).read_text(encoding="utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError,
            UnicodeDecodeError, ValueError):
        # UnicodeDecodeError (bad UTF-8 bytes) and ValueError (e.g. a non-dict
        # top-level) are not OSError/JSONDecodeError subclasses — catch them too
        # so read() honours its "never raises" contract on a corrupt file.
        return RecognitionState()
    if not isinstance(raw, dict):
        return RecognitionState()
    return RecognitionState(
        vision_enabled=bool(raw.get("vision_enabled", False)),
        face_rec_enabled=bool(raw.get("face_rec_enabled", False)),
        gallery_version=int(raw.get("gallery_version", 0)),
        zone_rec_enabled=bool(raw.get("zone_rec_enabled", False)),
        zones_version=int(raw.get("zones_version", 0)),
        nav_target_zone_id=int(raw.get("nav_target_zone_id", 0)),
        nav_target_place_id=int(raw.get("nav_target_place_id", 0)),
        movement_enabled=bool(raw.get("movement_enabled", False)),
        record_enabled=bool(raw.get("record_enabled", True)),
    )


def write(path: Path, state: RecognitionState) -> None:
    """Write atomically via tempfile + os.replace."""
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp = tempfile.mkstemp(prefix=f".{p.name}.", suffix=".tmp", dir=str(p.parent))
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            json.dump(asdict(state), fh, ensure_ascii=False, indent=2)
        os.replace(tmp, p)
    except Exception:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise


def mutate(path: Path, **changes) -> RecognitionState:
    """Read-modify-write helper. Returns the new state."""
    cur = read(path)
    for k, v in changes.items():
        if hasattr(cur, k):
            setattr(cur, k, v)
    write(path, cur)
    return cur
