#!/usr/bin/env python3
"""Validate the lean nAvid workflow contract without running production."""

from __future__ import annotations

import json
from pathlib import Path
import sys

import template_library


SKILL_ROOT = Path(__file__).resolve().parents[1]
REQUIRED_FILES = [
    "SKILL.md",
    "references/execution-policy.md",
    "references/run-state.md",
    "references/quality-gates.md",
    "references/routing-matrix.md",
    "references/channel-profiles.md",
    "references/presets.md",
    "references/quick-video-workflow.md",
    "agents/openai.yaml",
    "scripts/run_state.py",
    "scripts/test_run_state.py",
    "references/template-library.md",
    "references/template-creation.md",
    "references/editorial-media.md",
    "references/batch-queue.md",
    "templates/registry.json",
    "scripts/template_library.py",
    "scripts/test_template_library.py",
    "scripts/validate_pilot_output.py",
    "scripts/test_editorial_media.py",
    "scripts/batch_queue.py",
    "scripts/test_batch_queue.py",
    "templates/assets/required-assets.json",
    "templates/components/tiktok-profile-card/README.md",
    "templates/components/tiktok-profile-card/tiktok-profile-card.css",
    "templates/components/tiktok-profile-card/tiktok-profile-card.js",
]
MOJIBAKE_MARKERS = ("chu\u00c3\u00a1", "\u00c3\u201e\u00e2\u20ac\u02dc", "\u00c3\u0192")


def fail(errors: list[str], message: str) -> None:
    errors.append(message)


def read_active(path: Path, errors: list[str]) -> str:
    try:
        text = path.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError) as exc:
        fail(errors, f"{path.relative_to(SKILL_ROOT)} is not valid UTF-8: {exc}")
        return ""
    if any(marker in text for marker in MOJIBAKE_MARKERS):
        fail(errors, f"{path.relative_to(SKILL_ROOT)} contains likely mojibake")
    return text


def load_json(path: Path, errors: list[str]) -> dict:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
        fail(errors, f"{path.relative_to(SKILL_ROOT)} is not valid JSON: {exc}")
        return {}


def flatten_asset_contract(value) -> list[str]:
    if isinstance(value, str):
        return [value]
    if isinstance(value, list):
        files: list[str] = []
        for item in value:
            files.extend(flatten_asset_contract(item))
        return files
    if isinstance(value, dict):
        files: list[str] = []
        for item in value.values():
            files.extend(flatten_asset_contract(item))
        return files
    return []


def main() -> int:
    errors: list[str] = []
    text: dict[str, str] = {}

    if (SKILL_ROOT / "references" / "legacy" / "2026-05-24" / "SKILL.md").exists():
        fail(errors, "legacy audit snapshots must not be discoverable as active SKILL.md files")

    for relative in REQUIRED_FILES:
        path = SKILL_ROOT / relative
        if not path.exists():
            fail(errors, f"missing required runtime file: {relative}")
            continue
        text[relative] = read_active(path, errors)

    core = text.get("SKILL.md", "")
    if "name: navid" not in core:
        fail(errors, "SKILL.md does not expose technical identity navid")
    if len(core.splitlines()) > 260:
        fail(errors, "SKILL.md exceeds the approximate 260 line core target")
    for marker in (
        "Mandatory Intake Gate",
        "HyperFrames is mandatory",
        "Common Template Asset Contract",
        "templates/assets/required-assets.json",
        "templates/components/tiktok-profile-card/",
        "templates/assets/sfx/follow-click.wav",
        "templates/assets/default-media/",
        "K\u00eanh / profile",
        "Ng\u00f4n ng\u1eef \u0111\u1ea7u ra",
        "Chi\u1ebfn l\u01b0\u1ee3c template",
        "Template set",
        "review-first",
        "quiet",
        "verbose",
        "Stop And Fallback Boundary",
        "references/run-state.md",
        "run-state.json",
        "repo-evidence-digest",
        "API-minimized repo facts",
        "Crawl4AI web digests",
        "references/template-library.md",
        "references/template-creation.md",
        "references/editorial-media.md",
        "references/batch-queue.md",
        "source intake, not publishing metadata",
        "Attempt Crawl4AI",
        "PUBLISH.md",
        "Hashtags",
        "exactly 5 hashtags",
    ):
        if marker not in core:
            fail(errors, f"SKILL.md missing contract marker: {marker}")

    routing = text.get("references/routing-matrix.md", "")
    for flow in (
        "AI News / GitHub repo / tech explainer",
        "AI News with `review-first`",
        "Storytelling / personal content",
        "Reference video / remake",
        "Batch request",
        "Template creation / `/navid-template`",
    ):
        if flow not in routing:
            fail(errors, f"routing matrix missing dry-run flow: {flow}")
    for marker in ("Dedicated Template Creation Route", "/navid-template", "template-creation.md"):
        if marker not in routing:
            fail(errors, f"routing matrix missing template-creation marker: {marker}")
    if "HyperFrames as the primary composition/runtime path" not in routing:
        fail(errors, "routing matrix missing universal HyperFrames runtime requirement")

    metadata = text.get("agents/openai.yaml", "")
    if 'display_name: "nAvid"' not in metadata or "$navid " not in metadata:
        fail(errors, "agents/openai.yaml does not expose nAvid / $navid metadata")

    state_contract = text.get("references/run-state.md", "")
    for marker in (
        "videos/<project-name>/run-state.json",
        "`source`, `script`, `voice`, `composition`",
        "`pending`, `in_progress`, `complete`, `invalid`, `blocked`, `failed`",
        "resume-check",
        "approve-fallback",
        "select-template",
    ):
        if marker not in state_contract:
            fail(errors, f"run-state contract missing marker: {marker}")

    template_contract = text.get("references/template-library.md", "")
    for marker in ("candidate/", "approved/", "explicit user approval", "run-state.json", "template-creation.md"):
        if marker not in template_contract:
            fail(errors, f"template library contract missing marker: {marker}")

    template_creation = text.get("references/template-creation.md", "")
    for marker in (
        "Template Creation Workflow",
        "/navid-template",
        "Template Intake",
        "Runtime Boundary",
        "HyperFrames",
        "Non-Negotiable Assets",
        "templates/assets/required-assets.json",
        "templates/assets/sfx/follow-click.wav",
        "templates/portable/<set-id>/short/",
        "templates/portable/<set-id>/standard/",
        "templates/portable/<set-id>/extended/",
        "videos/template-library-preview/<set-id>/",
        "explicit user approval",
        "does not publish",
    ):
        if marker not in template_creation:
            fail(errors, f"template creation workflow missing marker: {marker}")

    execution_policy = text.get("references/execution-policy.md", "")
    for marker in (
        "HyperFrames Runtime Boundary",
        "HyperFrames is mandatory",
        "npx hyperframes preview",
        "npx hyperframes render",
    ):
        if marker not in execution_policy:
            fail(errors, f"execution policy missing HyperFrames marker: {marker}")

    quality_gates = text.get("references/quality-gates.md", "")
    for marker in (
        "HyperFrames composition",
        "HyperFrames CLI",
        "npx hyperframes render",
        "non-HyperFrames runtimes cannot satisfy this gate",
        "audience/use-case beat",
        "repo evidence digest",
        "capture/crawl4ai/",
        "avoid plain",
        "button-like bars",
        "profile cards must be horizontally compact",
        "targeted proof frame",
        "semantically tied",
        "download only the necessary selected images",
        "Every user-supplied URL",
        "social/share URLs",
        "Crawl4AI attempt/fallback",
        "supplementary_hook_only",
        "login wall",
        "QA/debug labels",
        "exactly 5 hashtags",
    ):
        if marker not in quality_gates:
            fail(errors, f"quality gates missing HyperFrames marker: {marker}")

    editorial_contract = text.get("references/editorial-media.md", "")
    for marker in ("SOURCE PROOF", "EXPLAINER", "focused crop",
                   "reference video", "QA report", "handoff",
                   "phone-like 9:16", "source-backed metric/code/release callout",
                   "audience/use-case beat", "release/changelog/history",
                   "QA/debug labels", "repo evidence digest",
                   "repository tree/contents APIs", "download only the necessary selected images",
                   "Crawl4AI", "capture/crawl4ai/", "python -m pip install -U crawl4ai",
                   "crawl4ai-setup", "dependency readiness", "Every URL supplied by the user",
                   "social/share URLs", "Crawl4AI attempt", "supplementary_hook_only",
                   "login wall"):
        if marker.lower() not in editorial_contract.lower():
            fail(errors, f"editorial media contract missing marker: {marker}")
    if "references/editorial-media.md" not in core:
        fail(errors, "SKILL.md does not route evidence-led work to editorial-media.md")
    if "editorial-media.md" not in routing:
        fail(errors, "routing matrix does not load editorial-media.md")
    for marker in ("batch-queue.md", "sequentially", "SUMMARY.md"):
        if marker not in core and marker not in routing:
            fail(errors, f"delivered batch routing missing marker: {marker}")
    if "not yet delivered" in routing or "Batch files remain deferred" in core:
        fail(errors, "staged runtime still describes delivered batch behavior as deferred")

    batch_contract = text.get("references/batch-queue.md", "")
    for marker in (
        "videos/_queues/<batch-id>/",
        "queue.json",
        "SUMMARY.md",
        "run-state.json",
        "`pending`, `running`, `complete`, `blocked`, `failed`, `skipped`",
        "sequential",
    ):
        if marker.lower() not in batch_contract.lower():
            fail(errors, f"batch queue contract missing marker: {marker}")

    required_assets_path = SKILL_ROOT / "templates" / "assets" / "required-assets.json"
    required_assets = load_json(required_assets_path, errors) if required_assets_path.exists() else {}
    if required_assets.get("scope") != "all-current-and-future-templates":
        fail(errors, "required assets contract must apply to all current and future templates")
    declared_assets = flatten_asset_contract(required_assets.get("required_common_assets", {}))
    for relative in declared_assets:
        asset_path = SKILL_ROOT / relative
        if not asset_path.exists():
            fail(errors, f"missing required common asset: {relative}")
        elif asset_path.is_file() and asset_path.stat().st_size <= 0:
            fail(errors, f"required common asset is empty: {relative}")
    for relative in (
        "templates/components/tiktok-profile-card/tiktok-profile-card.js",
        "templates/components/tiktok-profile-card/tiktok-profile-card.css",
        "templates/components/tiktok-profile-card/README.md",
        "templates/assets/sfx/follow-click.wav",
        "templates/assets/default-media/default-silence.mp3",
        "templates/assets/default-media/default-preview.mp4",
        "templates/assets/default-media/default-preview.gif",
    ):
        if relative not in declared_assets:
            fail(errors, f"required assets contract does not declare: {relative}")
    if "all-current-and-future-templates" not in text.get("templates/assets/required-assets.json", ""):
        fail(errors, "required assets contract does not state all-template scope")
    template_creation = text.get("references/template-creation.md", "")
    for marker in (
        "focused crops or source-backed callouts",
        "obvious editorial purpose",
        "non-button subtitle styling",
        "centered compact TikTok profile card",
        "audience/use-case",
    ):
        if marker.lower() not in template_creation.lower():
            fail(errors, f"template creation contract missing marker: {marker}")
    component_readme = text.get("templates/components/tiktok-profile-card/README.md", "")
    for marker in ("compact and content-fitted", "Center the card", "empty dark pill tail"):
        if marker.lower() not in component_readme.lower():
            fail(errors, f"profile-card README missing marker: {marker}")

    registry_path = SKILL_ROOT / "templates" / "registry.json"
    if registry_path.exists():
        registry = json.loads(text.get("templates/registry.json", "{}"))
        approved = [entry for entry in registry.get("sets", []) if entry.get("tier") == "approved"]
        if not approved:
            fail(errors, "template registry contains no approved set after Phase 3 promotion")
        registry_report = template_library.validate_registry(SKILL_ROOT)
        for error in registry_report["errors"]:
            fail(errors, f"template registry validation: {error}")

    all_active = "\n".join(text.values())
    if "references/legacy/" in all_active and "never" not in all_active.lower():
        fail(errors, "active runtime guidance may route to the legacy snapshot")
    retired_id = "navid" + "-video-workflow"
    retired_invocation = "$" + retired_id
    if retired_id in all_active or retired_invocation in all_active:
        fail(errors, "active runtime material still invokes the retired skill identity")

    report = {
        "skill_root": str(SKILL_ROOT),
        "core_lines": len(core.splitlines()),
        "core_bytes": len(core.encode("utf-8")),
        "checked_files": len(text),
        "runtime_bytes": sum(len(value.encode("utf-8")) for value in text.values()),
        "dry_run_flows_checked": 6,
        "state_contract_checked": bool(state_contract),
        "batch_contract_checked": bool(batch_contract),
        "identity": "navid",
        "errors": errors,
        "status": "pass" if not errors else "fail",
    }
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main())
