#!/usr/bin/env python3
"""Validate map-extension contracts without running provider jobs."""
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
ID = re.compile(r"^[a-z][a-z0-9_]*$")


def require(condition: bool, message: str, failures: list[str]) -> None:
    if not condition:
        failures.append(message)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--id", help="Require one environment ID")
    parser.add_argument("--check-dist", action="store_true", help="Also validate the built dist contract")
    args = parser.parse_args()
    failures: list[str] = []

    main_path = ROOT / "src" / "main.js"
    profiles_path = ROOT / "scene_profiles.json"
    build_path = ROOT / "scripts" / "build.mjs"
    for path in (main_path, profiles_path, build_path, ROOT / "guides" / "ADDING_MAPS.md"):
        require(path.is_file(), f"missing {path.relative_to(ROOT)}", failures)
    if failures:
        print(json.dumps({"status": "FAIL", "failures": failures}, indent=2))
        return 2

    source = main_path.read_text()
    character_marker = "const CHARACTER_PROFILES = {"
    require(character_marker in source, "src/main.js has no CHARACTER_PROFILES registry", failures)
    character_block = source.split(character_marker, 1)[1].split("\n};\nconst DEFAULT_CHARACTER_ID", 1)[0] if character_marker in source else ""
    character_ids = re.findall(r"^  ([a-z][a-z0-9_]*): \{$", character_block, re.MULTILINE)
    require(bool(character_ids), "no character profile IDs found", failures)
    require(len(character_ids) == len(set(character_ids)), "character profile IDs must be unique", failures)
    require("desert_nomad_scout" in character_ids, "default Desert Nomad character profile is missing", failures)
    character_motion_urls = re.findall(r"url:\s*['\"]\./(models/[^'\"]+\.glb)['\"]", character_block)
    require(len(character_motion_urls) == len(character_ids) * 2, "every character profile must declare Idle and Boast GLBs", failures)

    marker = "const PROFILES = {"
    require(marker in source, "src/main.js has no PROFILES registry", failures)
    profile_block = source.split(marker, 1)[1].split("\n};", 1)[0] if marker in source else ""
    runtime_ids = re.findall(r"^  ([a-z][a-z0-9_]*): \{$", profile_block, re.MULTILINE)
    builders = re.findall(r"\bbuilder:\s*([A-Za-z_$][\w$]*)", profile_block)
    runtime_character_refs = re.findall(r"\bcharacter:\s*['\"]([a-z][a-z0-9_]*)['\"]", profile_block)
    require(bool(runtime_ids), "no runtime profile IDs found", failures)
    require(len(runtime_ids) == len(set(runtime_ids)), "runtime profile IDs must be unique", failures)
    require(len(builders) == len(runtime_ids), "every runtime profile must declare one builder", failures)
    for builder in builders:
        declaration = re.search(rf"(?:async\s+)?function\s+{re.escape(builder)}\s*\(", source)
        require(bool(declaration), f"missing builder function: {builder}", failures)
    for character_id in runtime_character_refs:
        require(character_id in character_ids, f"runtime profile references unknown character: {character_id}", failures)

    contract = json.loads(profiles_path.read_text())
    contract_characters = contract.get("characters") or {}
    require(set(contract_characters) == set(character_ids), f"runtime/scene character IDs differ: runtime={character_ids}, contract={list(contract_characters)}", failures)
    environments = contract.get("environments") or []
    contract_ids = [str(row.get("id", "")) for row in environments]
    require(len(contract_ids) == len(set(contract_ids)), "scene_profiles.json IDs must be unique", failures)
    for index, row in enumerate(environments):
        env_id = str(row.get("id", ""))
        require(bool(ID.fullmatch(env_id)), f"environments[{index}].id must be snake_case", failures)
        for key in ("name", "time", "objects", "lights", "cinematic_intent"):
            require(bool(row.get(key)), f"{env_id or index}: missing {key} in scene_profiles.json", failures)
        require(str(row.get("character", "")) in character_ids, f"{env_id or index}: unknown or missing character in scene_profiles.json", failures)
    require(set(runtime_ids) == set(contract_ids), f"runtime/scene profile IDs differ: runtime={runtime_ids}, contract={contract_ids}", failures)
    if args.id:
        require(args.id in runtime_ids, f"environment is not registered: {args.id}", failures)

    build_source = build_path.read_text()
    count_match = re.search(r"environment_count:\s*(\d+)", build_source)
    require(bool(count_match), "scripts/build.mjs has no environment_count", failures)
    if count_match:
        require(int(count_match.group(1)) == len(runtime_ids), "build environment_count does not match PROFILES", failures)
    require("cpSync(join(root, 'guides')" in build_source, "build does not publish guides", failures)

    glbs = sorted((ROOT / "public" / "models").rglob("*.glb"))
    require(bool(glbs), "public/models contains no GLBs", failures)
    for path in glbs:
        require(path.stat().st_size > 1024 and path.read_bytes()[:4] == b"glTF", f"invalid public GLB: {path.relative_to(ROOT)}", failures)
    public_paths = {path.relative_to(ROOT / "public").as_posix() for path in glbs}
    for relative_path in character_motion_urls:
        require(relative_path in public_paths, f"character motion GLB is missing: public/{relative_path}", failures)

    if args.check_dist:
        manifest_path = ROOT / "dist" / "build-manifest.json"
        require(manifest_path.is_file(), "dist/build-manifest.json is missing; run npm run build", failures)
        if manifest_path.is_file():
            manifest = json.loads(manifest_path.read_text())
            require(manifest.get("status") == "complete", "dist manifest is not complete", failures)
            require(manifest.get("environment_count") == len(runtime_ids), "dist environment_count does not match runtime", failures)
            dist_paths = {row.get("path") for row in manifest.get("files", [])}
            require("guides/ADDING_MAPS.md" in dist_paths, "dist does not publish ADDING_MAPS.md", failures)
            require("guides/map_contract.template.json" in dist_paths, "dist does not publish map template", failures)
            require("guides/validate_map_extension.py" in dist_paths, "dist does not publish map validator", failures)

    result = {
        "status": "PASS" if not failures else "FAIL",
        "runtime_environments": runtime_ids,
        "contract_environments": contract_ids,
        "runtime_characters": character_ids,
        "contract_characters": list(contract_characters),
        "runtime_character_refs": runtime_character_refs,
        "character_motion_glbs": len(character_motion_urls),
        "builders": builders,
        "public_glbs": len(glbs),
        "checked_id": args.id,
        "checked_dist": args.check_dist,
        "failures": failures,
    }
    print(json.dumps(result, indent=2))
    return 0 if not failures else 2


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