#!/usr/bin/env python3
"""Generate and audit a synthetic bilingual AcroForm fixture."""

from __future__ import annotations

import csv
import hashlib
import json
from io import BytesIO
from pathlib import Path

import pypdf
import reportlab
from pypdf import PdfReader, PdfWriter
from pypdf.generic import (
    ArrayObject,
    DictionaryObject,
    NameObject,
    TextStringObject,
)
from reportlab.lib.colors import HexColor, black, white
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas


ROOT = Path(__file__).resolve().parent
SOURCE = ROOT / "source-fillable-form.pdf"
TARGET = ROOT / "translated-fillable-form.pdf"
AUDIT = ROOT / "form-field-audit.csv"
MANIFEST = ROOT / "fixture-manifest.json"


FIELD_NAMES = [
    "applicant_name",
    "contact_email",
    "account_type",
    "delivery_method",
    "page_count",
    "estimated_words",
    "consent",
    "submit_form",
]
REQUIRED_FLAG = 1 << 1
READ_ONLY_FLAG = 1
CALCULATION_SCRIPT = 'event.value = Number(this.getField("page_count").value) * 250;'
SUBMIT_URL = "https://example.invalid/synthetic-form-test"


COPY = {
    "en": {
        "title": "Reader request form",
        "intro": "Synthetic AcroForm fixture — not a BookTranslator output.",
        "name": "Name",
        "name_tooltip": "Enter the reader's full name",
        "email": "Email",
        "email_tooltip": "Enter the delivery email address",
        "account": "Account type",
        "account_tooltip": "Choose the account type",
        "accounts": [("personal", "Personal"), ("team", "Team")],
        "method": "Delivery method",
        "method_tooltip": "Choose how to receive the translated document",
        "methods": [("email", "Email"), ("download", "Secure download")],
        "pages": "Estimated page count",
        "pages_tooltip": "Enter the estimated number of pages",
        "words": "Calculated word estimate",
        "words_tooltip": "Estimated pages multiplied by 250",
        "consent": "I confirm that I may process this document.",
        "consent_tooltip": "Confirm permission to process the document",
        "submit": "Send test data",
        "submit_tooltip": "Submit synthetic test data",
        "note": "Required fields are marked with an asterisk.",
    },
    "es": {
        "title": "Formulario de solicitud del lector",
        "intro": "Formulario AcroForm sintético; no es una salida de BookTranslator.",
        "name": "Nombre",
        "name_tooltip": "Escriba el nombre completo del lector",
        "email": "Correo electrónico",
        "email_tooltip": "Escriba la dirección de correo de entrega",
        "account": "Tipo de cuenta",
        "account_tooltip": "Elija el tipo de cuenta",
        "accounts": [("personal", "Personal"), ("team", "Equipo")],
        "method": "Método de entrega",
        "method_tooltip": "Elija cómo recibir el documento traducido",
        "methods": [("email", "Correo electrónico"), ("download", "Descarga segura")],
        "pages": "Número estimado de páginas",
        "pages_tooltip": "Escriba el número estimado de páginas",
        "words": "Estimación calculada de palabras",
        "words_tooltip": "Páginas estimadas multiplicadas por 250",
        "consent": "Confirmo que puedo procesar este documento.",
        "consent_tooltip": "Confirme el permiso para procesar el documento",
        "submit": "Enviar datos de prueba",
        "submit_tooltip": "Enviar datos sintéticos de prueba",
        "note": "Los campos obligatorios están marcados con un asterisco.",
    },
}


def build_static_page(language: str) -> bytes:
    text = COPY[language]
    stream = BytesIO()
    doc = canvas.Canvas(stream, pagesize=letter, invariant=1)
    doc.setTitle("Synthetic Fillable PDF Form Fixture")
    doc.setSubject("CC0 test fixture; not a BookTranslator product output")
    doc.setFont("Helvetica-Bold", 22)
    doc.drawString(72, 750, text["title"])
    doc.setFont("Helvetica", 10)
    doc.drawString(72, 728, text["intro"])
    doc.setFont("Helvetica-Bold", 11)
    doc.drawString(72, 682, f'{text["name"]} *')
    doc.drawString(72, 610, f'{text["email"]} *')
    doc.drawString(72, 538, f'{text["account"]} *')
    doc.drawString(72, 466, f'{text["method"]} *')
    doc.drawString(72, 394, f'{text["pages"]} *')
    doc.drawString(292, 394, text["words"])
    doc.setFont("Helvetica", 10)
    doc.drawString(94, 510, text["accounts"][0][1])
    doc.drawString(242, 510, text["accounts"][1][1])
    doc.drawString(100, 302, text["consent"])
    doc.setFillColor(HexColor("#2357D9"))
    doc.rect(72, 238, 160, 30, fill=True, stroke=False)
    doc.setFillColor(white)
    doc.drawCentredString(152, 248, text["submit"])
    doc.setFillColor(black)
    doc.drawString(72, 208, text["note"])
    doc.save()
    return stream.getvalue()


def build_raw(language: str) -> bytes:
    text = COPY[language]
    form_stream = BytesIO()
    doc = canvas.Canvas(form_stream, pagesize=letter, invariant=1)
    form = doc.acroForm
    doc.setTitle("Synthetic Fillable PDF Form Fixture")
    doc.setSubject("CC0 test fixture; not a BookTranslator product output")

    # Build widgets on a separate page layer so their appearance generation
    # cannot disturb localized static text.
    form.textfield(
        name="applicant_name",
        tooltip="applicant name tooltip",
        x=72,
        y=650,
        width=320,
        height=24,
        borderColor=black,
        fillColor=white,
        textColor=black,
        forceBorder=True,
        fieldFlags="required",
    )

    form.textfield(
        name="contact_email",
        tooltip="contact email tooltip",
        x=72,
        y=578,
        width=320,
        height=24,
        borderColor=black,
        fillColor=white,
        textColor=black,
        forceBorder=True,
        fieldFlags="required",
    )

    form.radio(
        name="account_type",
        tooltip="account type tooltip",
        value="personal",
        selected=True,
        x=72,
        y=502,
        buttonStyle="circle",
        borderColor=black,
        fillColor=white,
        textColor=HexColor("#2357D9"),
        size=14,
    )
    form.radio(
        name="account_type",
        tooltip="account type tooltip",
        value="team",
        selected=False,
        x=220,
        y=502,
        buttonStyle="circle",
        borderColor=black,
        fillColor=white,
        textColor=HexColor("#2357D9"),
        size=14,
    )

    form.choice(
        name="delivery_method",
        tooltip="delivery method tooltip",
        value=text["methods"][0][1],
        options=[label for _, label in text["methods"]],
        x=72,
        y=434,
        width=220,
        height=24,
        borderColor=black,
        fillColor=white,
        textColor=black,
        forceBorder=True,
        fieldFlags="combo required",
    )

    form.textfield(
        name="page_count",
        tooltip="page count tooltip",
        x=72,
        y=362,
        width=180,
        height=24,
        borderColor=black,
        fillColor=white,
        textColor=black,
        forceBorder=True,
        fieldFlags="required",
    )

    form.textfield(
        name="estimated_words",
        tooltip="estimated words tooltip",
        x=292,
        y=362,
        width=180,
        height=24,
        borderColor=black,
        fillColor=HexColor("#E8E6E1"),
        textColor=black,
        forceBorder=True,
        fieldFlags="readOnly",
    )

    form.checkbox(
        name="consent",
        tooltip="consent tooltip",
        checked=False,
        buttonStyle="check",
        x=72,
        y=298,
        size=18,
        borderColor=black,
        fillColor=white,
        textColor=HexColor("#2357D9"),
        forceBorder=True,
        fieldFlags="required",
    )
    doc.save()

    form_reader = PdfReader(BytesIO(form_stream.getvalue()))
    static_reader = PdfReader(BytesIO(build_static_page(language)))
    writer = PdfWriter()
    writer.clone_document_from_reader(form_reader)
    writer.pages[0].merge_page(static_reader.pages[0], over=False)
    combined = BytesIO()
    writer.write(combined)
    return combined.getvalue()


def normalize(language: str, output: Path) -> None:
    reader = PdfReader(BytesIO(build_raw(language)))
    writer = PdfWriter()
    writer.clone_document_from_reader(reader)
    acroform = writer._root_object["/AcroForm"].get_object()
    fields = {
        str(field_ref.get_object()["/T"]): field_ref.get_object()
        for field_ref in acroform["/Fields"]
    }

    # Keep machine-facing export values stable while localizing visible choices.
    method = fields["delivery_method"]
    method[NameObject("/Opt")] = ArrayObject(
        [
            ArrayObject([TextStringObject(export), TextStringObject(label)])
            for export, label in COPY[language]["methods"]
        ]
    )
    method[NameObject("/V")] = TextStringObject("email")
    method[NameObject("/DV")] = TextStringObject("email")

    account = fields["account_type"]
    account[NameObject("/Opt")] = ArrayObject(
        [TextStringObject(export) for export, _ in COPY[language]["accounts"]]
    )

    calculation = fields["estimated_words"]
    calculation[NameObject("/AA")] = DictionaryObject(
        {
            NameObject("/C"): DictionaryObject(
                {
                    NameObject("/S"): NameObject("/JavaScript"),
                    NameObject("/JS"): TextStringObject(CALCULATION_SCRIPT),
                }
            )
        }
    )

    consent = fields["consent"]
    consent[NameObject("/Opt")] = ArrayObject([TextStringObject("consent")])
    for name, tooltip_key in {
        "applicant_name": "name_tooltip",
        "contact_email": "email_tooltip",
        "account_type": "account_tooltip",
        "delivery_method": "method_tooltip",
        "page_count": "pages_tooltip",
        "estimated_words": "words_tooltip",
        "consent": "consent_tooltip",
    }.items():
        fields[name][NameObject("/TU")] = TextStringObject(COPY[language][tooltip_key])

    submit = writer.add_annotation(
        0,
        {
            "/Type": "/Annot",
            "/Subtype": "/Widget",
            "/FT": "/Btn",
            "/Ff": 1 << 16,
            "/F": 4,
            "/Rect": [72, 238, 232, 268],
            "/T": "submit_form",
            "/TU": COPY[language]["submit_tooltip"],
            "/A": {"/S": "/SubmitForm", "/F": SUBMIT_URL, "/Flags": 4},
        },
    )
    acroform[NameObject("/Fields")].append(submit.indirect_reference)
    writer.pages[0][NameObject("/Tabs")] = NameObject("/A")

    writer.add_metadata(
        {
            "/Title": "Synthetic Fillable PDF Form Fixture",
            "/Subject": "CC0 test fixture; not a BookTranslator product output",
        }
    )
    with output.open("wb") as handle:
        writer.write(handle)


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


def field_type(field: dict[str, object]) -> str:
    return {"/Tx": "text", "/Ch": "choice", "/Btn": "button"}.get(
        str(field.get("/FT")), str(field.get("/FT"))
    )


def options(field: dict[str, object]) -> list[list[str]]:
    result: list[list[str]] = []
    for item in field.get("/Opt", []):
        if isinstance(item, list):
            result.append([str(item[0]), str(item[1])])
        else:
            result.append([str(item), str(item)])
    return result


def action_fingerprint(field: dict[str, object]) -> str:
    action = field.get("/A")
    if action:
        return f'{action.get("/S")}:{action.get("/F", "")}:{action.get("/Flags", "")}'
    additional = field.get("/AA")
    if additional and additional.get("/C"):
        calculation = additional["/C"]
        return f'{calculation.get("/S")}:{calculation.get("/JS", "")}'
    return ""


def inventory(path: Path) -> dict[str, dict[str, object]]:
    reader = PdfReader(path)
    fields = reader.get_fields() or {}
    result: dict[str, dict[str, object]] = {}
    for name in FIELD_NAMES:
        field = fields[name]
        flags = int(field.get("/Ff", 0))
        result[name] = {
            "type": field_type(field),
            "required": bool(flags & REQUIRED_FLAG),
            "read_only": bool(flags & READ_ONLY_FLAG),
            "tooltip": str(field.get("/TU", "")),
            "options": options(field),
            "action": action_fingerprint(field),
        }
    return result


def widget_order(path: Path) -> tuple[str, list[str]]:
    reader = PdfReader(path)
    page = reader.pages[0]
    order: list[str] = []
    for annotation_ref in page.get("/Annots", []):
        annotation = annotation_ref.get_object()
        parent = annotation.get("/Parent")
        owner = parent.get_object() if parent is not None else annotation
        name = str(owner.get("/T", ""))
        if name and name not in order:
            order.append(name)
    return str(page.get("/Tabs", "")), order


def main() -> None:
    normalize("en", SOURCE)
    normalize("es", TARGET)
    source = inventory(SOURCE)
    target = inventory(TARGET)
    source_tab_mode, source_order = widget_order(SOURCE)
    target_tab_mode, target_order = widget_order(TARGET)
    tab_order_equal = (
        source_tab_mode == target_tab_mode == "/A"
        and source_order == target_order == FIELD_NAMES
    )
    if not tab_order_equal:
        raise ValueError(
            "Widget order differs: "
            f"source=({source_tab_mode}, {source_order}), "
            f"target=({target_tab_mode}, {target_order})"
        )
    rows: list[dict[str, object]] = []
    for order, name in enumerate(source_order, start=1):
        source_field = source[name]
        target_field = target[name]
        source_exports = [item[0] for item in source_field["options"]]
        target_exports = [item[0] for item in target_field["options"]]
        structural_pass = (
            source_field["type"] == target_field["type"]
            and source_field["required"] == target_field["required"]
            and source_field["read_only"] == target_field["read_only"]
            and source_exports == target_exports
            and source_field["action"] == target_field["action"]
        )
        rows.append(
            {
                "tab_order": order,
                "field_name": name,
                "field_type": source_field["type"],
                "required": str(source_field["required"]).lower(),
                "read_only": str(source_field["read_only"]).lower(),
                "source_tooltip": source_field["tooltip"],
                "target_tooltip": target_field["tooltip"],
                "source_options": json.dumps(source_field["options"], ensure_ascii=False),
                "target_options": json.dumps(target_field["options"], ensure_ascii=False),
                "export_values_preserved": str(source_exports == target_exports).lower(),
                "action_preserved": str(source_field["action"] == target_field["action"]).lower(),
                "audit_result": "PASS" if structural_pass else "FAIL",
            }
        )
    if any(row["audit_result"] != "PASS" for row in rows):
        raise ValueError(f"Form contract differs: {rows}")

    with AUDIT.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=rows[0].keys(), lineterminator="\n")
        writer.writeheader()
        writer.writerows(rows)

    manifest = {
        "license": "CC0-1.0",
        "disclaimer": "Synthetic fixture authored for this article; not a BookTranslator product output.",
        "generator": Path(__file__).name,
        "libraries": {"pypdf": pypdf.__version__, "reportlab": reportlab.Version},
        "files": {
            SOURCE.name: {"sha256": sha256(SOURCE), "pages": len(PdfReader(SOURCE).pages)},
            TARGET.name: {"sha256": sha256(TARGET), "pages": len(PdfReader(TARGET).pages)},
            AUDIT.name: {"sha256": sha256(AUDIT), "rows": len(rows)},
        },
        "checks": {
            "field_names_equal": list(source) == list(target) == FIELD_NAMES,
            "tab_mode": source_tab_mode,
            "source_widget_order": source_order,
            "target_widget_order": target_order,
            "tab_order_equal": tab_order_equal,
            "field_types_equal": all(source[name]["type"] == target[name]["type"] for name in FIELD_NAMES),
            "required_flags_equal": all(source[name]["required"] == target[name]["required"] for name in FIELD_NAMES),
            "read_only_flags_equal": all(source[name]["read_only"] == target[name]["read_only"] for name in FIELD_NAMES),
            "choice_export_values_equal": [item[0] for item in source["delivery_method"]["options"]] == [item[0] for item in target["delivery_method"]["options"]],
            "radio_export_values_equal": [item[0] for item in source["account_type"]["options"]] == [item[0] for item in target["account_type"]["options"]],
            "actions_equal": all(source[name]["action"] == target[name]["action"] for name in FIELD_NAMES),
            "tooltips_localized": all(source[name]["tooltip"] != target[name]["tooltip"] for name in FIELD_NAMES),
            "audit_rows_pass": all(row["audit_result"] == "PASS" for row in rows),
        },
    }
    MANIFEST.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


if __name__ == "__main__":
    main()
