#!/usr/bin/env python3
"""
Build the C# gateway process config (config/gateway-config.json) from the
Experion controller export (Controller.csv).

The crawler's ControllerProcessManager reads this file and launches one
``hc900_gateway`` process per enabled controller:
    hc900_gateway <controllerIp> <registerMapPath> <pollIntervalMs> <grpcPort> <controllerPort>
Each controller therefore gets its own gRPC port and its own register map — the
same SignalTag name may exist on several controllers (peer comms) without any
collision, because the namespaces are isolated per process.

Controller.csv has several sections (UniversalModbus, OPC UA, Modicon); we keep
only the HC900 ``UniversalModbusController`` rows and use IPAddress1 (the primary
192.168.0.x net; IPAddress2 is the redundant link).

The existing file's ``shared`` block and any per-controller overrides (grpcPort,
enabled) are preserved when --merge is given.

Usage:
    python3 build_controllers_config.py --csv docs/Controller.csv \
        -o config/gateway-config.json --merge
"""

import csv
import json
import argparse
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent

DEFAULT_SHARED = {
    "binaryPath": str(REPO / "industrial-comm/cpp/build/hc900_gateway"),
    "ldLibraryPath": "/tmp/grpc_local/usr/lib/aarch64-linux-gnu:/tmp/absl_local/usr/lib/aarch64-linux-gnu",
    "logDir": "/tmp",
}


def parse_controllers(csv_path: Path):
    """Yield (name, ip) for each HC900 UniversalModbusController in Controller.csv."""
    header = None
    with open(csv_path, encoding="utf-8-sig") as f:
        for row in csv.reader(f):
            if not row or all(not c.strip() for c in row):
                continue
            if row[0].strip() == "ItemName":           # section header
                header = {n.strip(): i for i, n in enumerate(row)}
                continue
            if header is None:
                continue
            if row[header.get("Class", 1)].strip() != "UniversalModbusController":
                continue
            name = row[header["ItemName"]].strip()
            ip = row[header["IPAddress1"]].strip() if "IPAddress1" in header else ""
            if name and ip:
                yield name, ip


def build(csv_path: Path, map_dir: str, map_prefix: str, poll_ms: int,
          base_grpc_port: int, existing: dict | None) -> dict:
    shared = (existing or {}).get("shared") or dict(DEFAULT_SHARED)
    # index existing controllers by id to preserve per-controller overrides
    prev = {c.get("id"): c for c in (existing or {}).get("controllers", [])}

    controllers = []
    for i, (name, ip) in enumerate(parse_controllers(csv_path)):
        old = prev.get(name, {})
        controllers.append({
            "id": name,
            "name": old.get("name") or f"HC900 {name} Controller",
            "controllerIp": ip,
            "controllerPort": old.get("controllerPort", 502),
            # grpcPort is assigned sequentially to guarantee uniqueness across
            # the per-controller processes (one bind per port).
            "grpcPort": base_grpc_port + i,
            "pollIntervalMs": old.get("pollIntervalMs", poll_ms),
            "registerMapPath": str(REPO / map_dir / f"{map_prefix}{name.lower()}.json"),
            "enabled": old.get("enabled", True),
        })
    return {"shared": shared, "controllers": controllers}


if __name__ == "__main__":
    p = argparse.ArgumentParser(description="Build config/gateway-config.json from Controller.csv")
    p.add_argument("--csv", default="docs/Controller.csv")
    p.add_argument("--map-dir", default="docs")
    p.add_argument("--map-prefix", default="register-map-")
    p.add_argument("--poll-ms", type=int, default=500)
    p.add_argument("--base-grpc-port", type=int, default=50051)
    p.add_argument("-o", "--output", default="config/gateway-config.json")
    p.add_argument("--merge", action="store_true",
                   help="preserve shared block and per-controller overrides from the existing file")
    args = p.parse_args()

    out = Path(args.output)
    existing = None
    if args.merge and out.exists():
        existing = json.loads(out.read_text(encoding="utf-8"))

    cfg = build(Path(args.csv), args.map_dir, args.map_prefix, args.poll_ms,
                args.base_grpc_port, existing)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
    print(f"✓ Wrote {out}  ({len(cfg['controllers'])} controllers)")
    for c in cfg["controllers"]:
        en = "" if c["enabled"] else "  (disabled)"
        print(f"    {c['id']:4s} {c['controllerIp']:16s} grpc:{c['grpcPort']} → {Path(c['registerMapPath']).name}{en}")
