"""Native vSphere API catalog for the Web UI console.

Parameter / request-body metadata comes from the official Automation OpenAPI
(``app/vsphere/rest/param_index.json``, generated by
``scripts/generate_vsphere_param_index.py``). Nested ``body_example`` values are
flattened into dotted PARAM leaves (``placement.host``, ``cpu.count``, …).
Path-parameter examples still use lab seed identifiers so Send works against
the seeded inventory.
"""

from __future__ import annotations

import json
import re
from functools import lru_cache
from pathlib import Path
from typing import Any

from app.vsphere.contracts.matrix import (
    VERSIONS,
    catalog_entries_for_major,
    is_implemented_for_major,
    load_bundle,
)
from app.vsphere.rest.param_fields import body_fields_from_example, set_by_path

_PATH_PARAM = re.compile(r"\{([^{}/]+)\}")
_PARAM_INDEX_PATH = Path(__file__).resolve().parents[1] / "rest" / "param_index.json"

_PATH_EXAMPLES: dict[str, str] = {
    "vm": "vm-101",
    "host": "host-11",
    "datastore": "datastore-31",
    "task": "task-1",
    "snapshot": "snapshot-1",
    "category_id": "urn:vmomi:InventoryServiceCategory:demo:GLOBAL",
    "tag_id": "urn:vmomi:InventoryServiceTag:demo:GLOBAL",
    "item_id": "item-demo",
    "folder": "group-v23",
    "datacenter": "datacenter-21",
    "cluster": "domain-c21",
    "resource_pool": "resgroup-22",
    "permission_id": "1",
    "policy": "policy-default",
    "library_id": "library-demo",
}


@lru_cache(maxsize=1)
def _param_index() -> dict[str, Any]:
    if not _PARAM_INDEX_PATH.is_file():
        return {"methods": {}}
    payload = json.loads(_PARAM_INDEX_PATH.read_text(encoding="utf-8"))
    methods = payload.get("methods")
    return methods if isinstance(methods, dict) else {}


def list_vsphere_majors(*, runtime_version: str | None) -> dict[str, Any]:
    return {
        "runtime_version": runtime_version or VERSIONS[9]["version"],
        "plane": "vsphere-rest",
        "majors": [
            {
                "major": major,
                "series": meta["series"],
                "latest_version": meta["version"],
                "artifact_url": f"stub://vmware/vsphere-{meta['version']}/api-contract",
                "bundled": True,
            }
            for major, meta in VERSIONS.items()
        ],
    }


def vsphere_catalog_payload(major: int) -> dict[str, Any]:
    meta = VERSIONS.get(major) or VERSIONS[9]
    bundle = load_bundle(major)
    entries = catalog_entries_for_major(major)
    grouped: dict[str, dict[str, dict[str, Any]]] = {}
    for entry in entries:
        path = entry["path"]
        parts = [p for p in path.split("/") if p]
        tag = "/".join(parts[:3]) if len(parts) >= 3 else path
        by_path = grouped.setdefault(tag, {})
        path_entry = by_path.setdefault(path, {"path": path, "methods": []})
        path_entry["methods"].append(
            {
                "verb": entry["verb"],
                "name": f"{entry['verb'].lower()}_{parts[-1] if parts else 'root'}",
                "description": f"{entry['status']} {entry['verb']} {path}",
                "protected": True,
                "implemented": entry["status"] in {"implemented", "stub"},
            }
        )
    categories = [
        {
            "tag": tag,
            "paths": sorted(by_path.values(), key=lambda item: item["path"]),
        }
        for tag, by_path in sorted(grouped.items())
    ]
    return {
        "major": major,
        "series": meta["series"],
        "source_version": meta["version"],
        "latest_version": meta["version"],
        "artifact_url": f"stub://vmware/vsphere-{meta['version']}/api-contract",
        "bundled": True,
        "path_count": sum(len(cat["paths"]) for cat in categories),
        "method_count": len(entries),
        "categories": categories,
        "plane": "vsphere-rest",
        "contract_kind": bundle.get("kind", "stub-openapi-matrix"),
    }


def _field(
    name: str,
    *,
    type_name: str = "string",
    optional: bool = False,
    example: Any = None,
    description: str | None = None,
    enum: list[str] | None = None,
) -> dict[str, Any]:
    return {
        "name": name,
        "type": type_name,
        "description": description,
        "optional": optional,
        "enum": enum or [],
        "example": example if example is not None else name,
    }


def _path_fields(path: str) -> list[dict[str, Any]]:
    fields = []
    for name in _PATH_PARAM.findall(path):
        fields.append(
            _field(
                name,
                optional=False,
                example=_PATH_EXAMPLES.get(name, name),
                description=f"Path parameter {{{name}}}",
            )
        )
    return fields


def _normalize_index_fields(raw_fields: Any) -> list[dict[str, Any]]:
    if not isinstance(raw_fields, list):
        return []
    fields: list[dict[str, Any]] = []
    for item in raw_fields:
        if not isinstance(item, dict) or not item.get("name"):
            continue
        enum = item.get("enum") if isinstance(item.get("enum"), list) else []
        fields.append(
            _field(
                str(item["name"]),
                type_name=str(item.get("type") or "string"),
                optional=bool(item.get("optional", True)),
                example=item.get("example"),
                description=item.get("description") if isinstance(item.get("description"), str) else None,
                enum=[str(value) for value in enum],
            )
        )
    return fields


def _lookup_param_entry(verb: str, path: str) -> dict[str, Any] | None:
    methods = _param_index()
    key = f"{verb.upper()} {path}"
    entry = methods.get(key)
    return entry if isinstance(entry, dict) else None


def vsphere_method_payload(
    *,
    major: int,
    path: str,
    verb: str,
    runtime_version: str | None,
) -> dict[str, Any]:
    meta = VERSIONS.get(major) or VERSIONS[9]
    upper = verb.upper()
    path_fields = _path_fields(path)
    entry = _lookup_param_entry(upper, path)

    query_fields: list[dict[str, Any]] = []
    body_fields: list[dict[str, Any]] = []
    body_example: dict[str, Any] = {}

    if entry is not None:
        # Prefer OpenAPI path examples when present, but keep lab seed IDs.
        indexed_path = _normalize_index_fields(entry.get("path_fields"))
        if indexed_path:
            by_name = {field["name"]: field for field in indexed_path}
            merged_path: list[dict[str, Any]] = []
            for field in path_fields:
                indexed = by_name.get(str(field["name"]))
                if indexed is None:
                    merged_path.append(field)
                    continue
                merged = dict(indexed)
                # Lab seed identifiers beat generic OpenAPI "example" strings.
                if field["name"] in _PATH_EXAMPLES:
                    merged["example"] = _PATH_EXAMPLES[str(field["name"])]
                merged_path.append(merged)
            path_fields = merged_path
        query_fields = _normalize_index_fields(entry.get("query_fields"))
        body_fields = _normalize_index_fields(entry.get("body_fields"))
        raw_example = entry.get("body_example")
        if isinstance(raw_example, dict):
            body_example = raw_example

    # Prefer leaf paths flattened from nested body_example (placement.host, …).
    nested_fields = body_fields_from_example(body_example)
    if nested_fields:
        body_fields = nested_fields
    elif not body_example and body_fields:
        # Build a nested example from dotted / JSON-string body fields.
        built: dict[str, Any] = {}
        for field in body_fields:
            if field.get("optional"):
                continue
            example = field.get("example")
            name = str(field["name"])
            if isinstance(example, str) and example[:1] in {"{", "["}:
                try:
                    example = json.loads(example)
                except json.JSONDecodeError:
                    pass
            if "." in name:
                set_by_path(built, name, example)
            else:
                built[name] = example
        body_example = built
        nested_fields = body_fields_from_example(body_example)
        if nested_fields:
            body_fields = nested_fields

    # Params drawer shows query + body together; keep query_fields distinct for URL build.
    params_fields = [*body_fields, *query_fields]

    resolved = path
    for field in path_fields:
        resolved = resolved.replace(f"{{{field['name']}}}", str(field["example"]))
    return {
        "major": major,
        "path": path,
        "verb": upper,
        "name": path.strip("/").replace("/", "_"),
        "description": f"{upper} {path}",
        "resolved_path": resolved,
        "path_fields": path_fields,
        "query_fields": query_fields,
        "body_fields": params_fields,
        "indexed_fields": [],
        "body_example": body_example,
        "implemented": is_implemented_for_major(upper, path, major),
        "runtime_version": runtime_version or meta["version"],
        "source_version": meta["version"],
        "param_source": "openapi" if entry is not None else "none",
    }
