#!/usr/bin/env python3
"""Audit MN103S/GDR-8050L emulator configs for firmware provenance mistakes.

The important invariant for this project is that all proof, session, and bridge
configs use the same chain-produced decrypted payload.  This helper fails closed
when a config still references the old handwritten fallback firmware path.
"""
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from typing import Any

CANONICAL_GDR8050L_0L23_REL = r"decrypted_payloads\GDR-8050L_0L23_5af3ae9b_firmware_4c61eb12_plaintext_7cab0f5c.bin"
CANONICAL_GDR8050L_0L23_BASENAME = "GDR-8050L_0L23_5af3ae9b_firmware_4c61eb12_plaintext_7cab0f5c.bin"
BANNED_MARKERS = (
    r"\firmware\gdr-8050l\8050l_fw_plaintext_256k.bin",
    "/firmware/gdr-8050l/8050l_fw_plaintext_256k.bin",
    "8050l_fw_plaintext_256k.bin",
)


def _norm(s: Any) -> str:
    return str(s or "").replace("/", "\\").lower()


def has_banned_marker(s: Any) -> bool:
    n = _norm(s)
    return any(_norm(m).strip("\\") in n for m in BANNED_MARKERS)


def basename(path: Any) -> str:
    return str(path or "").replace("\\", "/").rsplit("/", 1)[-1]


def strip_jsonc(text: str) -> str:
    out: list[str] = []
    i = 0
    in_str = False
    esc = False
    while i < len(text):
        ch = text[i]
        nxt = text[i + 1] if i + 1 < len(text) else ""
        if in_str:
            out.append(ch)
            if esc:
                esc = False
            elif ch == "\\":
                esc = True
            elif ch == '"':
                in_str = False
            i += 1
            continue
        if ch == '"':
            in_str = True
            out.append(ch)
            i += 1
            continue
        if ch == "/" and nxt == "/":
            i += 2
            while i < len(text) and text[i] not in "\r\n":
                i += 1
            continue
        if ch == "/" and nxt == "*":
            i += 2
            while i + 1 < len(text) and not (text[i] == "*" and text[i + 1] == "/"):
                i += 1
            i += 2
            continue
        out.append(ch)
        i += 1
    return "".join(out)


def load_json_or_jsonc(path: Path) -> Any:
    text = path.read_text(encoding="utf-8", errors="replace")
    try:
        return json.loads(text)
    except Exception:
        return json.loads(strip_jsonc(text))


def resolve_workspace_path(workspace: str, path: str) -> str:
    if not path:
        return path
    # Windows absolute path: D:\foo or D:/foo
    if re.match(r"^[A-Za-z]:[\\/]", path):
        return path
    if path.startswith("/") or path.startswith("\\\\"):
        return path
    if workspace:
        sep = "\\" if "\\" in workspace or re.match(r"^[A-Za-z]:", workspace) else "/"
        return workspace.rstrip("\\/") + sep + path.lstrip("\\/")
    return path


def collect_path_fields(obj: Any, prefix: str = "") -> list[tuple[str, str]]:
    fields: list[tuple[str, str]] = []
    interesting = {
        "custom_bin", "proof_firmware_image", "previous_custom_bin",
        "previous_proof_firmware_image", "selected_custom_bin",
        "original_custom_bin", "original_proof_firmware_image",
    }
    if isinstance(obj, dict):
        for k, v in obj.items():
            p = f"{prefix}.{k}" if prefix else str(k)
            if k in interesting and isinstance(v, (str, int, float)):
                fields.append((p, str(v)))
            elif isinstance(v, (dict, list)):
                fields.extend(collect_path_fields(v, p))
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            if isinstance(v, (dict, list)):
                fields.extend(collect_path_fields(v, f"{prefix}[{i}]"))
    return fields


def audit_file(path: Path, expect_firmware: str | None, scan_previous: bool) -> dict[str, Any]:
    text = path.read_text(encoding="utf-8", errors="replace")
    row: dict[str, Any] = {
        "path": str(path),
        "ok": True,
        "reasons": [],
        "config_version": None,
        "requires_emulator_version": None,
        "custom_bin": None,
        "proof_firmware_image": None,
        "fields": [],
    }
    try:
        obj = load_json_or_jsonc(path)
    except Exception as e:
        row["ok"] = False
        row["reasons"].append(f"parse_error:{e}")
        if has_banned_marker(text):
            row["reasons"].append("banned_fallback_marker_in_text")
        return row

    if not isinstance(obj, dict):
        row["ok"] = False
        row["reasons"].append("not_json_object")
        return row

    workspace = str(obj.get("workspace") or "")
    custom_bin = str(obj.get("custom_bin") or "")
    proof_fw = str(obj.get("proof_firmware_image") or "")
    row["config_version"] = obj.get("config_version")
    row["requires_emulator_version"] = obj.get("requires_emulator_version")
    row["custom_bin"] = custom_bin
    row["proof_firmware_image"] = proof_fw

    fields = collect_path_fields(obj)
    if not scan_previous:
        fields = [(k, v) for k, v in fields if not k.split(".")[-1].startswith("previous_") and "original_" not in k]
    row["fields"] = [{"field": k, "value": v} for k, v in fields]

    for field, value in fields:
        if has_banned_marker(value):
            row["ok"] = False
            row["reasons"].append(f"banned_fallback:{field}")

    if not custom_bin:
        row["ok"] = False
        row["reasons"].append("missing_custom_bin")
    else:
        resolved = resolve_workspace_path(workspace, custom_bin)
        if basename(resolved).lower() != CANONICAL_GDR8050L_0L23_BASENAME.lower():
            row["ok"] = False
            row["reasons"].append("custom_bin_not_canonical_gdr8050l_0l23")

    if proof_fw:
        resolved_proof = resolve_workspace_path(workspace, proof_fw)
        if basename(resolved_proof).lower() != CANONICAL_GDR8050L_0L23_BASENAME.lower():
            row["ok"] = False
            row["reasons"].append("proof_firmware_image_not_canonical_gdr8050l_0l23")
        if custom_bin and basename(resolved_proof).lower() != basename(resolve_workspace_path(workspace, custom_bin)).lower():
            row["ok"] = False
            row["reasons"].append("custom_bin_proof_firmware_mismatch")

    if expect_firmware:
        exp_base = basename(expect_firmware).lower()
        if custom_bin and basename(resolve_workspace_path(workspace, custom_bin)).lower() != exp_base:
            row["ok"] = False
            row["reasons"].append("custom_bin_not_expected")
        if proof_fw and basename(resolve_workspace_path(workspace, proof_fw)).lower() != exp_base:
            row["ok"] = False
            row["reasons"].append("proof_firmware_image_not_expected")

    fwprov = obj.get("firmware_provenance") or {}
    if isinstance(fwprov, dict):
        if fwprov.get("banned_fallback_detected") is True:
            # This may be true because an old input was repaired. It is useful info,
            # not a failure, as long as live custom_bin/proof_firmware_image are clean.
            row["repaired_from_banned_fallback"] = True
    return row


def write_md(path: Path, report: dict[str, Any]) -> None:
    lines = [
        "# GDR firmware provenance audit",
        "",
        f"- ok: `{report['ok']}`",
        f"- checked: `{report['checked']}`",
        f"- failed: `{report['failed']}`",
        "",
        "| file | ok | config_version | reason | custom_bin | proof_firmware_image |",
        "|---|---|---|---|---|---|",
    ]
    for r in report["rows"]:
        reasons = ", ".join(r.get("reasons") or ["OK"])
        lines.append(
            f"| `{r.get('path')}` | `{r.get('ok')}` | `{r.get('config_version')}` | `{reasons}` | `{r.get('custom_bin') or ''}` | `{r.get('proof_firmware_image') or ''}` |"
        )
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("configs", nargs="*", help="Config JSON/JSONC files to audit")
    ap.add_argument("--glob", action="append", default=[], help="Additional glob(s) to expand, e.g. '*.json'")
    ap.add_argument("--expect-firmware", default=CANONICAL_GDR8050L_0L23_BASENAME)
    ap.add_argument("--scan-previous", action="store_true", help="Also fail if previous_/original_ fields contain banned fallback paths")
    ap.add_argument("--json", dest="json_out")
    ap.add_argument("--md", dest="md_out")
    args = ap.parse_args()

    paths: list[Path] = []
    for c in args.configs:
        paths.append(Path(c))
    for g in args.glob:
        paths.extend(sorted(Path().glob(g)))
    seen = set()
    unique = []
    for p in paths:
        key = str(p)
        if key not in seen:
            seen.add(key)
            unique.append(p)
    if not unique:
        raise SystemExit("no config files supplied")

    rows = [audit_file(p, args.expect_firmware, args.scan_previous) for p in unique]
    report = {"ok": all(r.get("ok") for r in rows), "checked": len(rows), "failed": sum(0 if r.get("ok") else 1 for r in rows), "rows": rows}
    if args.json_out:
        Path(args.json_out).write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
    if args.md_out:
        write_md(Path(args.md_out), report)
    print(f"[PROVENANCE] checked={report['checked']} failed={report['failed']} all={report['ok']}")
    for r in rows:
        status = "OK" if r.get("ok") else "FAIL"
        reasons = ",".join(r.get("reasons") or ["OK"])
        print(f"[{status}] {r.get('path')} {reasons}")
    return 0 if report["ok"] else 2


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