"""Central configuration for bullpen.

Resolution order per key (first non-empty wins):
  1. environment variable  BULLPEN_<KEY>
  2. a config file: $BULLPEN_CONF, else /etc/bullpen/bullpen.conf,
     else ~/.config/bullpen/config  (shell-style KEY=value / export KEY=value)
  3. built-in default — set to the origin fleet's value, so a host with no
     config file and no env override behaves exactly as before.

Deploy alongside bullpen_worker.py (e.g. /usr/local/lib/bullpen). Import as:
    import bullpen_config as cfg
    cfg.ROOM_HOST, cfg.PROXY, cfg.STATE_DIR, ...
"""
import os

_CONFIG_PATHS = [
    os.environ.get("BULLPEN_CONF", ""),
    "/etc/bullpen/bullpen.conf",
    os.path.expanduser("~/.config/bullpen/config"),
]


def _load_file():
    for path in _CONFIG_PATHS:
        if not path:
            continue
        try:
            f = open(path)
        except OSError:
            continue
        vals = {}
        with f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                if line.startswith("export "):
                    line = line[7:]
                if "=" not in line:
                    continue
                key, val = line.split("=", 1)
                vals[key.strip()] = val.strip().strip('"').strip("'")
        return vals
    return {}


_FILE = _load_file()


def get(key, default):
    """Resolve one BULLPEN_* key: env, then config file, then default."""
    v = os.environ.get(key)
    if v:
        return v
    v = _FILE.get(key)
    if v:
        return v
    return default


# --- roles ------------------------------------------------------------------
ROOM_HOST   = get("BULLPEN_ROOM_HOST",  "hertz")       # runs the lmcp room
GRIND_HOST  = get("BULLPEN_GRIND_HOST", "boltzmann")   # repos + pytest live here
COORD_HOST  = get("BULLPEN_COORD_HOST", "noether")     # lurkers/doctor/grinder-serve run here

# --- llm gateway ------------------------------------------------------------
PROXY       = get("BULLPEN_PROXY",    "http://hossenfelder.fritz.box:8082/v1/chat/completions")
MODEL       = get("BULLPEN_MODEL",    "nvidia/nemotron-3-ultra-550b-a55b")   # default worker model
OC_MODEL    = get("BULLPEN_OC_MODEL", "hossenfelder-proxy/nvidia/nemotron-3-ultra-550b-a55b")  # opencode provider/model
OC_URL      = get("BULLPEN_OC_URL",   "http://orca.fritz.box:4096")  # opencode server (oc-rpc, lurkers)
# Der eine Gateway-Provider, der als KATALOG gilt. Andere Provider zeigen zwar auf
# denselben Endpunkt, pinnen aber je ein Modell (siehe OC_MODEL) - das sind Aliase,
# keine Kataloge. Fuellte man sie mit derselben Liste, zeigte die Auswahl drei
# identische Kopien.
CATALOG_PROVIDER = get("BULLPEN_CATALOG_PROVIDER", "hossenfelder-proxy")

# --- paths ------------------------------------------------------------------
STATE_DIR   = get("BULLPEN_STATE_DIR",   "/var/lib/bullpen")
SECRET_FILE = get("BULLPEN_SECRET_FILE", "/etc/bullpen/post-secret")
ROOM_LOG    = get("BULLPEN_ROOM_LOG",    os.path.join(STATE_DIR, "room.jsonl"))
ART_DIR     = os.path.join(STATE_DIR, "artifacts")
HANDS_DIR   = os.path.join(STATE_DIR, "hands")
CODER_DIR   = os.path.join(STATE_DIR, "coder")

# --- shared memory (mneme) ---------------------------------------------------------
MNEME_URL        = get("BULLPEN_MNEME_URL",        "https://mneme.fritz.box")
MNEME_TOKEN_FILE = get("BULLPEN_MNEME_TOKEN_FILE", "/etc/bullpen/mneme-token")

# --- operator ---------------------------------------------------------------
USER        = get("BULLPEN_USER", "markus")            # the human's room nick

# Units the operator has deliberately switched OFF. bullpen-up's whole job is to enable
# and start its roster, so a hand-disabled unit was silently re-armed on the next run —
# an inhibit that expires the moment someone brings the herd up is not an inhibit.
# One file per unit, named exactly like the unit; its CONTENT is the reason. A reason is
# mandatory by construction: an empty inhibit is archaeology six weeks later.
# Both locations are read, so the same mechanism works in either install layout.
INHIBIT_DIRS = tuple(p for p in (
    get("BULLPEN_INHIBIT_DIR", ""),
    "/etc/bullpen/inhibit",
    os.path.expanduser("~/.config/bullpen/inhibit"),
) if p)


def inhibit_reason(unit):
    """Why `unit` is switched off, or None if it is not. Read at call time, never cached:
    an operator lifting an inhibit must not need to restart anything."""
    for d in INHIBIT_DIRS:
        try:
            with open(os.path.join(d, unit)) as f:
                text = " ".join(f.read().split())
        except OSError:
            continue
        return text or "(no reason recorded)"
    return None
# The room NICK that automation posts as. NOT a hostname — it only happens to equal
# COORD_HOST on the origin fleet, and conflating the two makes a scheduled run post as
# an unknown, unprivileged nick on any other fleet, where the room rejects it silently
# (see the R4 note in bin/bullpen-selfimprove). Must be in TRUST/PRIVILEGED below.
POST_NICK   = get("BULLPEN_POST_NICK", "noether")

# --- agents -----------------------------------------------------------------
# `deus` gehoert seit 2026-08-08 dazu. Es fehlte, und das war kein Sicherheitsgewinn,
# sondern ein Widerspruch: @deus ist der Eingriff von AUSSEN, wenn sich die Gesellschaft
# verklemmt - ein Weckrecht ist seine ganze Aufgabe. Ohne den Eintrag nahm @foreman
# seine Nachricht entgegen und verwarf sie: "refusing ask from untrusted nick 'deus'".
# Der Raum funktionierte dabei tadellos, was den Fehler drei Stunden unsichtbar macht.
# Privileged/allowed nick roster: who may WAKE an agent + post privileged (untrusted
# room posters must NOT). One source of truth; a lurker can still narrow it per-instance
# via ~/<nick>_lurker/.trust.
TRUST          = set(get("BULLPEN_TRUST", "markus noether deus pica pipi orca herder reviewer dispatcher testdesigner py godev jsdev coder callboy librarian researcher foreman triage skeptic architect").split())
# The claude binary lurkers exec (PATH-resolved by default).
CLAUDE_BIN     = get("BULLPEN_CLAUDE_BIN",    "claude")
# @callboy's local floor model id.
CALLBOY_MODEL  = get("BULLPEN_CALLBOY_MODEL", "qwen3.6-coding")
# Nicks that DRIVE a pipeline rather than answering one request. An orchestrator dispatches,
# ends its turn, and must be woken by the worker's REPLY to dispatch the next stage — with the
# default reply-deaf wake set that is an immediate deadlock, not a slow one. It opts in via
# ~/<nick>_lurker/.wakereplies (docs/ORCHESTRATION.md §2, "any orchestrator MUST wake on
# replies"). That invariant was written down after it cost a stalled campaign on 2026-07-24 and
# then enforced by nothing at all; bullpen-doctor checks it against this list.
ORCHESTRATORS  = tuple(get("BULLPEN_ORCHESTRATORS", "foreman").split())
# Machines this fleet owns. Used ONLY as routing vocabulary — "what's installed on <box>"
# should reach the infra agent. Not addresses (those are the role hosts above): on another
# fleet these words mean nothing, which is why the list is configurable.
# LITERAL names, not families: the regex this replaced had `pve\d`/`dcw\d`, so a future
# pve5 matched for free and now would not. That is the price of making it configurable —
# adding a machine means adding a word here (or via BULLPEN_FLEET_HOSTS).
FLEET_HOSTS    = tuple(get("BULLPEN_FLEET_HOSTS",
                           "higgs ampere ohm meitner data hertz boltzmann kepler noether tesla nc "
                           "orca pve1 pve2 pve3 pve4 dcw2 dcw3").split())
