#!/usr/bin/env python3
"""Validate a courseware visual manifest without third-party dependencies."""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path


LEVELS = {"hero", "support", "keep", "evidence"}
STATUSES = {"planned", "implemented", "verified", "blocked"}
MODES = {"desktop", "fullscreen", "mobile", "print"}
HEX = re.compile(r"^#[0-9A-Fa-f]{6}$")
KEBAB = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")


def validate(data: object) -> list[str]:
    errors: list[str] = []
    if not isinstance(data, dict):
        return ["Manifest root must be a JSON object"]

    for field in ("deck", "version", "theme", "delivery_modes", "pages"):
        if field not in data:
            errors.append(f"Missing root field: {field}")

    if not isinstance(data.get("deck"), str) or not data.get("deck", "").strip():
        errors.append("deck must be a non-empty string")
    if not isinstance(data.get("version"), str) or not data.get("version", "").strip():
        errors.append("version must be a non-empty string")

    theme = data.get("theme")
    if not isinstance(theme, dict):
        errors.append("theme must be an object")
    else:
        for field in ("structure", "flow", "highlight", "surface"):
            value = theme.get(field)
            if not isinstance(value, str) or not HEX.fullmatch(value):
                errors.append(f"theme.{field} must be a six-digit hex color")

    modes = data.get("delivery_modes")
    if not isinstance(modes, list) or not modes:
        errors.append("delivery_modes must be a non-empty array")
    else:
        invalid = [mode for mode in modes if mode not in MODES]
        if invalid:
            errors.append(f"delivery_modes contains unsupported values: {invalid}")
        if len(modes) != len(set(modes)):
            errors.append("delivery_modes must not contain duplicates")

    pages = data.get("pages")
    if not isinstance(pages, list) or not pages:
        errors.append("pages must be a non-empty array")
        return errors

    page_numbers: set[int] = set()
    for index, page in enumerate(pages):
        prefix = f"pages[{index}]"
        if not isinstance(page, dict):
            errors.append(f"{prefix} must be an object")
            continue
        number = page.get("page")
        if not isinstance(number, int) or isinstance(number, bool) or number < 1:
            errors.append(f"{prefix}.page must be a positive integer")
        elif number in page_numbers:
            errors.append(f"{prefix}.page duplicates page {number}")
        else:
            page_numbers.add(number)

        level = page.get("level")
        status = page.get("status")
        if level not in LEVELS:
            errors.append(f"{prefix}.level must be one of {sorted(LEVELS)}")
        if status not in STATUSES:
            errors.append(f"{prefix}.status must be one of {sorted(STATUSES)}")

        if level in {"hero", "support"}:
            for field in ("business_question", "visual_type", "caption"):
                value = page.get(field)
                if not isinstance(value, str) or len(value.strip()) < 4:
                    errors.append(f"{prefix}.{field} is required for {level} pages")
            visual_type = page.get("visual_type")
            if isinstance(visual_type, str) and not KEBAB.fullmatch(visual_type):
                errors.append(f"{prefix}.visual_type must be kebab-case")
        elif level == "keep":
            if not isinstance(page.get("reason"), str) or len(page["reason"].strip()) < 2:
                errors.append(f"{prefix}.reason is required for keep pages")
        elif level == "evidence":
            source = page.get("source")
            reason = page.get("reason")
            if not any(isinstance(value, str) and value.strip() for value in (source, reason)):
                errors.append(f"{prefix} evidence page requires source or reason")

    return errors


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate a courseware visual manifest")
    parser.add_argument("manifest", help="Path to visual-manifest.json")
    args = parser.parse_args()
    path = Path(args.manifest)
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        print(f"ERROR: manifest not found: {path}")
        return 2
    except json.JSONDecodeError as exc:
        print(f"ERROR: invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}")
        return 2

    errors = validate(data)
    if errors:
        print(f"Manifest invalid: {len(errors)} error(s)")
        for error in errors:
            print(f"- {error}")
        return 1
    print(f"Manifest valid: {len(data['pages'])} page record(s), modes={','.join(data['delivery_modes'])}")
    return 0


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

