#!/usr/bin/env python3
"""LibreOffice headless export (file-remediation Engine E1 library).

Two conversions, both via the vendored `soffice` CLI:
  - export_pdf: tagged PDF/UA export (PDFUACompliance + UseTaggedPDF filter
    options). Impress (.pptx) output ALWAYS gets artifact_postfix — Impress
    paints each slide's background twice; the second fill is unmarked and
    trips one veraPDF UA1 clause per slide (verified live, see plan §Engine
    E1). Never claims pixel-identity to a Word/PowerPoint rendering — this
    is a re-render, not the fidelity gate's exact-zero PDF-vs-PDF compare.
  - render_pdf_plain: plain convert, no filter options — used by
    remediate_docx.py/remediate_pptx.py for the before/after appearance
    check (input DOCX/PPTX render vs. remediated-copy render).

Usage: python lo_export.py IN.docx OUT.pdf [--plain] [--json]
"""

from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

SCRIPTS_DIR = Path(__file__).resolve().parent
if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPTS_DIR))

import pikepdf

import pdf_tree_edit
import setup_remediation

EXPORT_TIMEOUT_S = 300

_FILTER_BY_EXT = {".docx": "writer_pdf_Export", ".pptx": "impress_pdf_Export"}

_PDFUA_OPTS = json.dumps({
    "PDFUACompliance": {"type": "boolean", "value": "true"},
    "UseTaggedPDF": {"type": "boolean", "value": "true"},
})

_PAINT_OPS = {"f", "f*", "F", "S", "s", "B", "B*", "b", "b*", "sh", "Do", "TJ", "Tj", "'", '"'}
_BEGIN_MARKED = ("BMC", "BDC")


def get_soffice():
    """Return the vendored `soffice` binary Path, or None if not installed."""
    return setup_remediation.get_libreoffice()


def artifact_postfix(pdf_path):
    """Fix Impress's double-background-paint bug in place: wrap every
    depth-0 unmarked content-stream instruction run that contains a paint
    op in /Artifact BMC..EMC. Verified live: both a well-formed and a
    sloppy Impress-exported deck pass veraPDF UA1 106/106 after this."""
    pdf = pikepdf.open(str(pdf_path), allow_overwriting_input=True)
    for page in pdf.pages:
        out, run, depth = [], [], 0

        def flush():
            if not run:
                return
            if any(str(i.operator) in _PAINT_OPS for i in run):
                out.append(pikepdf.ContentStreamInstruction(
                    [pikepdf.Name("/Artifact")], pikepdf.Operator("BMC")))
                out.extend(run)
                out.append(pikepdf.ContentStreamInstruction([], pikepdf.Operator("EMC")))
            else:
                out.extend(run)
            run.clear()

        for instr in pikepdf.parse_content_stream(page):
            op = str(instr.operator)
            if depth == 0 and op not in _BEGIN_MARKED and op != "EMC":
                run.append(instr)
                continue
            flush()
            out.append(instr)
            if op in _BEGIN_MARKED:
                depth += 1
            elif op == "EMC":
                depth = max(0, depth - 1)
        flush()
        page.Contents = pdf.make_stream(pikepdf.unparse_content_stream(out))
    pdf_tree_edit.tag_link_annotations(pdf)  # same reopen pass, no double-open
    pdf.save(str(pdf_path))
    pdf.close()


def _fix_link_annotations(pdf_path):
    """Reopen an exported PDF and fix Link annotations missing /Contents (see
    pdf_tree_edit.tag_link_annotations). Used by export paths that don't
    already reopen the file (i.e. non-Impress -> no artifact_postfix pass)."""
    pdf = pikepdf.open(str(pdf_path), allow_overwriting_input=True)
    fixed = pdf_tree_edit.tag_link_annotations(pdf)
    if fixed:
        pdf.save(str(pdf_path))
    pdf.close()
    return fixed


def _soffice_convert(office_path, out_pdf, filter_opts=""):
    soffice = get_soffice()
    if not soffice:
        raise RuntimeError(
            "vendored LibreOffice not installed — run "
            "`python scripts/setup_remediation.py --install-libreoffice` first"
        )
    office_path = Path(office_path)
    lo_filter = _FILTER_BY_EXT.get(office_path.suffix.lower())
    if not lo_filter:
        raise RuntimeError(f"unsupported extension for PDF export: {office_path.suffix}")

    with tempfile.TemporaryDirectory(prefix="asucb-lo-") as tmp:
        cmd = [str(soffice), "--headless", "--convert-to", f"pdf:{lo_filter}{filter_opts}",
               "--outdir", tmp, str(office_path)]
        try:
            proc = subprocess.run(cmd, capture_output=True, text=True, timeout=EXPORT_TIMEOUT_S)
        except subprocess.TimeoutExpired as exc:
            raise RuntimeError(f"soffice export timed out after {EXPORT_TIMEOUT_S}s") from exc
        if proc.returncode != 0:
            raise RuntimeError(f"soffice export failed (rc={proc.returncode}): {proc.stderr[:500]}")
        produced = Path(tmp) / f"{office_path.stem}.pdf"
        if not produced.exists():
            raise RuntimeError(f"soffice did not produce {produced.name} in {tmp}")
        out_pdf = Path(out_pdf)
        out_pdf.parent.mkdir(parents=True, exist_ok=True)
        shutil.move(str(produced), str(out_pdf))
    return out_pdf, lo_filter


def export_pdf(office_path, out_pdf, pdfua=True):
    """Tagged PDF export. Impress output always gets artifact_postfix."""
    filter_opts = f":{_PDFUA_OPTS}" if pdfua else ""
    out_pdf, lo_filter = _soffice_convert(office_path, out_pdf, filter_opts)
    if lo_filter == "impress_pdf_Export":
        artifact_postfix(out_pdf)  # link-annotation fix runs in this same pass
    else:
        _fix_link_annotations(out_pdf)
    return out_pdf


def render_pdf_plain(office_path, out_pdf):
    """Plain convert, no filter options — appearance-check rendering only."""
    out_pdf, _lo_filter = _soffice_convert(office_path, out_pdf)
    return out_pdf


def main():
    parser = argparse.ArgumentParser(description="LibreOffice headless PDF export (Engine E1)")
    parser.add_argument("input")
    parser.add_argument("output")
    parser.add_argument("--plain", action="store_true",
                         help="Plain convert (no PDFUA options, no artifact postfix) — "
                              "appearance checks only")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    try:
        out = render_pdf_plain(args.input, args.output) if args.plain else export_pdf(args.input, args.output)
    except RuntimeError 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)

    print(json.dumps({"output": str(out)}, indent=2) if args.json else f"OK: {out}")


if __name__ == "__main__":
    main()
