#!/usr/bin/env python3
"""Durable machine-status JSON for pog-atlas (and field debug).

Lives under ``$ETHERGOLD_ROOT/status/`` — next to progressives.csv root, **not**
under ``dev/`` / ``sync/`` trees that ``update.sh`` restages.

Sources:
  * ``status/pogcc-telemetry.json`` — written by DyzineFrame from POG-CC
    ``/telemetry/...`` every ~5s (power rails, temps, credits, menu).
  * ``progressives.csv`` — Master 0x0A (all progressive pennies).
  * ``status/last-progressive-hit.json`` — last bank progressive grant
    (progwin>0) noted by the bridge.

Env:
  ETHERGOLD_ROOT                 default /home/topbox/EtherGold
  ETHERGOLD_STATUS_DIR           default $ROOT/status
  ETHERGOLD_MACHINE_STATUS_PATH  default $STATUS_DIR/machine-status.json
  ETHERGOLD_POGCC_TELEMETRY_PATH default $STATUS_DIR/pogcc-telemetry.json
"""

from __future__ import annotations

import csv
import json
import os
import tempfile
import threading
import time
from datetime import datetime, timezone
from typing import Any

# Avoid circular import of full pog422_core at module load when only paths needed.
DEFAULT_ETHERGOLD_ROOT = "/home/topbox/EtherGold"
STATUS_DIR_NAME = "status"
MACHINE_STATUS_NAME = "machine-status.json"
POGCC_TELEMETRY_NAME = "pogcc-telemetry.json"
LAST_HIT_NAME = "last-progressive-hit.json"
SCHEMA = "machine-status/1"

# DyzineFrame VID_NAMES stems (1..16) — shared identity for atlas / OHD.
GAME_INDEX_NAMES: dict[int, str] = {
    0: "POG Menu",
    1: "Jacks or Better Poker",
    2: "Joker Poker",
    3: "Wild Jokers Poker",
    4: "Deuces Wild Poker",
    5: "Shamrock 7's Poker",
    6: "8-Ball Poker",
    7: "Super Gold Bingo",
    8: "Touch 6 Lotto",
    9: "SuperPick Lotto",
    10: "Toucheasy Keno",
    11: "SuperBall Keno",
    12: "Lightning Keno",
    13: "Triple 7's",
    14: "SpinBall Bonus",
    15: "Black Gold 21",
    16: "SpinJack 21",
}

_lock = threading.Lock()
_last_refresh_mono = 0.0


def resolve_ethergold_root() -> str:
    explicit = os.environ.get("ETHERGOLD_ROOT", "").strip()
    if explicit:
        return explicit
    return DEFAULT_ETHERGOLD_ROOT


def resolve_status_dir() -> str:
    explicit = os.environ.get("ETHERGOLD_STATUS_DIR", "").strip()
    if explicit:
        return explicit
    return os.path.join(resolve_ethergold_root(), STATUS_DIR_NAME)


def resolve_machine_status_path() -> str:
    explicit = os.environ.get("ETHERGOLD_MACHINE_STATUS_PATH", "").strip()
    if explicit:
        return explicit
    return os.path.join(resolve_status_dir(), MACHINE_STATUS_NAME)


def resolve_pogcc_telemetry_path() -> str:
    explicit = os.environ.get("ETHERGOLD_POGCC_TELEMETRY_PATH", "").strip()
    if explicit:
        return explicit
    return os.path.join(resolve_status_dir(), POGCC_TELEMETRY_NAME)


def resolve_last_hit_path() -> str:
    explicit = os.environ.get("ETHERGOLD_LAST_PROG_HIT_PATH", "").strip()
    if explicit:
        return explicit
    return os.path.join(resolve_status_dir(), LAST_HIT_NAME)


def game_name_for_index(game_index: int | None) -> str | None:
    if game_index is None:
        return None
    return GAME_INDEX_NAMES.get(int(game_index))


def _atomic_json_write(path: str, payload: dict[str, Any]) -> str:
    parent = os.path.dirname(path) or "."
    os.makedirs(parent, exist_ok=True)
    try:
        os.chmod(parent, 0o775)
        real_user = os.environ.get("ETHERGOLD_REAL_USER", "topbox")
        import pwd

        pw = pwd.getpwnam(real_user)
        os.chown(parent, pw.pw_uid, pw.pw_gid)
    except (OSError, KeyError, ImportError):
        pass
    fd, tmp_path = tempfile.mkstemp(dir=parent, prefix=".machine-status.", suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(payload, handle, indent=2, sort_keys=False)
            handle.write("\n")
        os.replace(tmp_path, path)
        try:
            os.chmod(path, 0o664)
            real_user = os.environ.get("ETHERGOLD_REAL_USER", "topbox")
            import pwd

            pw = pwd.getpwnam(real_user)
            os.chown(path, pw.pw_uid, pw.pw_gid)
        except (OSError, KeyError, ImportError):
            pass
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
    return path


def _read_json(path: str) -> dict[str, Any] | None:
    if not os.path.isfile(path):
        return None
    try:
        with open(path, encoding="utf-8") as handle:
            data = json.load(handle)
        if isinstance(data, dict):
            return data
    except (OSError, ValueError, json.JSONDecodeError):
        pass
    return None


def _read_progressives(csv_path: str) -> dict[str, Any]:
    """Load 1..16 pennies from progressives.csv (+ names)."""
    games: dict[str, Any] = {}
    updated_utc: str | None = None
    if not os.path.isfile(csv_path):
        return {"updated_utc": None, "games": games}
    try:
        with open(csv_path, newline="", encoding="utf-8") as handle:
            reader = csv.DictReader(handle)
            for row in reader:
                try:
                    gid = int((row.get("game_id") or "").strip())
                    pennies = int((row.get("pennies") or "0").strip())
                except (TypeError, ValueError):
                    continue
                if gid < 1 or gid > 16:
                    continue
                stamp = (row.get("updated_utc") or "").strip() or None
                if stamp:
                    updated_utc = stamp
                dollars = pennies / 100.0
                games[str(gid)] = {
                    "game_index": gid,
                    "game_name": game_name_for_index(gid),
                    "pennies": pennies,
                    "dollars": round(dollars, 2),
                }
    except OSError:
        pass
    return {"updated_utc": updated_utc, "games": games}


def write_pogcc_telemetry(payload: dict[str, Any], path: str | None = None) -> str:
    """Atomic write of Heltec-origin fields (called from DyzineFrame or tests)."""
    target = path or resolve_pogcc_telemetry_path()
    body = dict(payload)
    body.setdefault(
        "updated_utc",
        datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    )
    body.setdefault("schema", "pogcc-telemetry/1")
    return _atomic_json_write(target, body)


def note_progressive_hit(
    *,
    game_index: int | None,
    game_id_hex: str | None = None,
    progwin_cents: int = 0,
    tid: str | None = None,
    path: str | None = None,
    source: str = "bank_jpot",
) -> str:
    """Persist last progressive hit (overwrites)."""
    target = path or resolve_last_hit_path()
    payload: dict[str, Any] = {
        "schema": "last-progressive-hit/1",
        "hit_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "source": source,
        "progwin_cents": int(progwin_cents),
        "progwin_dollars": round(int(progwin_cents) / 100.0, 2),
    }
    if game_index is not None:
        payload["game_index"] = int(game_index)
        payload["game_name"] = game_name_for_index(int(game_index))
    if game_id_hex:
        payload["game_id_hex"] = game_id_hex.strip().upper()
    if tid:
        payload["tid"] = tid.strip().upper()
    _atomic_json_write(target, payload)
    publish_machine_status(force=True)
    return target


def publish_machine_status(
    *,
    force: bool = False,
    min_interval_s: float = 5.0,
    progressives_csv: str | None = None,
) -> str | None:
    """Merge telemetry + progressives + last hit → machine-status.json.

    Throttled to ~min_interval_s unless force=True.
    """
    global _last_refresh_mono
    now = time.monotonic()
    with _lock:
        if not force and (now - _last_refresh_mono) < min_interval_s:
            return None
        _last_refresh_mono = now

    try:
        from pog422_core import EG_VERSION, EG_VERSION_SLUG, resolve_progressives_csv_path
    except ImportError:
        EG_VERSION = "unknown"
        EG_VERSION_SLUG = "unknown"

        def resolve_progressives_csv_path(explicit=None):  # type: ignore
            if explicit:
                return explicit
            return os.path.join(resolve_ethergold_root(), "progressives.csv")

    csv_path = progressives_csv or resolve_progressives_csv_path()
    tele_path = resolve_pogcc_telemetry_path()
    tele = _read_json(tele_path) or {}
    hit = _read_json(resolve_last_hit_path())
    progs = _read_progressives(csv_path)

    stamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    payload: dict[str, Any] = {
        "schema": SCHEMA,
        "updated_utc": stamp,
        "eg_version": EG_VERSION,
        "eg_version_slug": EG_VERSION_SLUG,
        "pogcc": tele or None,
        "pogcc_telemetry_path": tele_path,
        "pogcc_telemetry_present": bool(tele),
        "progressives": progs,
        "last_progressive_hit": hit,
    }
    if not tele:
        payload["pogcc_hint"] = (
            "No pogcc-telemetry.json. Heltec 0706+ sends /telemetry on USB CDC; "
            "DyzineFrame 2026-08-07-b+ must be running and writing this path. "
            "Check DF log for 'invalid command: telemetry' (old DF) or "
            "'telemetry write failed' (permissions)."
        )
    path = resolve_machine_status_path()
    try:
        _atomic_json_write(path, payload)
    except OSError:
        return None
    return path


def maybe_refresh_machine_status(min_interval_s: float = 5.0) -> str | None:
    return publish_machine_status(force=False, min_interval_s=min_interval_s)


def notify_progressives_changed() -> str | None:
    return publish_machine_status(force=True)
