#!/usr/bin/env python3
"""infra-net — reconcile every project's .infra.yaml against mesh-hosts.json.

Walks ~/Code for .infra.yaml manifests (convention:infra_manifest), validates each
against the convention's JSON Schema, checks that `service.host` is a known
mesh-hosts.json host, flags per-host port collisions, prints the live infra-net
table, and writes the reconciled inventory to data/infra-net.json (non-destructive
— it does NOT overwrite mesh-hosts.json's services map, so existing consumers are
untouched).

  net-tools/bin/infra-net            # print + write inventory
  net-tools/bin/infra-net --check    # validate only, non-zero exit on problems
"""
import glob
import json
import os
import sys

HOME = os.path.expanduser("~")
CODE = os.path.join(HOME, "Code")
NET_TOOLS = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
MESH = os.path.join(NET_TOOLS, "data", "mesh-hosts.json")
SCHEMA_CONV = os.path.join(CODE, "@conventions", "programming_general", "infra_manifest.yaml")
OUT = os.path.join(NET_TOOLS, "data", "infra-net.json")

# Where deployable projects live (each may carry a root .infra.yaml).
ROOTS = [
    os.path.join(CODE, "@applications", "*", ".infra*.yaml"),
    os.path.join(CODE, "@projects", "@cocottetech", "@platform", "codebase", "@features", "*", ".infra*.yaml"),
    os.path.join(CODE, "@projects", "@cocottetech", ".infra*.yaml"),
    os.path.join(CODE, "@projects", "@magic-civilization", ".infra*.yaml"),
]


def main() -> int:
    try:
        import yaml
        import jsonschema
    except ImportError as e:
        print(f"infra-net needs pyyaml + jsonschema ({e})", file=sys.stderr)
        return 2

    check_only = "--check" in sys.argv

    mesh = json.load(open(MESH))
    # Accept canonical host names AND their aliases (e.g. lime == lilith-store-backend).
    hosts = {h["name"] for h in mesh.get("hosts", [])}
    hosts |= {a for h in mesh.get("hosts", []) for a in h.get("aliases", [])}
    schema = yaml.safe_load(open(SCHEMA_CONV))["providesFile"]["schema"]

    manifests = []
    for pat in ROOTS:
        manifests.extend(sorted(glob.glob(pat)))

    problems = []
    seen_ports: dict[tuple[str, int], str] = {}
    rows = []
    for path in manifests:
        rel = path.replace(CODE + "/", "")
        try:
            m = yaml.safe_load(open(path))
            jsonschema.validate(m, schema)
        except Exception as e:  # noqa: BLE001 — surface any parse/validation error
            problems.append(f"{rel}: {getattr(e, 'message', e)}")
            continue
        svc = m.get("service", {}) or {}
        host, port = svc.get("host"), svc.get("port")
        if host and host not in hosts:
            problems.append(f"{rel}: service.host '{host}' not in mesh-hosts.json {sorted(hosts)}")
        if host and port is not None:
            key = (host, port)
            if key in seen_ports:
                problems.append(f"port collision on {host}:{port} — {m['project']} vs {seen_ports[key]}")
            else:
                seen_ports[key] = m["project"]
        db = m.get("database", {}) or {}
        rows.append({
            "project": m["project"],
            "environment": m.get("environment", "prod"),
            "provider": m.get("provider"),
            "host": host,
            "port": port,
            "db": (f"{db.get('name')}@{db.get('cluster')}" if db else None),
            "depends_on": m.get("depends_on", []),
            "source": rel,
        })

    rows.sort(key=lambda r: (r["host"] or "~", r["port"] or 0))
    w = max([len(r["project"]) for r in rows] + [7])
    print(f"\n  infra-net — {len(rows)} services across {len(hosts)} hosts\n")
    print(f"  {'PROJECT'.ljust(w)}  {'ENV'.ljust(4)} {'HOST'.ljust(8)} {'PORT'.ljust(5)} {'PROVIDER'.ljust(12)} DB / DEPS")
    for r in rows:
        deps = (" deps:" + ",".join(r["depends_on"])) if r["depends_on"] else ""
        dbp = (r["db"] or "") + deps
        print(f"  {r['project'].ljust(w)}  {str(r['environment']).ljust(4)} {str(r['host']).ljust(8)} {str(r['port'] or '').ljust(5)} {str(r['provider']).ljust(12)} {dbp}")

    if problems:
        print("\n  PROBLEMS:")
        for p in problems:
            print(f"    ✗ {p}")
    else:
        print("\n  ✓ all manifests valid; hosts known; no port collisions")

    if not check_only:
        json.dump({"_generated_by": "net-tools/bin/infra-net", "_source": "project .infra.yaml + mesh-hosts.json",
                   "services": rows, "problems": problems}, open(OUT, "w"), indent=2)
        print(f"\n  wrote {os.path.relpath(OUT, NET_TOOLS)}")

    return 1 if problems else 0


if __name__ == "__main__":
    sys.exit(main())
