#!/usr/bin/env python3
"""Render-diff fidelity gate (file-remediation Stage 3 — hard gate).

Remediated PDF output must be visually identical to the input (plan §3, §1).
Renders both PDFs page-by-page via PyMuPDF and compares raw pixel samples.

# ponytail: exact-zero is still the CLI/PDF-gate default (plan §3) — nonzero is
# a hard fail unless a caller explicitly opts into threshold_pct. The Engine E1
# appearance check (remediate_docx.py/remediate_pptx.py) is the first caller to
# pass a nonzero threshold: LibreOffice re-renders DOCX/PPTX from scratch, so
# exact-zero isn't the right bar there — style-application should still land
# near 0, just not bit-for-bit against a different renderer's output.

Usage: python fidelity_gate.py FILE_A FILE_B [--dpi 110] [--threshold-pct 0.0] [--json]
Exit: 0 identical (within threshold), 1 not identical, 2 error.
"""

from __future__ import annotations

import argparse
import json
import sys

import fitz  # PyMuPDF


def pixel_diff(pdf_a, pdf_b, dpi=110, threshold_pct=0.0) -> dict:
    """Compare rendered pixels of two PDFs page-by-page. Returns
    {identical, page_count, pages_diff: [{page, pct}], worst_pct, dpi}.
    identical = worst_pct <= threshold_pct (default 0.0 — exact-zero)."""
    a = fitz.open(str(pdf_a))
    b = fitz.open(str(pdf_b))

    if len(a) != len(b):
        return {
            "identical": False, "page_count": {"a": len(a), "b": len(b)},
            "pages_diff": [], "worst_pct": 100.0, "dpi": dpi,
            "reason": "page_count_mismatch",
        }

    pages_diff = []
    worst = 0.0
    for i in range(len(a)):
        pa = a[i].get_pixmap(dpi=dpi)
        pb = b[i].get_pixmap(dpi=dpi)
        if pa.width != pb.width or pa.height != pb.height:
            pct = 100.0
        elif pa.samples == pb.samples:  # fast path: identical is the common case
            pct = 0.0
        else:
            sa, sb = pa.samples, pb.samples
            diff_bytes = sum(1 for x, y in zip(sa, sb) if x != y)
            pct = 100.0 * diff_bytes / len(sa) if sa else 0.0
        if pct > 0:
            pages_diff.append({"page": i + 1, "pct": round(pct, 4)})
        worst = max(worst, pct)

    return {
        "identical": worst <= threshold_pct, "page_count": len(a),
        "pages_diff": pages_diff, "worst_pct": round(worst, 4), "dpi": dpi,
    }


def structural_diff(pdf_a, pdf_b, delta=64, dpi=110, threshold_pct=3.5) -> dict:
    """Structural render diff for the font-embed path: fraction of pixels whose
    grayscale intensity changes by MORE than `delta` (0-255). Ignores the
    sub-pixel anti-aliasing shift that makes raw pixel_diff useless for font
    substitution (a metric-clone outline moves every glyph edge a hair, ~10% of
    a dense text page, while being visually identical); catches structural
    breakage (missing-glyph .notdef boxes, reflow) which flips whole regions.
    Returns {identical, page_count, pages_diff:[{page,pct}], worst_pct, dpi,
    delta}. identical = worst_pct <= threshold_pct."""
    a = fitz.open(str(pdf_a))
    b = fitz.open(str(pdf_b))

    if len(a) != len(b):
        return {
            "identical": False, "page_count": {"a": len(a), "b": len(b)},
            "pages_diff": [], "worst_pct": 100.0, "dpi": dpi, "delta": delta,
            "reason": "page_count_mismatch",
        }

    pages_diff = []
    worst = 0.0
    for i in range(len(a)):
        pa = a[i].get_pixmap(dpi=dpi, colorspace=fitz.csGRAY)
        pb = b[i].get_pixmap(dpi=dpi, colorspace=fitz.csGRAY)
        if pa.width != pb.width or pa.height != pb.height:
            pct = 100.0
        elif pa.samples == pb.samples:  # fast path: identical is the common case
            pct = 0.0
        else:
            sa, sb = pa.samples, pb.samples
            big = sum(1 for x, y in zip(sa, sb) if abs(x - y) > delta)
            pct = 100.0 * big / len(sa) if sa else 0.0
        if pct > 0:
            pages_diff.append({"page": i + 1, "pct": round(pct, 4)})
        worst = max(worst, pct)

    return {
        "identical": worst <= threshold_pct, "page_count": len(a),
        "pages_diff": pages_diff, "worst_pct": round(worst, 4), "dpi": dpi,
        "delta": delta,
    }


def main():
    parser = argparse.ArgumentParser(description="Render-diff fidelity gate")
    parser.add_argument("file_a")
    parser.add_argument("file_b")
    parser.add_argument("--dpi", type=int, default=110)
    parser.add_argument("--threshold-pct", type=float, default=0.0,
                         help="Max allowed worst-page diff pct (default 0.0 — exact-zero)")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    try:
        result = pixel_diff(args.file_a, args.file_b, dpi=args.dpi, threshold_pct=args.threshold_pct)
    except Exception as exc:
        if args.json:
            print(json.dumps({"error": str(exc)}), file=sys.stderr)
        else:
            print(f"ERROR: {exc}", file=sys.stderr)
        sys.exit(2)

    if args.json:
        print(json.dumps(result, indent=2))
    else:
        print(f"{'IDENTICAL' if result['identical'] else 'DIFFERS'} "
              f"(worst page {result['worst_pct']}% @ {result['dpi']}dpi)")
    sys.exit(0 if result["identical"] else 1)


if __name__ == "__main__":
    main()
