#!/usr/bin/env python3
"""On-demand P3 GAMESTATS grab to fill cabinet TID + Master/Slave.

GAMESTATS blocks the T340+ ~600 ms. Never grab while current-game.json shows
recent play. HTTP auto-fills Identity when TID/Mode are empty; /config can
also request a refresh (`force`) once idle.
"""
from __future__ import annotations

import os
import time
from pathlib import Path
from typing import Any, Callable, Optional

from cabinet_identity import load_cabinet_identity, normalize_tid_hex, persist_gamestats_identity
from current_game_ipc import read_current_game, resolve_current_game_path

PLAY_BUSY_AGE_S = 8.0


def _game_index(payload: dict[str, Any] | None) -> int | None:
    if not payload:
        return None
    try:
        idx = int(payload.get("game_index"))
    except (TypeError, ValueError):
        return None
    if 1 <= idx <= 16:
        return idx
    return None


def p3_in_play(
    *,
    current_game_path: str | None = None,
    now: float | None = None,
    busy_age_s: float = PLAY_BUSY_AGE_S,
) -> bool:
    """True when current-game.json shows a 1..16 index updated within busy_age_s."""
    path = current_game_path or resolve_current_game_path()
    payload = read_current_game(path)
    if _game_index(payload) is None:
        return False
    try:
        mtime = os.path.getmtime(path)
    except OSError:
        return False
    age = (now if now is not None else time.time()) - mtime
    return age < float(busy_age_s)


def _identity_filled(ident: dict[str, Any]) -> bool:
    tid = normalize_tid_hex(str(ident.get("tid_hex") or ident.get("pog_tid") or ""))
    role = (ident.get("machine_role") or "").strip()
    return bool(tid and role)


def _result(
    *,
    ok: bool,
    grabbed: bool,
    reason: str,
    ident: dict[str, Any],
) -> dict[str, Any]:
    tid = normalize_tid_hex(str(ident.get("tid_hex") or ident.get("pog_tid") or ""))
    role = (ident.get("machine_role") or "") or None
    machine_number = int(ident.get("machine_number") or ident.get("term_number") or 0) or None
    return {
        "ok": ok,
        "grabbed": grabbed,
        "reason": reason,
        "identity": {
            "tid_hex": tid or None,
            "pog_tid": tid or None,
            "machine_number": machine_number,
            "term_number": machine_number,
            "machine_role": role,
            "mode_label": ident.get("mode_label") or None,
            "bank_number": int(ident.get("bank_number") or 0) or None,
        },
    }


def grab_p3_identity(
    *,
    config_path: str | os.PathLike[str] | None = None,
    current_game_path: str | None = None,
    serial_dev: str | None = None,
    fetch_fn: Optional[Callable[..., str | None]] = None,
    parse_fn: Optional[Callable[[str], Any]] = None,
    force: bool = False,
    lock_timeout_s: float = 8.0,
    **kwargs: Any,
) -> dict[str, Any]:
    force = bool(force or kwargs.get("force"))
    ini = Path(config_path) if config_path else None
    ident = load_cabinet_identity(ini)
    if not force and _identity_filled(ident):
        return _result(ok=True, grabbed=False, reason="already", ident=ident)
    if p3_in_play(current_game_path=current_game_path):
        return _result(ok=False, grabbed=False, reason="busy", ident=ident)

    if fetch_fn is None:
        from p3_floor_games_refresh import fetch_gamestats_text
        from pogcc_com_ports import resolve_p3_port

        port = serial_dev or resolve_p3_port()
        try:
            text = fetch_gamestats_text(
                serial_dev=port,
                timeout_s=8.0,
                lock_timeout_s=lock_timeout_s,
            )
        except Exception as exc:
            ident = load_cabinet_identity(ini)
            result = _result(ok=False, grabbed=False, reason="p3_unavailable", ident=ident)
            result["error"] = str(exc)
            return result
    else:
        text = fetch_fn(serial_dev=serial_dev, lock_timeout_s=lock_timeout_s)

    if not text:
        ident = load_cabinet_identity(ini)
        return _result(ok=False, grabbed=False, reason="p3_unavailable", ident=ident)

    try:
        if parse_fn is None:
            from p3_gamestats import parse_gamestats_csv

            parsed = parse_gamestats_csv(text)
        else:
            parsed = parse_fn(text)
    except Exception:
        ident = load_cabinet_identity(ini)
        return _result(ok=False, grabbed=False, reason="parse_failed", ident=ident)

    persist_gamestats_identity(
        ini,
        machine_role=getattr(parsed, "machine_role", None),
        bank_number=getattr(parsed, "bank_number", None),
        machine_number=getattr(parsed, "machine_number", None) or getattr(parsed, "term_number", None),
        term_number=getattr(parsed, "machine_number", None) or getattr(parsed, "term_number", None),
        pog_tid=getattr(parsed, "tid_hex", None),
        tid_hex=getattr(parsed, "tid_hex", None),
    )
    ident = load_cabinet_identity(ini)
    if not _identity_filled(ident):
        return _result(ok=False, grabbed=False, reason="incomplete", ident=ident)
    return _result(ok=True, grabbed=True, reason="grabbed", ident=ident)


# Compatibility aliases.
p3_in_play = p3_in_play
grab_p3_identity = grab_p3_identity
