#!/usr/bin/env python3
"""Generate and validate the repository's maintained-script inventory."""

from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
INVENTORY = ROOT / "release" / "scripts.json"
DOC_INDEX = ROOT / "docs" / "contributor_guide" / "script-inventory.mdx"
SEARCH_ROOTS = (ROOT / "scripts", ROOT / "graph" / "tests" / "heavy", ROOT / "sandbox")
PUBLIC = {
    "scripts/quickstart.sh",
    "sandbox/start_playground.sh",
    "sandbox/run_benchmarks.sh",
    "sandbox/cleanup.sh",
}
MAINTAINER = {
    "graph/tests/heavy/run_release_gate.sh",
    "graph/tests/heavy/run_pg_matrix.sh",
    "graph/tests/heavy/run_pg_matrix_docker.sh",
    "scripts/check_docs_drift.sh",
    "scripts/check_secrets.sh",
    "scripts/validate_release.py",
}
HOST_MUTATING = {
    "graph/tests/heavy/open_type_package_smoke.sh",
    "graph/tests/heavy/open_type_query_latency.sh",
    "graph/tests/heavy/run_open_type_query_criterion.sh",
    "graph/tests/heavy/run_open_type_query_resource_matrix.sh",
    "graph/tests/heavy/run_open_type_query_resources_docker.sh",
    "graph/tests/heavy/open_type_query_resources.sh",
    "graph/tests/heavy/rls_large_table_baseline.sh",
    "graph/tests/heavy/phase3_update_smoke.sh",
    "graph/tests/heavy/v1_1_update_artifact_rollback.sh",
    "graph/tests/heavy/v1_2_update_artifact_rollback.sh",
}
LINUX_ONLY = {
    "graph/tests/heavy/open_type_query_resources.sh",
    "graph/tests/heavy/run_open_type_query_resource_matrix.sh",
}
TOOL_OVERRIDES = {
    "graph/tests/heavy/open_type_package_smoke.sh": [
        "bash",
        "psql",
        "createdb",
        "dropdb",
    ],
    "graph/tests/heavy/open_type_query_resources.sh": [
        "bash",
        "cargo-pgrx",
        "psql",
        "createdb",
        "dropdb",
    ],
    "graph/tests/heavy/open_type_query_latency.sh": [
        "bash",
        "cargo-pgrx",
        "pgbench",
        "psql",
        "createdb",
        "dropdb",
        "python3",
    ],
    "graph/tests/heavy/run_open_type_query_criterion.sh": [
        "bash",
        "cargo",
        "python3",
    ],
    "graph/tests/heavy/run_open_type_query_resource_matrix.sh": [
        "bash",
        "cargo-pgrx",
        "psql",
        "createdb",
        "dropdb",
    ],
    "graph/tests/heavy/run_open_type_query_resources_docker.sh": ["bash", "docker"],
    "graph/tests/heavy/rls_large_table_baseline.sh": [
        "bash",
        "cargo-pgrx",
        "pg_config",
        "psql",
        "createdb",
        "dropdb",
        "git",
    ],
    "graph/tests/heavy/phase3_update_smoke.sh": ["bash", "cargo-pgrx", "psql", "createdb", "dropdb"],
    "graph/tests/heavy/v1_1_update_artifact_rollback.sh": [
        "bash",
        "cargo-pgrx",
        "psql",
        "pg_dump",
        "pg_restore",
        "createdb",
        "dropdb",
        "dropuser",
    ],
    "graph/tests/heavy/v1_2_update_artifact_rollback.sh": [
        "bash",
        "cargo-pgrx",
        "psql",
        "pg_dump",
        "pg_restore",
        "createdb",
        "dropdb",
        "dropuser",
    ],
}


def maintained_scripts() -> list[Path]:
    tracked = subprocess.run(
        ["git", "ls-files", "--cached", "--others", "--exclude-standard"],
        cwd=ROOT,
        capture_output=True,
        text=True,
        check=True,
    ).stdout.splitlines()
    prefixes = tuple(f"{root.relative_to(ROOT).as_posix()}/" for root in SEARCH_ROOTS)
    return sorted(
        ROOT / relative
        for relative in tracked
        if relative.startswith(prefixes)
        and Path(relative).suffix in {".sh", ".py"}
        and "__pycache__" not in Path(relative).parts
    )


def entry(path: Path) -> dict[str, object]:
    relative = path.relative_to(ROOT).as_posix()
    audience = "public/stable" if relative in PUBLIC else "maintainer/stable" if relative in MAINTAINER else "internal"
    heavy = relative.startswith("graph/tests/heavy/")
    destructive = relative in HOST_MUTATING or any(
        word in path.name for word in ("clean", "cleanup", "crash", "install", "upgrade")
    )
    return {
        "path": relative,
        "owner": "pgGraph maintainers",
        "audience": audience,
        "tier": "rc" if heavy or relative in MAINTAINER else "pr",
        "required_tools": TOOL_OVERRIDES.get(
            relative, ["bash"] if path.suffix == ".sh" else ["python3"]
        ),
        "inputs": ["documented CLI arguments", "documented environment variables"],
        "outputs": ["stdout/stderr", "exit status"],
        "destructive": destructive,
        "timeout_seconds": 3600 if heavy else 600,
        "platforms": (
            ["Linux", "Docker"]
            if relative in LINUX_ONLY
            else ["Linux", "macOS"]
            if not heavy
            else ["Linux", "macOS", "Docker"]
        ),
        "ci_release_usage": "release gate" if heavy or relative in MAINTAINER else "static inventory gate",
        "decision": "keep",
    }


def generated_inventory() -> dict[str, object]:
    return {
        "schema_version": 1,
        "contract": {
            "public_compatibility": "Public/stable commands, defaults, environment variables, and exit meanings remain compatible throughout 1.x.",
            "exit_codes": {"0": "success", "1": "runtime or gate failure", "2": "invalid command line or unsafe target"},
            "decisions": {
                "keep": "Retain the entry point.",
                "merge": "Move implementation behind a compatibility wrapper.",
                "replace": "Retain a deprecation wrapper for the 1.x window.",
                "delete": "Only unreferenced internal scripts may be removed after inventory review.",
            },
        },
        "reviewed_overlaps": [
            {
                "paths": ["scripts/quickstart.sh", "sandbox/start_playground.sh"],
                "decision": "merge implementation, keep wrappers",
                "reason": "quickstart is the stable dispatcher; start_playground remains the stable direct playground command",
            },
            {
                "paths": ["graph/tests/heavy/run_pg_matrix.sh", "graph/tests/heavy/run_pg_matrix_docker.sh"],
                "decision": "keep platform adapters",
                "reason": "local pgrx and isolated Docker matrices exercise distinct supported operator environments",
            },
            {
                "paths": ["graph/tests/heavy/*.sh"],
                "decision": "keep versioned workflow fixtures",
                "reason": "database setup differs by failure boundary; shared naming, polling, prerequisite, timeout, logging, and evidence behavior belongs in scripts/lib/pggraph-common.sh and the tier runner",
            },
        ],
        "scripts": [entry(path) for path in maintained_scripts()],
    }


def generated_doc(inventory: dict[str, object]) -> str:
    lines = [
        "---",
        'title: "Script inventory"',
        'description: "Generated ownership and stability index for pgGraph scripts."',
        "---",
        "",
        "{/* Generated by scripts/check_script_inventory.py; do not edit by hand. */}",
        "",
        "This index lists every maintained shell and Python entry point. Public/stable",
        "commands retain their documented arguments, environment variables, defaults, and",
        "exit meanings throughout the pgGraph 1.x compatibility window.",
        "",
        "| Path | Audience | Tier | Destructive | Timeout |",
        "|---|---|---|---:|---:|",
    ]
    for item in inventory["scripts"]:
        lines.append(
            f"| `{item['path']}` | {item['audience']} | {item['tier']} | "
            f"{'yes' if item['destructive'] else 'no'} | {item['timeout_seconds']}s |"
        )
    lines.extend(
        [
            "",
            "The machine-readable source at `release/scripts.json` also records owners,",
            "tools, inputs, outputs, platforms, CI/release usage, and keep/merge/replace/delete",
            "decisions, including reviewed overlaps and the reason each wrapper or adapter remains.",
            "Internal fixtures may have workflow-specific interfaces; stable public",
            "and maintainer entry points use the shared validation and release-runner contracts.",
            "",
        ]
    )
    return "\n".join(lines)


def check_static(inventory: dict[str, object]) -> list[str]:
    errors: list[str] = []
    entries = inventory.get("scripts")
    if not isinstance(entries, list):
        return ["scripts must be a list"]
    paths = {item.get("path") for item in entries if isinstance(item, dict)}
    expected = {path.relative_to(ROOT).as_posix() for path in maintained_scripts()}
    if paths != expected:
        errors.append(f"inventory paths differ: missing={sorted(expected - paths)}, stale={sorted(paths - expected)}")
    required = {"path", "owner", "audience", "tier", "required_tools", "inputs", "outputs", "destructive", "timeout_seconds", "platforms", "ci_release_usage", "decision"}
    for item in entries:
        if isinstance(item, dict):
            missing = required - item.keys()
            if missing:
                errors.append(f"{item.get('path', '<unknown>')}: missing {sorted(missing)}")
    for path in maintained_scripts():
        if path.suffix == ".sh":
            proc = subprocess.run(["bash", "-n", str(path)], capture_output=True, text=True, check=False)
            if proc.returncode:
                errors.append(f"bash -n {path.relative_to(ROOT)}: {proc.stderr.strip()}")
    if shellcheck := shutil.which("shellcheck"):
        shells = [str(path) for path in maintained_scripts() if path.suffix == ".sh"]
        proc = subprocess.run([shellcheck, "--severity=error", *shells], cwd=ROOT, check=False)
        if proc.returncode:
            errors.append("ShellCheck reported an error-severity finding")
    return errors


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--write", action="store_true", help="rewrite release/scripts.json")
    args = parser.parse_args()
    generated = generated_inventory()
    if args.write:
        INVENTORY.write_text(json.dumps(generated, indent=2) + "\n", encoding="utf-8")
        DOC_INDEX.write_text(generated_doc(generated), encoding="utf-8")
    if not INVENTORY.exists():
        print(f"missing {INVENTORY.relative_to(ROOT)}; run with --write", file=sys.stderr)
        return 1
    recorded = json.loads(INVENTORY.read_text(encoding="utf-8"))
    errors = [] if recorded == generated else ["release/scripts.json is stale; run with --write"]
    if not DOC_INDEX.exists() or DOC_INDEX.read_text(encoding="utf-8") != generated_doc(recorded):
        errors.append("generated script inventory documentation is stale; run with --write")
    errors.extend(check_static(recorded))
    if errors:
        print("\n".join(errors), file=sys.stderr)
        return 1
    print(f"Script inventory passed for {len(recorded['scripts'])} maintained entry points")
    return 0


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