#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import re
import xml.etree.ElementTree as ET
from pathlib import Path

BATCH_RECIPES = {
    1: ["Application.Window.Attach", "Application.Window.ExplicitLifetime", "Layout.Page.Basic", "Controls.BasicSet", "Button.Primary.Basic", "Button.Group.SaveCancel", "Input.Text.Basic", "Input.Search.Basic"],
    2: ["Feedback.StatusBanner", "Feedback.ValidationMessage", "Feedback.EmptyState", "Feedback.Progress", "Button.Split.Basic", "Input.ComboBox.Basic", "Input.Slider.Basic", "Surface.GroupBox.Basic", "Surface.Expander.Basic", "Surface.Accordion.Basic", "Surface.TileBox.Basic", "Navigation.Tabs.Basic", "Navigation.Breadcrumb.Basic", "Navigation.Pagination.Basic", "Navigation.CommandBar.Basic", "DataGrid.BasicRows"],
    3: ["Theme.Surface.Apply", "Theme.Surface.Options", "Localization.Rtl.AttachPage", "Localization.Rtl.AttachSection", "Output.VisualCapture", "Output.CapabilityCatalog", "Output.Manifest", "DataGrid.LargeData.Boundary"],
    4: ["Product.TreeGrid.Basic", "Product.Kanban.Basic", "Product.PivotTable.Basic", "Product.Agenda.Basic", "Product.Dashboard.Basic", "Product.MasterDetail.Basic", "Product.AppLayout.Basic", "Product.SplitView.Basic"],
    5: ["Verification.RenderSnapshot"],
}
MAS_RE = re.compile(r"\bMAS[A-Za-z0-9_]+\b")
DECL_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\s+As\s+(MAS[A-Za-z0-9_]+)\b", re.I)
PAIR_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*|MAS[A-Za-z0-9_]+)\.([A-Za-z_][A-Za-z0-9_]*)\b")


def strip_comments_and_strings(text: str) -> str:
    result = []
    for raw in text.splitlines():
        line = []
        in_string = False
        i = 0
        while i < len(raw):
            ch = raw[i]
            if ch == '"':
                if in_string and i + 1 < len(raw) and raw[i + 1] == '"':
                    line.extend("  ")
                    i += 2
                    continue
                in_string = not in_string
                line.append(" ")
                i += 1
                continue
            if ch == "'" and not in_string:
                break
            line.append(" " if in_string else ch)
            i += 1
        result.append("".join(line))
    return "\n".join(result)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", default=str(Path(__file__).resolve().parents[2]))
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()
    root = Path(args.root).resolve()

    api = json.loads((root / "docs/_inventory/assembly-api/public-api-assembly.json").read_text(encoding="utf-8"))
    types = {item["name"]: item for item in api["types"]}
    full_to_simple = {item["fullName"].replace("+", "."): item["name"] for item in api["types"]}
    declared_members = {name: {m["name"] for m in item["members"] if m.get("access") in {"Public", "Protected", "Protected Friend"}} for name, item in types.items()}

    def all_members(type_name: str, seen: set[str] | None = None) -> set[str]:
        seen = set() if seen is None else seen
        if type_name in seen or type_name not in types:
            return set()
        seen.add(type_name)
        result = set(declared_members[type_name])
        base = types[type_name].get("baseType")
        base_simple = full_to_simple.get(base or "")
        if base_simple:
            result.update(all_members(base_simple, seen))
        return result

    members = {name: all_members(name) for name in types}

    failures: list[str] = []
    results = []
    total_pairs = 0

    for batch, recipes in BATCH_RECIPES.items():
        folder = root / f"samples/RecipeVerification.Batch{batch}"
        source = folder / "MainForm.vb"
        project = folder / f"Nexamas.UI.RecipeVerification.Batch{batch}.vbproj"
        batch_failures: list[str] = []

        if not source.is_file() or not project.is_file():
            batch_failures.append("source or project missing")
        else:
            raw = source.read_text(encoding="utf-8")
            clean = strip_comments_and_strings(raw)
            unknown = sorted(set(MAS_RE.findall(clean)) - set(types))
            if unknown:
                batch_failures.append("unknown/internal MAS types: " + ", ".join(unknown))

            for recipe in recipes:
                if recipe not in raw:
                    batch_failures.append(f"recipe ID not bound in source: {recipe}")

            variables: dict[str, tuple[str, bool]] = {}
            member_checks = 0
            for line in clean.splitlines():
                for name, type_name in DECL_RE.findall(line):
                    array_decl = bool(re.search(rf"\b{re.escape(name)}\s+As\s+{re.escape(type_name)}\s*\(\)", line, re.I))
                    variables[name.lower()] = (type_name, array_decl)
                for receiver, member in PAIR_RE.findall(line):
                    if "Global.System.Windows.Forms.Application." in line and receiver == "Application":
                        continue
                    if receiver in types:
                        type_name, is_array = receiver, False
                    else:
                        variable = variables.get(receiver.lower())
                        if not variable:
                            continue
                        type_name, is_array = variable
                    if is_array and member in {"Length", "LongLength", "Rank", "GetLength"}:
                        continue
                    if type_name not in members:
                        continue
                    member_checks += 1
                    total_pairs += 1
                    if member not in members[type_name]:
                        batch_failures.append(f"{receiver}.{member} is not public on {type_name}")

            try:
                tree = ET.parse(project)
                ns = {"m": "http://schemas.microsoft.com/developer/msbuild/2003"}
                tf = tree.find(".//m:TargetFrameworkVersion", ns)
                if tf is None or (tf.text or "").strip() != "v4.8":
                    batch_failures.append("project does not target v4.8")
                platform_targets = {(node.text or "").strip() for node in tree.findall(".//m:PlatformTarget", ns)}
                if "x64" not in platform_targets:
                    batch_failures.append("project does not declare x64 PlatformTarget")
                refs = tree.findall(".//m:ProjectReference", ns)
                if len(refs) != 1 or "Nexamas.UI.vbproj" not in (refs[0].attrib.get("Include") or ""):
                    batch_failures.append("project must contain exactly one Nexamas.UI project reference")
            except Exception as exc:
                batch_failures.append(f"project XML error: {exc}")

            if "--smoke" not in raw:
                batch_failures.append("runtime --smoke mode missing")
            if batch == 5 and "--snapshot-smoke" not in raw:
                batch_failures.append("Batch 5 --snapshot-smoke mode missing")

            results.append({
                "batch": batch,
                "recipes": len(recipes),
                "publicMemberPairsChecked": member_checks,
                "status": "PASS" if not batch_failures else "FAIL",
                "failures": batch_failures,
            })

        failures.extend(f"Batch {batch}: {failure}" for failure in batch_failures)

    report = {
        "schema": "nexamas-ui-documentation-recipe-host-static-proof/v1",
        "status": "PASS" if not failures else "FAIL",
        "batches": len(BATCH_RECIPES),
        "recipes": sum(len(v) for v in BATCH_RECIPES.values()),
        "publicMemberPairsChecked": total_pairs,
        "results": results,
        "failures": failures,
    }

    if args.write:
        out = root / "docs/_inventory/phase5-recipe-host-static-verification.json"
        out.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

    print(json.dumps(report, indent=2, ensure_ascii=False))
    return 0 if not failures else 1


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