#!/usr/bin/env python3
"""Read EtherGold current-game.json (shared with bridge current_game_ipc.py).

Also writes ``pogcc-boot-arm.json`` when POG-CC reports boot/USB-up via ``/hello``
so EtherGold can force-publish the current index on the next local play.

POG-CC ``/telemetry`` → ``status/pogcc-telemetry.json`` for EtherGold
``machine-status.json`` merge (pog-atlas).
"""

from __future__ import annotations

import json
import os
import tempfile
import time
from datetime import datetime, timezone
from typing import Any, Optional

CURRENT_GAME_NAME = "current-game.json"
DF_DISPLAYED_GAME_NAME = "df-displayed-game.json"
POGCC_BOOT_ARM_NAME = "pogcc-boot-arm.json"
POGCC_TELEMETRY_REL = os.path.join("status", "pogcc-telemetry.json")
DEFAULT_ETHERGOLD_ROOT = "/home/topbox/EtherGold"


def resolve_ethergold_root() -> str:
    explicit = os.environ.get("ETHERGOLD_ROOT", "").strip()
    if explicit:
        return explicit
    # Field default first so DF and EG always share ~/EtherGold/status.
    if os.path.isdir(DEFAULT_ETHERGOLD_ROOT):
        return DEFAULT_ETHERGOLD_ROOT
    base = os.path.dirname(os.path.abspath(__file__))
    sibling = os.path.join(os.path.dirname(base), "EtherGold")
    if os.path.isdir(sibling):
        return sibling
    return DEFAULT_ETHERGOLD_ROOT


def resolve_current_game_path() -> str:
    explicit = os.environ.get("ETHERGOLD_CURRENT_GAME_PATH", "").strip()
    if explicit:
        return explicit
    return os.path.join(resolve_ethergold_root(), CURRENT_GAME_NAME)


def resolve_df_displayed_game_path() -> str:
    explicit = os.environ.get("ETHERGOLD_DF_DISPLAYED_GAME_PATH", "").strip()
    if explicit:
        return explicit
    return os.path.join(resolve_ethergold_root(), DF_DISPLAYED_GAME_NAME)


def write_df_displayed_game(
    game_index: int,
    path: str | None = None,
) -> dict[str, Any]:
    """HDMI game DF is showing (0 = POG logo / menu). Listen compares this on x0."""
    idx = int(game_index)
    if idx < 0 or idx > 16:
        raise ValueError("df displayed game_index must be 0..16")
    target = path or resolve_df_displayed_game_path()
    parent = os.path.dirname(target) or "."
    os.makedirs(parent, exist_ok=True)
    payload: dict[str, Any] = {
        "game_index": idx,
        "updated_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "source": "dyzineframe",
    }
    fd, tmp_path = tempfile.mkstemp(
        dir=parent,
        prefix=".df-displayed-game.",
        suffix=".tmp",
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(payload, handle, separators=(",", ":"))
            handle.write("\n")
        os.replace(tmp_path, target)
        try:
            os.chmod(target, 0o664)
        except OSError:
            pass
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
    return payload


def read_df_displayed_index_this_boot(
    path: str | None = None,
    boot_epoch: float | None = None,
) -> int | None:
    """DF HDMI game this OS boot. None = missing, pre-boot, or junk. 0 = logo."""
    target = path or resolve_df_displayed_game_path()
    boot = os_boot_epoch() if boot_epoch is None else boot_epoch
    try:
        mtime = os.path.getmtime(target)
    except OSError:
        return None
    if boot is not None and mtime < (boot - 2.0):
        return None
    try:
        with open(target, encoding="utf-8") as handle:
            payload = json.load(handle)
    except (OSError, ValueError, json.JSONDecodeError):
        return None
    if not isinstance(payload, dict):
        return None
    try:
        idx = int(payload.get("game_index"))
    except (TypeError, ValueError):
        return None
    if idx < 0 or idx > 16:
        return None
    return idx


PREBOOT_PROGRESSIVE_RUNTIME_NAMES = (
    CURRENT_GAME_NAME,
    "progressives.csv",
    "progressives-sync.json",
)


def os_boot_epoch() -> float | None:
    """Wall-clock time of this OS boot, or None when it cannot be determined."""
    try:
        return time.time() - time.clock_gettime(time.CLOCK_BOOTTIME)
    except (AttributeError, OSError):
        pass
    try:
        with open("/proc/uptime", encoding="ascii") as handle:
            uptime = float(handle.read().split()[0])
        return time.time() - uptime
    except (OSError, ValueError, IndexError):
        return None


def clear_preboot_progressive_runtime(root: str | None = None) -> list[str]:
    """Unlink jackpot CSV / current-game.json written before this OS boot.

    Never show last-boot SuperBall (or any) meter after power-up. Floor-games.json
    stays (enabled-game list, not dollar amounts). Windows OHD is a no-op.
    """
    boot = os_boot_epoch()
    if boot is None:
        return []
    base = root or resolve_ethergold_root()
    cutoff = boot - 2.0
    removed: list[str] = []
    for name in PREBOOT_PROGRESSIVE_RUNTIME_NAMES:
        path = os.path.join(base, name)
        try:
            mtime = os.path.getmtime(path)
        except OSError:
            continue
        if mtime >= cutoff:
            continue
        try:
            os.unlink(path)
        except OSError as exc:
            print(f"WARN: could not unlink stale {path}: {exc}", flush=True)
            continue
        removed.append(path)
        print(f"cleared pre-boot progressive runtime: {path}", flush=True)
    return removed


def resolve_pogcc_boot_arm_path() -> str:
    explicit = os.environ.get("ETHERGOLD_POGCC_BOOT_ARM_PATH", "").strip()
    if explicit:
        return explicit
    return os.path.join(resolve_ethergold_root(), POGCC_BOOT_ARM_NAME)


def resolve_pogcc_telemetry_path() -> str:
    """Durable sidecar under status/ — not overwritten by update.sh tip pulls."""
    explicit = os.environ.get("ETHERGOLD_POGCC_TELEMETRY_PATH", "").strip()
    if explicit:
        return explicit
    status_dir = os.environ.get("ETHERGOLD_STATUS_DIR", "").strip()
    if status_dir:
        return os.path.join(status_dir, "pogcc-telemetry.json")
    return os.path.join(resolve_ethergold_root(), POGCC_TELEMETRY_REL)


def write_pogcc_telemetry(payload: dict[str, Any], path: str | None = None) -> str:
    """Atomic write of Heltec /telemetry fields for EtherGold machine-status merge."""
    target = path or resolve_pogcc_telemetry_path()
    parent = os.path.dirname(target) or "."
    os.makedirs(parent, exist_ok=True)
    try:
        os.chmod(parent, 0o775)
    except OSError:
        pass
    body = dict(payload)
    body.setdefault("schema", "pogcc-telemetry/1")
    body.setdefault(
        "updated_utc",
        datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    )
    fd, tmp_path = tempfile.mkstemp(dir=parent, prefix=".pogcc-telemetry.", suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(body, handle, indent=2)
            handle.write("\n")
        os.replace(tmp_path, target)
        try:
            os.chmod(target, 0o664)
        except OSError:
            pass
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
    return target


def current_game_ipc_consume_enabled() -> bool:
    """DyzineFrame watcher on by default (Linux); set DYZINEFRAME_CURRENT_GAME_IPC=0 to disable."""
    raw = os.environ.get("DYZINEFRAME_CURRENT_GAME_IPC", "1").strip().lower()
    return raw not in ("0", "false", "no", "off")


def read_current_game(path: str | None = None) -> dict[str, Any] | None:
    target = path or resolve_current_game_path()
    if not os.path.isfile(target):
        return None
    try:
        with open(target, encoding="utf-8") as handle:
            payload = json.load(handle)
        if isinstance(payload, dict):
            return payload
    except (OSError, ValueError, json.JSONDecodeError):
        pass
    return None


def this_boot_current_game_payload(
    path: str | None = None,
    boot_epoch: float | None = None,
) -> dict[str, Any] | None:
    """Return current-game.json if it was written this OS boot and names a game.

    Pre-boot leftovers are unlinked separately. A DF process restart must still
    apply this-boot SuperBall (or any 1..16) after splash — the watcher records
    last_index on start and will not SDS until game_index changes.
    """
    target = path or resolve_current_game_path()
    boot = os_boot_epoch() if boot_epoch is None else boot_epoch
    try:
        mtime = os.path.getmtime(target)
    except OSError:
        return None
    if boot is not None and mtime < (boot - 2.0):
        return None
    payload = read_current_game(target)
    idx = game_index_from_payload(payload)
    if idx is None:
        return None
    return payload


def game_index_from_payload(payload: dict[str, Any] | None) -> Optional[int]:
    if not payload:
        return None
    raw = payload.get("game_index")
    try:
        idx = int(raw)
    except (TypeError, ValueError):
        return None
    if idx < 1 or idx > 16:
        return None
    return idx


def write_current_game(
    *,
    game_index: int,
    game_id_hex: str,
    tid_hex: str | None = None,
    source: str = "sds_enter_game",
    path: str | None = None,
) -> dict[str, Any]:
    """Atomically publish current-game.json. SDS Enter Game is the live writer."""
    target = path or resolve_current_game_path()
    parent = os.path.dirname(target) or "."
    os.makedirs(parent, exist_ok=True)
    payload: dict[str, Any] = {
        "game_index": int(game_index),
        "game_id_hex": game_id_hex.strip().upper(),
        "updated_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "source": source,
    }
    if tid_hex:
        payload["tid_hex"] = tid_hex.strip().upper()
    fd, tmp_path = tempfile.mkstemp(
        dir=parent,
        prefix=".current-game.",
        suffix=".tmp",
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(payload, handle, separators=(",", ":"))
            handle.write("\n")
        os.replace(tmp_path, target)
        try:
            os.chmod(target, 0o664)
        except OSError:
            pass
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
    return payload


def write_pogcc_boot_arm(
    *,
    firmware_version: str,
    heltec_local_ms: int | None = None,
    heltec_utc_ms: int | None = None,
    path: str | None = None,
) -> str:
    """Atomic arm file for EtherGold: force-publish on next local gameplay after Heltec boot."""
    target = path or resolve_pogcc_boot_arm_path()
    parent = os.path.dirname(target) or "."
    os.makedirs(parent, exist_ok=True)
    payload: dict[str, Any] = {
        "armed": True,
        "reason": "pogcc_usb_up",
        "firmware_version": (firmware_version or "").strip() or "unknown",
        "armed_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    }
    if heltec_local_ms is not None:
        payload["heltec_local_ms"] = int(heltec_local_ms)
    if heltec_utc_ms is not None and int(heltec_utc_ms) > 0:
        payload["heltec_utc_ms"] = int(heltec_utc_ms)
    fd, tmp_path = tempfile.mkstemp(dir=parent, prefix=".pogcc-boot-arm.", suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(payload, handle, separators=(",", ":"))
            handle.write("\n")
        os.replace(tmp_path, target)
        try:
            os.chmod(target, 0o644)
        except OSError:
            pass
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
    return target
