#!/usr/bin/env python3
"""Validate and manage the repository service consume/provide manifest."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import sys
import tempfile
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Iterator


SCHEMA_VERSION = "1.0"
MANIFEST_PATH = Path(".prd_plugin/services.json")
KINDS = ("consumes", "provides")
NAME_RE = re.compile(r"^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$")
CONTRACT_RE = re.compile(r"^[0-9]+(?:\.[0-9]+){0,2}(?:[-+][A-Za-z0-9.-]+)?$")
VISIBILITIES = {"private", "repository", "workspace", "public"}
FALLBACKS = {"fail", "local", "skip"}
TOP_FIELDS = {"schema_version", "repository", "consumes", "provides", "metadata"}
REPOSITORY_FIELDS = {"id", "display_name", "workspace", "visibility"}
# The template placeholder. It passes name validation, so it survived in five of
# six workspace manifests until addressed routing needed a real id (REQ-127).
PLACEHOLDER_REPO_ID = "auto"
COMMON_FIELDS = {
    "id", "service", "enabled", "capabilities", "contract_versions", "visibility", "owner", "metadata"
}
KIND_FIELDS = {
    "consumes": COMMON_FIELDS | {"provider", "required", "fallback", "bounds"},
    "provides": COMMON_FIELDS | {"consumer", "bounds", "runtime"},
}
# Durable runtime bindings for provided services (REQ-122): agents launching or
# restarting a service read these instead of guessing. A public service with a
# loopback bind took live websites down; that mistake is now a named error.
RUNTIME_FIELDS = {"bind_host", "port", "public", "restart_requires_consent"}
LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1", "[::1]"}


class ManifestError(ValueError):
    """The service manifest or requested mutation is invalid."""


def _path(repo_root: str | Path) -> Path:
    return Path(repo_root).resolve() / MANIFEST_PATH


def _canonical(value: Any) -> str:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True)


def _hash(value: Any) -> str:
    return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()


def load_manifest(repo_root: str | Path = ".") -> dict[str, Any]:
    path = _path(repo_root)
    try:
        value = json.loads(path.read_text(encoding="utf-8-sig"))
    except FileNotFoundError as exc:
        raise ManifestError(f"service manifest not found: {path}") from exc
    except json.JSONDecodeError as exc:
        raise ManifestError(f"invalid service manifest JSON: {exc}") from exc
    if not isinstance(value, dict):
        raise ManifestError("service manifest must be a JSON object")
    return value


def _finding(severity: str, code: str, location: str, summary: str) -> dict[str, str]:
    return {"severity": severity, "code": code, "location": location, "summary": summary}


def _validate_name(value: Any, *, field: str, location: str, findings: list[dict[str, str]]) -> None:
    if not isinstance(value, str) or not NAME_RE.fullmatch(value):
        findings.append(_finding("high", f"invalid_{field}", location, f"{field} must be a lowercase service identifier"))


def _normalize_service(kind: str, service: dict[str, Any]) -> dict[str, Any]:
    row = dict(service)
    for field in ("capabilities", "contract_versions"):
        value = row.get(field)
        if isinstance(value, list):
            row[field] = sorted(set(value))
    return row


def audit_value(value: Any, *, installed: bool = False) -> dict[str, Any]:
    findings: list[dict[str, str]] = []
    if not isinstance(value, dict):
        findings.append(_finding("critical", "manifest_not_object", "$", "manifest must be a JSON object"))
        value = {}
    for field in sorted(set(value) - TOP_FIELDS):
        findings.append(_finding("medium", "unknown_manifest_field", f"$.{field}", "unknown top-level field"))
    if value.get("schema_version") != SCHEMA_VERSION:
        findings.append(_finding("critical", "unsupported_schema_version", "$.schema_version", f"expected {SCHEMA_VERSION}"))
    repository = value.get("repository")
    if not isinstance(repository, dict):
        findings.append(_finding("high", "missing_repository", "$.repository", "repository descriptor must be an object"))
    else:
        for field in sorted(set(repository) - REPOSITORY_FIELDS):
            findings.append(_finding("medium", "unknown_repository_field", f"$.repository.{field}", "unknown repository field"))
        _validate_name(repository.get("id"), field="repository_id", location="$.repository.id", findings=findings)
        if installed and repository.get("id") == PLACEHOLDER_REPO_ID:
            findings.append(_finding(
                "medium", "placeholder_repository_id", "$.repository.id",
                "repository.id is still the install placeholder 'auto'; resolve it to a real "
                "repo id so this repository can be addressed"))
        if repository.get("visibility") not in VISIBILITIES:
            findings.append(_finding("high", "invalid_visibility", "$.repository.visibility", "invalid repository visibility"))
    for kind in KINDS:
        rows = value.get(kind)
        if not isinstance(rows, list):
            findings.append(_finding("high", "missing_service_list", f"$.{kind}", f"{kind} must be an array"))
            continue
        seen: set[str] = set()
        for index, raw in enumerate(rows):
            location = f"$.{kind}[{index}]"
            if not isinstance(raw, dict):
                findings.append(_finding("high", "service_not_object", location, "service entry must be an object"))
                continue
            for field in sorted(set(raw) - KIND_FIELDS[kind]):
                findings.append(_finding("medium", "unknown_service_field", f"{location}.{field}", "unknown service field"))
            service_id = raw.get("id")
            _validate_name(service_id, field="service_id", location=f"{location}.id", findings=findings)
            if isinstance(service_id, str):
                if service_id in seen:
                    findings.append(_finding("high", "duplicate_service_id", f"{location}.id", f"duplicate {kind} id {service_id}"))
                seen.add(service_id)
            _validate_name(raw.get("service"), field="service", location=f"{location}.service", findings=findings)
            counterpart = "provider" if kind == "consumes" else "consumer"
            _validate_name(raw.get(counterpart), field=counterpart, location=f"{location}.{counterpart}", findings=findings)
            if not isinstance(raw.get("enabled"), bool):
                findings.append(_finding("high", "invalid_enabled", f"{location}.enabled", "enabled must be boolean"))
            if raw.get("visibility") not in VISIBILITIES:
                findings.append(_finding("high", "invalid_visibility", f"{location}.visibility", "invalid service visibility"))
            _validate_name(raw.get("owner"), field="owner", location=f"{location}.owner", findings=findings)
            capabilities = raw.get("capabilities")
            if not isinstance(capabilities, list) or not capabilities:
                findings.append(_finding("high", "invalid_capabilities", f"{location}.capabilities", "capabilities must be a non-empty array"))
            else:
                for cap in capabilities:
                    _validate_name(cap, field="capability", location=f"{location}.capabilities", findings=findings)
                if len(capabilities) != len(set(capabilities)):
                    findings.append(_finding("medium", "duplicate_capability", f"{location}.capabilities", "capabilities must be unique"))
            versions = raw.get("contract_versions")
            if not isinstance(versions, list) or not versions or any(not isinstance(v, str) or not CONTRACT_RE.fullmatch(v) for v in versions):
                findings.append(_finding("high", "invalid_contract_versions", f"{location}.contract_versions", "contract_versions must contain version strings"))
            if kind == "provides" and "runtime" in raw:
                runtime = raw.get("runtime")
                rloc = f"{location}.runtime"
                if not isinstance(runtime, dict):
                    findings.append(_finding("high", "invalid_runtime", rloc, "runtime must be an object"))
                else:
                    for field in sorted(set(runtime) - RUNTIME_FIELDS):
                        findings.append(_finding("medium", "unknown_runtime_field", f"{rloc}.{field}", "unknown runtime field"))
                    bind_host = runtime.get("bind_host")
                    if "bind_host" in runtime and (not isinstance(bind_host, str) or not bind_host.strip()):
                        findings.append(_finding("high", "invalid_bind_host", f"{rloc}.bind_host", "bind_host must be a non-empty string"))
                    port = runtime.get("port")
                    if "port" in runtime and (not isinstance(port, int) or isinstance(port, bool) or not 1 <= port <= 65535):
                        findings.append(_finding("high", "invalid_port", f"{rloc}.port", "port must be an integer in 1-65535"))
                    public = runtime.get("public")
                    if "public" in runtime and not isinstance(public, bool):
                        findings.append(_finding("high", "invalid_public", f"{rloc}.public", "public must be boolean"))
                    if public is True:
                        if not isinstance(bind_host, str) or not bind_host.strip():
                            findings.append(_finding("high", "public_service_missing_bind_host", f"{rloc}.bind_host",
                                                     "a public service must declare its bind_host (e.g. 0.0.0.0) so launches never default to loopback"))
                        elif bind_host.strip().lower() in LOOPBACK_HOSTS:
                            findings.append(_finding("high", "public_service_loopback_binding", f"{rloc}.bind_host",
                                                     "a public service must not bind loopback; declare the externally reachable host (e.g. 0.0.0.0)"))
                        if runtime.get("restart_requires_consent") is False:
                            findings.append(_finding("high", "public_service_consent_not_waivable", f"{rloc}.restart_requires_consent",
                                                     "restarting a public service always requires explicit consent; the manifest cannot waive it"))
                    consent = runtime.get("restart_requires_consent")
                    if "restart_requires_consent" in runtime and not isinstance(consent, bool):
                        findings.append(_finding("high", "invalid_restart_requires_consent", f"{rloc}.restart_requires_consent",
                                                 "restart_requires_consent must be boolean"))
            if kind == "consumes":
                if not isinstance(raw.get("required"), bool):
                    findings.append(_finding("high", "invalid_required", f"{location}.required", "required must be boolean"))
                fallback = raw.get("fallback")
                if fallback not in FALLBACKS:
                    findings.append(_finding("high", "invalid_fallback", f"{location}.fallback", "fallback must be fail, local, or skip"))
                if raw.get("required") is True and fallback != "fail":
                    findings.append(_finding("high", "required_service_fallback", f"{location}.fallback", "required services must fail closed"))
    counts = {level: sum(1 for finding in findings if finding["severity"] == level) for level in ("critical", "high", "medium", "low")}
    return {"status": "ok" if not findings else "error", "summary": counts, "findings": findings}


def workspace_peers(left: Any, right: Any) -> bool:
    """True when two repositories declare the SAME NON-EMPTY workspace.

    The boundary rule (REQ-127): workspace peers share a live coordination
    channel and should use it; everyone else uses durable PRD messaging. An
    empty/missing workspace means STANDALONE — never "unknown, assume peer" —
    so an undeclared repo can never be silently treated as an insider.
    """
    if not isinstance(left, str) or not isinstance(right, str):
        return False
    left, right = left.strip(), right.strip()
    return bool(left) and left == right


def resolve_repository_identity(repo_root: str | Path = ".") -> dict[str, Any]:
    """Replace the install placeholder with a concrete repo id.

    Transport already derives identity from the directory name (origin_repo,
    inbox scoping), so the manifest must agree or addressing and provenance
    would disagree. A repository that already names itself is never rewritten.
    """
    root = Path(repo_root).resolve()
    path = _path(root)
    if not path.is_file():
        return {"id": None, "changed": False, "reason": "no manifest"}
    value = json.loads(path.read_text(encoding="utf-8-sig"))
    repository = value.get("repository")
    if not isinstance(repository, dict):
        return {"id": None, "changed": False, "reason": "invalid repository descriptor"}
    current = repository.get("id")
    if isinstance(current, str) and current.strip() and current != PLACEHOLDER_REPO_ID:
        return {"id": current, "changed": False, "reason": "already resolved"}
    resolved = _safe_repo_name(root.name)
    if not resolved:
        return {"id": current, "changed": False, "reason": "directory name is not a valid id"}
    repository["id"] = resolved
    _write_manifest(path, value)
    return {"id": resolved, "changed": True, "reason": "resolved from directory name"}


def set_repository_identity(repo_root: str | Path = ".", *, repo_id: str | None = None,
                            workspace: str | None = None,
                            display_name: str | None = None) -> dict[str, Any]:
    """Declare this repository's identity and workspace membership.

    Workspace membership decides the messaging boundary, so it is a validated
    write rather than a hand-edit.
    """
    root = Path(repo_root).resolve()
    path = _path(root)
    if not path.is_file():
        raise ManifestError(f"no service manifest at {path}")
    value = json.loads(path.read_text(encoding="utf-8-sig"))
    repository = value.setdefault("repository", {})
    if not isinstance(repository, dict):
        raise ManifestError("repository descriptor must be an object")
    for field, incoming in (("id", repo_id), ("workspace", workspace)):
        if incoming is None:
            continue
        candidate = str(incoming).strip()
        # An empty workspace is legitimate: it declares a standalone repo.
        if candidate and not NAME_RE.fullmatch(candidate):
            raise ManifestError(f"{field} must be a lowercase identifier: {incoming!r}")
        repository[field] = candidate
    if display_name is not None:
        repository["display_name"] = str(display_name)
    _write_manifest(path, value)
    return dict(repository)


def _safe_repo_name(name: str) -> str:
    cleaned = re.sub(r"[^a-z0-9._-]+", "-", str(name).lower()).strip("-.")
    return cleaned if NAME_RE.fullmatch(cleaned or "") else ""


def _write_manifest(path: Path, value: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8", newline="\n")


def audit_manifest(repo_root: str | Path = ".") -> dict[str, Any]:
    try:
        value = load_manifest(repo_root)
    except ManifestError as exc:
        return {"status": "error", "summary": {"critical": 1, "high": 0, "medium": 0, "low": 0}, "findings": [_finding("critical", "manifest_load", str(MANIFEST_PATH), str(exc))]}
    report = audit_value(value, installed=True)
    config_path = Path(repo_root).resolve() / ".prd_plugin" / "config.json"
    structurally_sound = not any(
        finding["severity"] in ("critical", "high") for finding in report["findings"])
    if config_path.is_file() and structurally_sound:
        try:
            config = json.loads(config_path.read_text(encoding="utf-8-sig"))
            integrations = config.get("integrations", {})
            substrate = integrations.get("substrate") if isinstance(integrations, dict) else None
            if isinstance(substrate, dict):
                expected_enabled = bool(substrate.get("enabled", False)) and substrate.get("mode", "off") != "off"
                row = next((item for item in value["consumes"] if item.get("id") == "ai-collab-substrate"), None)
                if row is None:
                    report["findings"].append(_finding("high", "missing_substrate_service", "$.consumes", "unified config requires the ai-collab-substrate declaration"))
                elif bool(row.get("enabled")) != expected_enabled:
                    report["findings"].append(_finding("high", "substrate_enabled_drift", "$.consumes.ai-collab-substrate.enabled", "service declaration does not match effective integrations.substrate enablement"))
                else:
                    expected_caps = sorted(set(substrate.get("capabilities", [])))
                    if sorted(row.get("capabilities", [])) != expected_caps:
                        report["findings"].append(_finding("medium", "substrate_capability_drift", "$.consumes.ai-collab-substrate.capabilities", "service declaration capabilities do not match unified config"))
                    if row.get("contract_versions") != [str(substrate.get("contract_version", 2))]:
                        report["findings"].append(_finding("medium", "substrate_contract_drift", "$.consumes.ai-collab-substrate.contract_versions", "service declaration contract does not match unified config"))
        except (OSError, json.JSONDecodeError, AttributeError):
            pass
    report["summary"] = {level: sum(1 for finding in report["findings"] if finding["severity"] == level) for level in ("critical", "high", "medium", "low")}
    report["status"] = "ok" if not report["findings"] else "error"
    return report


def _validated(repo_root: str | Path) -> dict[str, Any]:
    value = load_manifest(repo_root)
    report = audit_value(value)
    if report["status"] != "ok":
        first = report["findings"][0]
        raise ManifestError(f"{first['code']} at {first['location']}: {first['summary']}")
    return value


def manifest_projection(repo_root: str | Path = ".") -> dict[str, Any]:
    value = _validated(repo_root)
    normalized = dict(value)
    for kind in KINDS:
        normalized[kind] = sorted((_normalize_service(kind, row) for row in value[kind]), key=lambda row: row["id"])
    return {
        "schema_version": SCHEMA_VERSION,
        "repository": normalized["repository"],
        "consumes": normalized["consumes"],
        "provides": normalized["provides"],
        "service_count": len(normalized["consumes"]) + len(normalized["provides"]),
        "manifest_hash": _hash(normalized),
    }


def list_services(repo_root: str | Path = ".", kind: str = "") -> dict[str, Any]:
    value = _validated(repo_root)
    kinds = (kind,) if kind else KINDS
    if any(item not in KINDS for item in kinds):
        raise ManifestError(f"kind must be one of: {', '.join(KINDS)}")
    rows = [{"kind": item, **_normalize_service(item, row)} for item in kinds for row in value[item]]
    return {"status": "ok", "services": sorted(rows, key=lambda row: (row["kind"], row["id"])), "manifest_hash": manifest_projection(repo_root)["manifest_hash"]}


def get_service(repo_root: str | Path, kind: str, service_id: str) -> dict[str, Any]:
    if kind not in KINDS:
        raise ManifestError(f"kind must be one of: {', '.join(KINDS)}")
    for row in _validated(repo_root)[kind]:
        if row.get("id") == service_id:
            return {"kind": kind, "service": _normalize_service(kind, row), "manifest_hash": manifest_projection(repo_root)["manifest_hash"]}
    raise ManifestError(f"service not found: {kind}/{service_id}")


@contextmanager
def _lock(repo_root: str | Path, timeout: float = 5.0) -> Iterator[None]:
    root = Path(repo_root).resolve()
    lock = root / ".prd_plugin" / "local" / "services.lock"
    lock.parent.mkdir(parents=True, exist_ok=True)
    deadline = time.monotonic() + timeout
    while True:
        try:
            fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
            os.close(fd)
            break
        except FileExistsError:
            if time.monotonic() >= deadline:
                raise ManifestError("timed out waiting for service manifest lock")
            time.sleep(0.05)
    try:
        yield
    finally:
        try:
            lock.unlink()
        except FileNotFoundError:
            pass


def _atomic_write(path: Path, value: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    handle, temporary = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", dir=path.parent)
    try:
        with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
            json.dump(value, stream, indent=2, ensure_ascii=False)
            stream.write("\n")
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(temporary, path)
    finally:
        try:
            os.unlink(temporary)
        except FileNotFoundError:
            pass


def upsert_service(repo_root: str | Path, kind: str, service: dict[str, Any]) -> dict[str, Any]:
    if kind not in KINDS:
        raise ManifestError(f"kind must be one of: {', '.join(KINDS)}")
    if not isinstance(service, dict):
        raise ManifestError("service must be an object")
    service = _normalize_service(kind, service)
    service_id = service.get("id")
    with _lock(repo_root):
        value = load_manifest(repo_root)
        rows = value.get(kind)
        if not isinstance(rows, list):
            raise ManifestError(f"{kind} must be an array")
        matches = [index for index, row in enumerate(rows) if isinstance(row, dict) and row.get("id") == service_id]
        if len(matches) > 1:
            raise ManifestError(f"duplicate service id already exists: {service_id}")
        action = "updated" if matches else "created"
        if matches:
            rows[matches[0]] = service
        else:
            rows.append(service)
        rows.sort(key=lambda row: str(row.get("id", "")))
        report = audit_value(value)
        if report["status"] != "ok":
            first = report["findings"][0]
            raise ManifestError(f"{first['code']} at {first['location']}: {first['summary']}")
        _atomic_write(_path(repo_root), value)
    projection = manifest_projection(repo_root)
    return {"status": "ok", "action": action, "kind": kind, "service": service, "manifest_hash": projection["manifest_hash"]}


def remove_service(repo_root: str | Path, kind: str, service_id: str) -> dict[str, Any]:
    if kind not in KINDS:
        raise ManifestError(f"kind must be one of: {', '.join(KINDS)}")
    with _lock(repo_root):
        value = _validated(repo_root)
        before = len(value[kind])
        value[kind] = [row for row in value[kind] if row.get("id") != service_id]
        if len(value[kind]) == before:
            raise ManifestError(f"service not found: {kind}/{service_id}")
        report = audit_value(value)
        if report["status"] != "ok":
            first = report["findings"][0]
            raise ManifestError(f"{first['code']} at {first['location']}: {first['summary']}")
        _atomic_write(_path(repo_root), value)
    return {"status": "ok", "action": "removed", "kind": kind, "id": service_id, "manifest_hash": manifest_projection(repo_root)["manifest_hash"]}


def sync_substrate_consumer(
    repo_root: str | Path,
    *,
    enabled: bool,
    capabilities: list[str],
    contract_version: int,
    fallback: str,
) -> dict[str, Any]:
    """Synchronize the well-known AI-Collab declaration from unified config."""
    manifest = _validated(repo_root)
    existing = next(
        (row for row in manifest["consumes"] if row.get("id") == "ai-collab-substrate"),
        {},
    )
    service = {
        **existing,
        "id": "ai-collab-substrate",
        "service": "ai-collab.substrate",
        "provider": "ai-collab-v3",
        "enabled": bool(enabled),
        "required": bool(existing.get("required", False)),
        "capabilities": sorted(set(capabilities)),
        "contract_versions": [str(contract_version)],
        "visibility": existing.get("visibility", "workspace"),
        "fallback": "fail" if existing.get("required") else fallback,
        "owner": existing.get("owner", "repository"),
    }
    return upsert_service(repo_root, "consumes", service)


def reconcile_services(repo_root: str | Path = ".", *, available: list[dict[str, Any]]) -> dict[str, Any]:
    manifest = _validated(repo_root)
    offered = {(str(row.get("service", "")), str(row.get("provider", ""))) for row in available if isinstance(row, dict)}
    missing_required: list[str] = []
    degraded_optional: list[str] = []
    for row in manifest["consumes"]:
        if not row.get("enabled"):
            continue
        key = (row["service"], row["provider"])
        if key in offered:
            continue
        if row["required"]:
            missing_required.append(row["id"])
        else:
            degraded_optional.append(row["id"])
    return {
        "status": "error" if missing_required else ("degraded" if degraded_optional else "ok"),
        "missing_required": sorted(missing_required),
        "degraded_optional": sorted(degraded_optional),
        "manifest_hash": manifest_projection(repo_root)["manifest_hash"],
    }


def _json_object(value: str) -> dict[str, Any]:
    try:
        parsed = json.loads(value)
    except json.JSONDecodeError as exc:
        raise argparse.ArgumentTypeError(str(exc)) from exc
    if not isinstance(parsed, dict):
        raise argparse.ArgumentTypeError("value must be a JSON object")
    return parsed


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    sub = parser.add_subparsers(dest="action", required=True)
    for name in ("audit", "list"):
        item = sub.add_parser(name)
        item.add_argument("--repo-root", default=".")
        item.add_argument("--json", action="store_true")
        if name == "list":
            item.add_argument("--kind", choices=KINDS, default="")
    get = sub.add_parser("get"); get.add_argument("--repo-root", default="."); get.add_argument("--kind", choices=KINDS, required=True); get.add_argument("--id", required=True); get.add_argument("--json", action="store_true")
    upsert = sub.add_parser("upsert"); upsert.add_argument("--repo-root", default="."); upsert.add_argument("--kind", choices=KINDS, required=True); upsert.add_argument("--service", type=_json_object, required=True); upsert.add_argument("--json", action="store_true")
    remove = sub.add_parser("remove"); remove.add_argument("--repo-root", default="."); remove.add_argument("--kind", choices=KINDS, required=True); remove.add_argument("--id", required=True); remove.add_argument("--json", action="store_true")
    identity = sub.add_parser("identity", help="Show, resolve, or declare this repository's identity.")
    identity.add_argument("--repo-root", default=".")
    identity.add_argument("--resolve", action="store_true",
                          help="Replace the install placeholder id with the repo name.")
    identity.add_argument("--repo-id", default=None)
    identity.add_argument("--workspace", default=None,
                          help="Workspace membership; empty declares a standalone repo.")
    identity.add_argument("--display-name", default=None)
    identity.add_argument("--json", action="store_true")

    args = parser.parse_args(argv)
    try:
        if args.action == "identity":
            if args.resolve:
                result = resolve_repository_identity(args.repo_root)
            elif any(v is not None for v in (args.repo_id, args.workspace, args.display_name)):
                result = set_repository_identity(
                    args.repo_root, repo_id=args.repo_id,
                    workspace=args.workspace, display_name=args.display_name)
            else:
                result = load_manifest(args.repo_root).get("repository", {})
            print(json.dumps(result, indent=2))
            return 0
        if args.action == "audit": result = audit_manifest(args.repo_root)
        elif args.action == "list": result = list_services(args.repo_root, args.kind)
        elif args.action == "get": result = get_service(args.repo_root, args.kind, args.id)
        elif args.action == "upsert": result = upsert_service(args.repo_root, args.kind, args.service)
        else: result = remove_service(args.repo_root, args.kind, args.id)
        print(json.dumps(result, indent=2))
        return 0 if result.get("status") in {"ok", "degraded"} else 2
    except ManifestError as exc:
        print(f"[PRD Plugin] service manifest error: {exc}", file=sys.stderr)
        return 2


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