#!/usr/bin/env python3
"""Read MACHINE_ROLE / BANK_NUMBER / TERM_NUMBER from DyzineFrame config.ini.

EtherGold must not open /dev/ttyUSB0 for GAMESTATS — that port belongs to DyzineFrame.
Identity is latched once per process so a mid-session T340 reconfig cannot flap hub/spoke.
"""

from __future__ import annotations

import configparser
import os
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any


DEFAULT_DYZINEFRAME_CONFIG = "/home/topbox/DyzineFrame/config.ini"
VALID_ROLES = frozenset({"M", "S"})

_lock = threading.Lock()
_latched: "CabinetIdentity | None" = None


@dataclass(frozen=True)
class CabinetIdentity:
    machine_role: str  # "M" | "S" | ""
    bank_number: int
    term_number: int
    config_path: str
    source: str  # "config.ini" | "env" | "empty"

    @property
    def is_master(self) -> bool:
        return self.machine_role == "M"

    @property
    def is_slave(self) -> bool:
        return self.machine_role == "S"

    def suggested_edge_topology(self) -> str:
        """Derive EDGE_TOPOLOGY hint from MACHINE_ROLE (hub|spoke|"")."""
        if self.is_master:
            return "hub"
        if self.is_slave:
            return "spoke"
        return ""


def resolve_dyzineframe_config_path(explicit: str | None = None) -> str:
    if explicit:
        return explicit
    env = os.environ.get("DYZINEFRAME_CONFIG", "").strip()
    if env:
        return env
    return DEFAULT_DYZINEFRAME_CONFIG


def _normalize_role(raw: str | None) -> str:
    role = (raw or "").strip().upper()
    return role if role in VALID_ROLES else ""


def read_cabinet_identity(
    config_path: str | None = None,
    *,
    allow_env_fallback: bool = True,
) -> CabinetIdentity:
    """Read-only load from DyzineFrame config.ini (no GAMESTATS / no serial)."""
    path = resolve_dyzineframe_config_path(config_path)
    role = ""
    bank = 0
    term = 0
    source = "empty"

    cfg_path = Path(path)
    if cfg_path.is_file():
        parser = configparser.ConfigParser()
        try:
            parser.read(cfg_path, encoding="utf-8")
        except (OSError, configparser.Error):
            parser = configparser.ConfigParser()
        if parser.has_section("TOPBOX"):
            if parser.has_option("TOPBOX", "MACHINE_ROLE"):
                role = _normalize_role(parser.get("TOPBOX", "MACHINE_ROLE"))
            if parser.has_option("TOPBOX", "BANK_NUMBER"):
                try:
                    bank = int(parser.get("TOPBOX", "BANK_NUMBER"))
                except ValueError:
                    bank = 0
            if parser.has_option("TOPBOX", "TERM_NUMBER"):
                try:
                    term = int(parser.get("TOPBOX", "TERM_NUMBER"))
                except ValueError:
                    term = 0
            source = "config.ini"

    if allow_env_fallback and not role:
        env_role = _normalize_role(os.environ.get("MACHINE_ROLE"))
        if env_role:
            role = env_role
            source = "env"
        env_bank = os.environ.get("BANK_NUMBER", "").strip()
        if env_bank.isdigit():
            bank = int(env_bank)
            source = "env" if source == "empty" else source
        env_term = os.environ.get("TERM_NUMBER", "").strip()
        if env_term.isdigit():
            term = int(env_term)
            source = "env" if source == "empty" else source

    return CabinetIdentity(
        machine_role=role,
        bank_number=max(0, bank),
        term_number=max(0, term),
        config_path=path,
        source=source,
    )


def latch_cabinet_identity(
    config_path: str | None = None,
    *,
    force: bool = False,
) -> CabinetIdentity:
    """Latch identity for the process lifetime.

    Subsequent calls return the same object unless force=True (operator confirmation
    after an intentional hub/spoke flip — never call force from automatic paths).
    """
    global _latched
    with _lock:
        if _latched is not None and not force:
            return _latched
        _latched = read_cabinet_identity(config_path)
        return _latched


def clear_identity_latch_for_tests() -> None:
    global _latched
    with _lock:
        _latched = None


def identity_as_session_fields(ident: CabinetIdentity) -> dict[str, Any]:
    return {
        "cabinet_role": ident.machine_role or None,
        "cabinet_bank": ident.bank_number,
        "cabinet_term": ident.term_number,
        "cabinet_identity_source": ident.source,
        "edge_topology_suggested": ident.suggested_edge_topology() or None,
    }


if __name__ == "__main__":
    ident = read_cabinet_identity()
    print(
        f"role={ident.machine_role or '-'} bank={ident.bank_number} "
        f"term={ident.term_number} source={ident.source} path={ident.config_path}"
    )
