#!/usr/bin/env python3
"""Fetch P3 GAMESTATS once and refresh ~/EtherGold/floor-games.json."""
from __future__ import annotations

import argparse
import fcntl
import os
import sys
import time

from floor_games_cache import (
    build_payload_from_config,
    build_payload_from_p3,
    load_cached_payload,
    save_cached_payload,
)
from p3_gamestats import GAMESTATS_CMD, READ_UNTIL, normalize_gamestats_bytes, parse_gamestats_csv

DEFAULT_SERIAL = "/dev/ttyUSB0"
LOCK_CANDIDATES = (
    os.environ.get("ETHERGOLD_P3_SERIAL_LOCK", "").strip(),
    "/tmp/ethergold-p3-serial.lock",
    "/var/lib/ethergold/p3-serial.lock",
)


def _acquire_serial_lock(timeout_s: float) -> int | None:
    """Flock a P3 serial lock file. /var/run is root-only on field images."""
    last_err: OSError | None = None
    for path in LOCK_CANDIDATES:
        if not path:
            continue
        try:
            parent = os.path.dirname(path) or "."
            os.makedirs(parent, exist_ok=True)
            lock_fd = os.open(path, os.O_CREAT | os.O_RDWR, 0o666)
        except OSError as exc:
            last_err = exc
            continue
        deadline = time.monotonic() + max(0.0, timeout_s)
        while True:
            try:
                fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
                return lock_fd
            except BlockingIOError:
                if time.monotonic() >= deadline:
                    os.close(lock_fd)
                    break
                time.sleep(0.25)
    if last_err is not None:
        print(f"WARN: P3 serial lock: {last_err}", file=sys.stderr)
    return None


def fetch_gamestats_text(
    *,
    serial_dev: str,
    baud: int = 115200,
    timeout_s: float = 8.0,
    lock_timeout_s: float = 30.0,
) -> str | None:
    try:
        import serial
    except ImportError:
        print("ERROR: pyserial not installed", file=sys.stderr)
        return None

    lock_fd = _acquire_serial_lock(lock_timeout_s)
    if lock_fd is None:
        print("WARN: P3 serial busy (lock timeout); keeping existing floor-games cache", file=sys.stderr)
        return None

    cable = None
    try:
        cable = serial.Serial(
            port=serial_dev,
            baudrate=baud,
            bytesize=8,
            timeout=timeout_s,
            stopbits=serial.STOPBITS_ONE,
        )
        if not cable.is_open:
            cable.open()
        cable.write(GAMESTATS_CMD)
        raw_data = cable.read_until(expected=READ_UNTIL)
        if not raw_data:
            print("ERROR: P3 read_until returned no data", file=sys.stderr)
            return None
        if not raw_data.endswith(READ_UNTIL):
            print(
                f"ERROR: incomplete P3 GAMESTATS response "
                f"({len(raw_data)} bytes; END terminator missing)",
                file=sys.stderr,
            )
            return None
        return normalize_gamestats_bytes(raw_data)
    except Exception as exc:
        print(f"ERROR: P3 GAMESTATS fetch failed: {exc}", file=sys.stderr)
        return None
    finally:
        if cable is not None:
            try:
                cable.close()
            except Exception:
                pass
        if lock_fd is not None:
            try:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
            except Exception:
                pass
            os.close(lock_fd)


def refresh_floor_games(
    *,
    serial_dev: str,
    force: bool = False,
    lock_timeout_s: float = 30.0,
) -> int:
    if not force and load_cached_payload() is not None:
        print("floor-games cache already present (use --force to refresh)")
        return 0

    text = fetch_gamestats_text(serial_dev=serial_dev, lock_timeout_s=lock_timeout_s)
    if text is None:
        if load_cached_payload() is not None:
            return 0
        path = save_cached_payload(build_payload_from_config())
        print(f"WARN: P3 unavailable — wrote config fallback → {path}", file=sys.stderr)
        return 0

    try:
        parsed = parse_gamestats_csv(text)
    except Exception as exc:
        print(f"ERROR: GAMESTATS parse failed: {exc}", file=sys.stderr)
        return 1

    payload = build_payload_from_p3(
        parsed.progressive_floor_games,
        black_gold_21_enabled=parsed.black_gold_21_enabled,
    )
    path = save_cached_payload(payload)
    games = ",".join(str(g) for g in parsed.progressive_floor_games)
    print(
        f"OK floor-games → {path} "
        f"progressive_floor_games=[{games}] black_gold_21={parsed.black_gold_21_enabled}"
    )
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Refresh EtherGold floor-games.json from P3 GAMESTATS")
    parser.add_argument(
        "--serial",
        default=os.environ.get("P3_SERIAL_DEV", DEFAULT_SERIAL),
        help="P3 USB serial device (default: /dev/ttyUSB0)",
    )
    parser.add_argument(
        "--force",
        action="store_true",
        help="Refresh even when cache file already exists",
    )
    parser.add_argument(
        "--lock-timeout",
        type=float,
        default=float(os.environ.get("P3_SERIAL_LOCK_TIMEOUT_S", "30")),
        help="Seconds to wait for P3 serial lock",
    )
    args = parser.parse_args()
    return refresh_floor_games(
        serial_dev=args.serial,
        force=args.force,
        lock_timeout_s=args.lock_timeout,
    )


if __name__ == "__main__":
    raise SystemExit(main())
