#!/usr/bin/env python3
"""Validate the immutable HSKB V14 evidence master and bundled production assets."""

from __future__ import annotations

import base64
import csv
import hashlib
import json
import struct
import sys
import xml.etree.ElementTree as ET
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
REFERENCE_SHA = "339d07f014168cca33346d4737a4d96815b522efd367a03d26ce5ce537ac47cc"
ASSET_HASHES = {
    "assets/reference/HSKB-original-reference-screenshot-2026-08-15.png": REFERENCE_SHA,
    "assets/logo/hskb-v14-symbol.svg": "23ceca234c0ff49b76bab0b55514d16e46ec393a9979115a3b80c5fe9a174830",
    "assets/logo/hskb-v14-symbol-red.svg": "f0bfef896a3d997c1c2c8b96dc181fd3e12687bf083d3dfc603af7b8c68afada",
    "assets/logo/hskb-v14-primary.svg": "8f9b4603ffd33953f452eed9e38ca8547bc6491b0952de7dba3efad7931c6c54",
    "assets/logo/hskb-v14-reversed.svg": "40b71e9220e12355607818f914ddd8a35257f1f8d906b1d58d31207340a84651",
    "assets/logo/hskb-v14-chinese.svg": "317a4d930707b6609c12a6ee17f9d0c9cabbaac8069550ca609a1f53c6619132",
    "assets/logo/hskb-v14-english.svg": "ff2a5d5cbb98d9c7dd2a86bf57915ef460d67e561d6bd9715fd8e2049a03ac1e",
    "assets/logo/hskb-v14-symbol.png": "4c3e7092ea12163cc4d5265c16fd369a48f39e44c947c306ce98bf638074b8d4",
    "assets/logo/hskb-v14-symbol-on-white.png": "a8d4c0a55591dd14bbb16f25cd8003c2eb19dd1c0cbd249337b213d6310b8109",
    "assets/logo/hskb-v14-symbol-on-gallery-ink.png": "d4a98546ed7c265dd03c353f6bf9aaf867a820e46fb546979a1b58d47a4643c0",
    "assets/logo/hskb-v14-apple-touch-icon.png": "963dfbbe34bbf16f38b0873ad814a4d454cc684154f6db2617043318319e60a2",
    "assets/logo/hskb-v14-icon-192.png": "10afe353887b6df593785481fb5bfff067f6d29eabe32898c7b22a9968360450",
    "assets/logo/hskb-v14-icon-512.png": "5404d2d46e70b6cbc51766a90c3fbc38d949993892fa5d2096852654c37fedb2",
    "assets/logo/hskb-v14-icon-maskable-512.png": "d4e2c9b34c9174af60e7fbedf85fd39cc5978ee2196df55644663efdb661bbf7",
    "assets/logo/hskb-v14-favicon.ico": "d62c09c79af6b68519682b8553850607cf4c56d92ae9024469de4a6c7d3345bf",
    "assets/logo/hskb-v14-social-card.png": "62c649e78f6fb257a45278c83f07e91a64ddcc383d74b0f0107242a50fc48712",
    "assets/frame-system/hskb-v14-gallery-frame.svg": "9a448b3f806076b67c001fcb0d3d89c43f3a853257faee5c1e2a9c284aedfd11",
    "assets/preview/hskb-v14-gallery-frame.png": "81daf64ddda5c324780e8327a91825dd02666d987236343ef19c46bd0541e9c4",
    "assets/spec/identity-master.json": "286a599d7e8bc3bf89bfb4e35282a8d50e88fe82c8fe6720ccea70acc1fd65b3",
    "assets/spec/geometry-lock.json": "1e9b8d21729cde70976ad8cc7404a742ef886b021ed2d7e113dd1743b613fcdb",
    "assets/tokens/hskb-brand-tokens.json": "154890f9c517fe031795d6672c5177834e575098a49b1fca1980070fb4af7beb",
    "assets/tokens/hskb-brand-tokens.css": "43616fc615e59910983133e1c415db68e196a29d7e19ad235a73395e198e170b",
}
PNG_DIMENSIONS = {
    "assets/reference/HSKB-original-reference-screenshot-2026-08-15.png": (1146, 1132),
    "assets/logo/hskb-v14-symbol.png": (1020, 1019),
    "assets/logo/hskb-v14-symbol-on-white.png": (1020, 1019),
    "assets/logo/hskb-v14-symbol-on-gallery-ink.png": (1020, 1019),
    "assets/logo/hskb-v14-apple-touch-icon.png": (180, 180),
    "assets/logo/hskb-v14-icon-192.png": (192, 192),
    "assets/logo/hskb-v14-icon-512.png": (512, 512),
    "assets/logo/hskb-v14-icon-maskable-512.png": (512, 512),
    "assets/logo/hskb-v14-social-card.png": (1200, 630),
    "assets/preview/hskb-v14-gallery-frame.png": (1200, 1600),
}
LOGO_SVGS = {
    "hskb-v14-symbol.svg",
    "hskb-v14-symbol-red.svg",
    "hskb-v14-primary.svg",
    "hskb-v14-reversed.svg",
    "hskb-v14-chinese.svg",
    "hskb-v14-english.svg",
}
APPLICATION_RELEASE = "14.1.0"
APPLICATION_CATEGORIES = {
    "stationery": 11,
    "business-cards": 3,
    "certificates": 3,
    "reports": 3,
    "campaign": 7,
    "digital": 7,
    "environment": 14,
    "credentials": 4,
    "merchandise": 4,
    "events": 2,
    "partnerships": 2,
}
APPLICATION_PRIORITIES = {"P0": 36, "P1": 24}
GUIDEBOOK_PATH = "assets/guides/HSKB_V14.1_VI_Brand_Guidebook_2026-08-28.pdf"
GUIDEBOOK_SHA = "c0c67f22c0b32d2f2ab8b66f9725f5f12dcf6c75f9aa2cb5841fc34b1f5b9a8d"


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def local_name(tag: str) -> str:
    return tag.rsplit("}", 1)[-1]


def png_size(path: Path) -> tuple[int, int]:
    data = path.read_bytes()[:24]
    if len(data) != 24 or data[:8] != b"\x89PNG\r\n\x1a\n" or data[12:16] != b"IHDR":
        raise ValueError("not a valid PNG header")
    return struct.unpack(">II", data[16:24])


def validate_svg(path: Path, canonical_png: bytes, errors: list[str]) -> None:
    try:
        root = ET.parse(path).getroot()
    except (ET.ParseError, OSError) as exc:
        errors.append(f"{path.name}: invalid SVG ({exc})")
        return
    if "viewBox" not in root.attrib:
        errors.append(f"{path.name}: missing viewBox")
    tags = {local_name(node.tag) for node in root.iter()}
    if path.name in LOGO_SVGS:
        forbidden = tags & {"text", "script", "linearGradient", "radialGradient", "filter", "mask"}
        if forbidden:
            errors.append(f"{path.name}: forbidden tags {sorted(forbidden)}")
    images = [node for node in root.iter() if local_name(node.tag) == "image"]
    if not images:
        errors.append(f"{path.name}: missing immutable embedded symbol")
    for node in images:
        href = node.attrib.get("href") or node.attrib.get("{http://www.w3.org/1999/xlink}href", "")
        prefix = "data:image/png;base64,"
        if not href.startswith(prefix):
            errors.append(f"{path.name}: image must be an embedded PNG data URI")
            continue
        try:
            embedded = base64.b64decode(href[len(prefix):], validate=True)
        except ValueError as exc:
            errors.append(f"{path.name}: invalid embedded PNG ({exc})")
            continue
        if embedded != canonical_png:
            errors.append(f"{path.name}: embedded H differs from canonical symbol PNG")
        annotation = node.attrib.get("data-reference-sha256")
        if annotation is not None and annotation != REFERENCE_SHA:
            errors.append(f"{path.name}: incorrect locked reference SHA annotation")


def validate_application_system(errors: list[str]) -> None:
    matrix_path = ROOT / "assets/spec/application-matrix-v14.1.json"
    application_manifest_path = ROOT / "assets/applications/APPLICATION_MANIFEST.csv"
    production_manifest_path = ROOT / "assets/applications/PRODUCTION_MANIFEST.csv"
    guidebook_path = ROOT / GUIDEBOOK_PATH
    if not guidebook_path.is_file():
        errors.append(f"missing {GUIDEBOOK_PATH}")
    elif sha256(guidebook_path) != GUIDEBOOK_SHA:
        errors.append(f"{GUIDEBOOK_PATH}: SHA-256 differs from the final V14.1 guidebook")
    try:
        matrix = json.loads(matrix_path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        errors.append(f"invalid V14.1 application matrix ({exc})")
        return

    if matrix.get("identityMaster") != "14.0":
        errors.append("application matrix must keep identity master 14.0")
    if matrix.get("applicationRelease") != APPLICATION_RELEASE:
        errors.append(f"application matrix release must be {APPLICATION_RELEASE}")
    if matrix.get("applicationCount") != 60:
        errors.append("application matrix must declare 60 templates")
    if matrix.get("categories") != APPLICATION_CATEGORIES:
        errors.append("application category counts differ from the V14.1 release")

    applications = matrix.get("applications")
    if not isinstance(applications, list) or len(applications) != 60:
        errors.append("application matrix must contain exactly 60 application records")
        return

    filenames: set[str] = set()
    priority_counts = {priority: 0 for priority in APPLICATION_PRIORITIES}
    matrix_hashes: dict[str, str] = {}
    for item in applications:
        if not isinstance(item, dict):
            errors.append("application matrix contains a non-object record")
            continue
        filename = item.get("filename")
        expected_hash = item.get("sha256")
        priority = item.get("priority")
        category = item.get("category")
        logo_asset = item.get("logoAsset")
        if not isinstance(filename, str):
            errors.append("application record is missing a filename")
            continue
        relative = Path(filename)
        if relative.is_absolute() or ".." in relative.parts or relative.suffix != ".svg" or relative.parts[:2] != ("assets", "applications"):
            errors.append(f"unsafe or invalid application path: {filename}")
            continue
        if filename in filenames:
            errors.append(f"duplicate application path: {filename}")
        filenames.add(filename)
        if category not in APPLICATION_CATEGORIES:
            errors.append(f"{filename}: unknown application category {category!r}")
        if priority not in APPLICATION_PRIORITIES:
            errors.append(f"{filename}: unknown priority {priority!r}")
        else:
            priority_counts[priority] += 1
        if logo_asset not in ASSET_HASHES:
            errors.append(f"{filename}: logo asset is not a locked production asset")
        path = ROOT / relative
        if not path.is_file():
            errors.append(f"missing {filename}")
        elif not isinstance(expected_hash, str) or sha256(path) != expected_hash:
            errors.append(f"{filename}: SHA-256 differs from the V14.1 matrix")
        if isinstance(expected_hash, str):
            matrix_hashes[filename] = expected_hash

    if priority_counts != APPLICATION_PRIORITIES:
        errors.append("application P0/P1 counts differ from the V14.1 release")

    bundled_svgs = {
        path.relative_to(ROOT).as_posix()
        for path in (ROOT / "assets/applications").rglob("*.svg")
    }
    if bundled_svgs != filenames:
        missing = sorted(filenames - bundled_svgs)
        extra = sorted(bundled_svgs - filenames)
        if missing:
            errors.append(f"application assets missing from bundle: {', '.join(missing)}")
        if extra:
            errors.append(f"application assets absent from matrix: {', '.join(extra)}")

    try:
        with application_manifest_path.open(newline="", encoding="utf-8") as handle:
            application_rows = list(csv.DictReader(handle))
        with production_manifest_path.open(newline="", encoding="utf-8") as handle:
            production_rows = list(csv.DictReader(handle))
    except OSError as exc:
        errors.append(f"invalid V14.1 CSV manifest ({exc})")
        return

    application_filenames = {row.get("filename", "") for row in application_rows}
    production_filenames = {row.get("filename", "") for row in production_rows}
    if len(application_rows) != 60 or application_filenames != filenames:
        errors.append("application manifest must list the same 60 templates as the matrix")
    if len(production_rows) != 60 or production_filenames != filenames:
        errors.append("production manifest must list the same 60 templates as the matrix")
    for row in application_rows:
        filename = row.get("filename", "")
        if filename in matrix_hashes and row.get("sha256") != matrix_hashes[filename]:
            errors.append(f"{filename}: application manifest SHA-256 differs from the matrix")


def main() -> int:
    errors: list[str] = []
    for relative, expected in ASSET_HASHES.items():
        path = ROOT / relative
        if not path.is_file():
            errors.append(f"missing {relative}")
        elif sha256(path) != expected:
            errors.append(f"{relative}: SHA-256 differs from locked master")

    for relative, expected in PNG_DIMENSIONS.items():
        path = ROOT / relative
        if not path.is_file():
            continue
        try:
            actual = png_size(path)
        except ValueError as exc:
            errors.append(f"{relative}: {exc}")
        else:
            if actual != expected:
                errors.append(f"{relative}: expected {expected}, got {actual}")

    canonical_path = ROOT / "assets/logo/hskb-v14-symbol.png"
    if canonical_path.is_file():
        canonical_png = canonical_path.read_bytes()
        for path in sorted((ROOT / "assets/logo").glob("*.svg")):
            validate_svg(path, canonical_png, errors)
        frame = ROOT / "assets/frame-system/hskb-v14-gallery-frame.svg"
        if frame.is_file():
            validate_svg(frame, canonical_png, errors)

    try:
        identity = json.loads((ROOT / "assets/spec/identity-master.json").read_text(encoding="utf-8"))
        geometry = json.loads((ROOT / "assets/spec/geometry-lock.json").read_text(encoding="utf-8"))
        tokens = json.loads((ROOT / "assets/tokens/hskb-brand-tokens.json").read_text(encoding="utf-8"))
        if identity["identityMaster"] != geometry["identityMaster"] or identity["identityMaster"] != tokens["version"] or identity["identityMaster"] != "14.0":
            errors.append("identity, geometry and token versions must all be 14.0")
        if identity["reference"]["fileSha256"] != REFERENCE_SHA or geometry["referenceFileSha256"] != REFERENCE_SHA:
            errors.append("machine specs do not point to the immutable evidence master")
        if identity["geometry"]["redBounds"] != {"x": 64, "y": 61, "width": 1020, "height": 1019}:
            errors.append("identity red bounds are not locked to 1020x1019 at 64,61")
        if geometry["topCut"] != [568, 61, 13, 340] or geometry["bottomCut"] != [568, 740, 13, 340] or geometry["bridge"] != [401, 339]:
            errors.append("geometry cuts or bridge differ from the evidence master")
        colors = tokens["color"]
        if colors["referenceRed"] != "#FF2F2D" or colors["galleryInk"] != "#002E39":
            errors.append("production sRGB colors differ from the V14 lock")
        if identity["rights"]["status"] != "clearance-required-before-public-trademark-use":
            errors.append("rights status must remain clearance-required")
    except (OSError, KeyError, TypeError, json.JSONDecodeError) as exc:
        errors.append(f"invalid machine-readable spec ({exc})")

    v13_assets = sorted(path.name for path in (ROOT / "assets").rglob("*v13*") if path.is_file())
    if v13_assets:
        errors.append(f"historical V13 assets found in current asset tree: {', '.join(v13_assets)}")

    validate_application_system(errors)

    if errors:
        print("HSKB V14 brand validation failed:", file=sys.stderr)
        for error in errors:
            print(f"- {error}", file=sys.stderr)
        return 1
    print(
        f"Validated HSKB V14 evidence master, {len(ASSET_HASHES) - 1} locked identity assets, "
        "and 60 V14.1 application masters."
    )
    return 0


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