#!/usr/bin/env python3
"""Generate REST universe stubs from Broadcom vSphere Automation operations index.

Source: contracts/vsphere/broadcom-9.1-operations-index.txt
  (scraped from https://developer.broadcom.com/xapis/vsphere-automation-api/latest/operation-index/)

Output: app/vsphere/rest/universe.json
"""

from __future__ import annotations

import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
INDEX = ROOT / "contracts" / "vsphere" / "broadcom-9.1-operations-index.txt"
OUT = ROOT / "app" / "vsphere" / "rest" / "universe.json"

# Title-Case service tokens that already imply a hyphenated REST segment.
_SERVICE_ALIAS: dict[tuple[str, ...], tuple[str, ...]] = {
    ("Cis", "Session"): ("session",),
    ("Content", "LocalLibrary"): ("content", "local-library"),
    ("Content", "SubscribedLibrary"): ("content", "subscribed-library"),
    ("Vcenter", "VM"): ("vcenter", "vm"),
    ("Vcenter", "ResourcePool"): ("vcenter", "resource-pool"),
    ("Vcenter", "Authorization", "Privileges"): ("vcenter", "privilege"),
    ("Appliance", "Timesync"): ("appliance", "timesync"),
}

# Segments that carry a resource id when used as a parent, or as a collection leaf.
_ID_NAMES: dict[str, str] = {
    "vm": "vm",
    "host": "host",
    "hosts": "host",
    "cluster": "cluster",
    "clusters": "cluster",
    "datacenter": "datacenter",
    "datacenters": "datacenter",
    "folder": "folder",
    "folders": "folder",
    "datastore": "datastore",
    "datastores": "datastore",
    "network": "network",
    "networks": "network",
    "resource-pool": "resource_pool",
    "library": "library_id",
    "libraries": "library_id",
    "local-library": "library_id",
    "subscribed-library": "library_id",
    "item": "item_id",
    "items": "item_id",
    "category": "category_id",
    "categories": "category_id",
    "tag": "tag_id",
    "tags": "tag_id",
    "policy": "policy",
    "policies": "policy",
    "snapshot": "snapshot",
    "snapshots": "snapshot",
    "disk": "disk",
    "disks": "disk",
    "ethernet": "nic",
    "cdrom": "cdrom",
    "cdroms": "cdrom",
    "serial": "port",
    "parallel": "port",
    "floppy": "floppy",
    "nvme": "adapter",
    "sata": "adapter",
    "scsi": "adapter",
    "provider": "provider",
    "providers": "provider",
    "task": "task",
    "tasks": "task",
    "permission": "permission_id",
    "permissions": "permission_id",
    "role": "role",
    "roles": "role",
    "zone": "zone",
    "zones": "zone",
    "project": "project",
    "projects": "project",
    "domain": "domain",
    "domains": "domain",
    "service": "service",
    "services": "service",
    "supervisor": "supervisor",
    "supervisors": "supervisor",
    "namespace": "namespace",
    "namespaces": "namespace",
    "depot": "depot",
    "depots": "depot",
    "component": "component",
    "components": "component",
    "image": "image",
    "images": "image",
    "draft": "draft",
    "drafts": "draft",
    "connection": "connection",
    "connections": "connection",
    "vpc": "vpc",
    "vpcs": "vpc",
    "subnet": "subnet",
    "subnets": "subnet",
    "download-session": "download_session_id",
    "update-session": "update_session_id",
    "subscription": "subscription_id",
    "subscriptions": "subscription_id",
    "usage": "usage_id",
    "usages": "usage_id",
    "library-items": "item_id",
    "versions": "version",
    "check-outs": "vm",
    "trusted-root-chains": "chain",
    "nodes": "node",
    "profiles": "profile",
    "interfaces": "interface",
    "cores": "core",
    "commit": "commit",
    "commits": "commit",
}
_LEAF_ID_ACTIONS = {
    "get",
    "delete",
    "update",
    "set",
    "remove",
    "forceddelete",
    "forcedDelete",
    "forceDelete",
}


def _to_kebab(token: str) -> str:
    token = token.replace("_", "-")
    s = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", token)
    s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1-\2", s)
    return s.lower()


def _parse_ops(text: str) -> list[tuple[str, tuple[str, ...], str]]:
    ops: list[tuple[str, tuple[str, ...], str]] = []
    for raw in text.splitlines():
        line = raw.strip()
        match = re.match(r"^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$", line)
        if not match:
            continue
        method, rest = match.group(1), match.group(2)
        parts = rest.split()
        if len(parts) < 2:
            continue
        action = parts[-1]
        service = tuple(parts[:-1])
        ops.append((method, service, action))
    return ops


def _segments(service: tuple[str, ...]) -> list[str]:
    if service in _SERVICE_ALIAS:
        return list(_SERVICE_ALIAS[service])
    # Prefer Vm over VM when both appear in aliases above.
    return [_to_kebab(part) for part in service]


def _action_base(action: str) -> str:
    return action.split("$", 1)[0]


def _build_path(service: tuple[str, ...], action: str, actions_for_service: set[str]) -> str:
    action_base = _action_base(action)
    segs = _segments(service)
    out: list[str] = []
    for index, seg in enumerate(segs):
        out.append(seg)
        id_name = _ID_NAMES.get(seg)
        if not id_name:
            continue
        is_last = index == len(segs) - 1
        if not is_last:
            out.append("{" + id_name + "}")
            continue
        leaf_actions = {_action_base(a).lower() for a in actions_for_service}
        collection = bool(leaf_actions & {"list", "create", "add"})
        if collection and action_base.lower() in {a.lower() for a in _LEAF_ID_ACTIONS}:
            out.append("{" + id_name + "}")
    return "/api/" + "/".join(out)


def generate() -> dict:
    text = INDEX.read_text(encoding="utf-8")
    ops = _parse_ops(text)
    by_service: dict[tuple[str, ...], set[str]] = defaultdict(set)
    for _method, service, action in ops:
        by_service[service].add(action)

    routes: dict[str, dict[str, str]] = {}
    # key: "VERB PATH"
    for method, service, action in ops:
        path = _build_path(service, action, by_service[service])
        key = f"{method} {path}"
        # Prefer keeping first-seen; all map to implemented stub.
        routes.setdefault(
            key,
            {
                "verb": method,
                "path": path,
                "service": " ".join(service),
                "sample_action": action,
                "status": "stub",
            },
        )

    methods = sorted(routes.values(), key=lambda item: (item["path"], item["verb"]))
    verb_counts = Counter(item["verb"] for item in methods)
    payload = {
        "source": "https://developer.broadcom.com/xapis/vsphere-automation-api/latest/operation-index/",
        "source_label": "vSphere Automation API 9.1 (Latest) operations index",
        "source_file": str(INDEX.relative_to(ROOT)),
        "broadcom_operations": len(ops),
        "broadcom_by_verb": dict(Counter(m for m, _s, _a in ops)),
        "unique_routes": len(methods),
        "unique_by_verb": dict(verb_counts),
        "methods": methods,
    }
    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
    return payload


def main() -> int:
    if not INDEX.is_file():
        print(f"missing index: {INDEX}", file=sys.stderr)
        return 1
    payload = generate()
    print(
        json.dumps(
            {
                "out": str(OUT.relative_to(ROOT)),
                "broadcom_operations": payload["broadcom_operations"],
                "unique_routes": payload["unique_routes"],
                "unique_by_verb": payload["unique_by_verb"],
            },
            indent=2,
        )
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
