"""Measure what a reviewer sees, what an extraction pipeline hands a model, and what Wolf labels.

Reads the fixtures and plants.json that make_fixtures.py wrote into OUT, then for each fixture:

  reviewer view  `wolf render` draws page 1, slide 1, or the first sheet to PNG. The PDF is also
                 drawn by Poppler's pdftoppm for comparison.
  model view     the plain text of `wolf extract --what text` and the Markdown of MarkItDown, a
                 widely used converter; each planted sentence is recorded as inline or absent.
  Wolf labels    the hidden channels of `wolf extract --what structure` and the receipt's
                 hidden_content summary; each planted sentence is recorded with the channel that
                 labels it, or as unlabelled.

Writes results.json, the published outputs, card.png, copies of both scripts, and SHA256SUMS into
OUT. Run from the site root:

    export WOLF_BIN=/path/to/wolf   # wolf 0.2.17 release binary
    uv run --no-project --python 3.12 --with python-docx==1.2.0 --with python-pptx==1.0.2 \
        --with openpyxl==3.1.5 --with pillow==13.0.0 \
        python scripts/examples/hidden-text/make_fixtures.py public/examples/hidden-text
    uv run --no-project --python 3.12 --with 'markitdown[all]==0.1.9' --with pillow==13.0.0 \
        python scripts/examples/hidden-text/run_demo.py public/examples/hidden-text

PDFTOPPM may name the pdftoppm binary (default: pdftoppm on PATH). CARD_FONT may name the font file
for the share card (default: the site's Archivo variable font, SIL Open Font License).
"""

import hashlib
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
from importlib import metadata
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

HERE = Path(__file__).resolve().parent
OUT = Path(sys.argv[1]).resolve()
FONT = Path(os.environ.get("CARD_FONT", HERE.parents[2] / "app/fonts/archivo-variable.woff2"))
WOLF = os.environ.get("WOLF_BIN")
PDFTOPPM = os.environ.get("PDFTOPPM", "pdftoppm")
DPI = "110"

FORMATS = {
    "docx": {"label": "Word invoice", "render": ["--pages", "1"]},
    "xlsx": {"label": "Excel payment schedule", "render": ["--sheet", "Payments"]},
    "pptx": {"label": "PowerPoint vendor review", "render": ["--pages", "1"]},
    "pdf": {"label": "PDF account statement", "render": ["--pages", "1"]},
}


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


def run(argv: list[str], cwd: Path) -> subprocess.CompletedProcess:
    done = subprocess.run(argv, cwd=cwd, capture_output=True, text=True)
    if done.returncode != 0:
        raise SystemExit(f"{' '.join(argv)} exited {done.returncode}:\n{done.stdout}\n{done.stderr}")
    return done


def shown(argv: list[str]) -> str:
    """The command as published: the program by name, every path relative to the work directory."""
    return " ".join(["wolf" if arg == WOLF else arg for arg in argv])


def published_receipt(path: Path) -> dict:
    receipt = json.loads(path.read_text())
    receipt["invocation"]["argv"][0] = "wolf"
    return receipt


def present(key: str, text: str) -> bool:
    return " ".join(key.split()) in " ".join(text.split())


def measure(fmt: str, fixture: dict, work: Path) -> dict:
    name = fixture["file"]
    stem = Path(name).stem
    shutil.copy(OUT / name, work / name)

    # Reviewer view: Wolf draws the first page, slide, or sheet.
    render_argv = [WOLF, "render", name, "--to", "png", "--dpi", DPI, *FORMATS[fmt]["render"], "--outdir", "render"]
    run([*render_argv, "--receipt", f"render/{stem}.receipt.json"], work)
    pages = sorted((work / "render").glob(f"{stem}-*.png"))
    assert len(pages) == 1, f"{name}: expected one rendered page, got {[p.name for p in pages]}"
    reviewer_png = f"{stem}-reviewer.png"
    shutil.copy(pages[0], OUT / reviewer_png)
    width, height = Image.open(pages[0]).size
    reviewer = {
        "engine": "wolf render",
        "command": shown(render_argv),
        "image": reviewer_png,
        "width": width,
        "height": height,
    }
    if fmt == "pdf":
        poppler_argv = [PDFTOPPM, "-png", "-r", DPI, "-f", "1", "-l", "1", "-singlefile", name, f"{stem}-poppler"]
        run(poppler_argv, work)
        shutil.copy(work / f"{stem}-poppler.png", OUT / f"{stem}-poppler.png")
        p_width, p_height = Image.open(work / f"{stem}-poppler.png").size
        reviewer["comparison"] = {
            "engine": "Poppler pdftoppm",
            "command": " ".join(["pdftoppm", *poppler_argv[1:]]),
            "image": f"{stem}-poppler.png",
            "width": p_width,
            "height": p_height,
        }

    # Model view (Wolf plain text) and Wolf labels from one extract run.
    extract_argv = [WOLF, "extract", name, "--what", "text,structure", "--outdir", "extract"]
    run([*extract_argv, "--receipt", f"extract/{stem}.receipt.json"], work)
    wolf_text = (work / "extract" / f"{stem}.txt").read_text()
    structure = json.loads((work / "extract" / f"{stem}.structure.json").read_text())
    receipt = published_receipt(work / "extract" / f"{stem}.receipt.json")
    assert receipt["hidden_content"] == structure["hidden_content"], name
    shutil.copy(work / "extract" / f"{stem}.txt", OUT / f"{stem}.wolf.txt")
    shutil.copy(work / "extract" / f"{stem}.structure.json", OUT / f"{stem}.structure.json")
    (OUT / f"{stem}.receipt.json").write_text(json.dumps(receipt, indent=1) + "\n")

    # The same input with only text requested: the text payload is byte-identical, and the receipt
    # still carries the hidden_content summary.
    text_only_argv = [WOLF, "extract", name, "--what", "text", "--outdir", "text-only"]
    run([*text_only_argv, "--receipt", f"text-only/{stem}.receipt.json"], work)
    text_only = {
        "command": shown(text_only_argv),
        "text_identical": (work / "text-only" / f"{stem}.txt").read_bytes()
        == (work / "extract" / f"{stem}.txt").read_bytes(),
        "receipt_hidden_content_identical": json.loads((work / "text-only" / f"{stem}.receipt.json").read_text()).get(
            "hidden_content"
        )
        == receipt["hidden_content"],
    }

    # Model view: MarkItDown's Markdown, through its command line.
    markitdown = run([sys.executable, "-m", "markitdown", name], work).stdout
    (OUT / f"{stem}.markitdown.md").write_text(markitdown)

    hidden = [channel for channel in structure["channels"] if not channel["visible"]]
    plants = []
    for item in fixture["plants"]:
        label = next((channel for channel in hidden if present(item["key"], channel.get("text", ""))), None)
        plants.append(
            {
                **item,
                "wolf_text": "inline" if present(item["key"], wolf_text) else "absent",
                "markitdown": "inline" if present(item["key"], markitdown) else "absent",
                "wolf_label": None
                if label is None
                else {key: label[key] for key in ("channel", "locator", "reason", "visible", "chars")},
            }
        )
    return {
        "label": FORMATS[fmt]["label"],
        "file": name,
        "sha256": sha256(OUT / name),
        "reviewer": reviewer,
        "model": {
            "wolf_text": {"command": shown(extract_argv), "file": f"{stem}.wolf.txt", "text": wolf_text},
            "markitdown": {"command": f"markitdown {name}", "file": f"{stem}.markitdown.md", "text": markitdown},
            "wolf_text_only_run": text_only,
        },
        "labels": {
            "command": shown(extract_argv),
            "structure": f"{stem}.structure.json",
            "structure_schema": structure["schema"],
            "receipt": f"{stem}.receipt.json",
            "hidden_content": receipt["hidden_content"],
            "warnings": [{"code": w["code"], "message": w["message"]} for w in receipt["warnings"]],
            "channels": structure["channels"],
        },
        "plants": plants,
        "counts": {
            "planted": len(plants),
            "labelled": sum(p["wolf_label"] is not None for p in plants),
            "unlabelled": sum(p["wolf_label"] is None for p in plants),
            "wolf_text_inline": sum(p["wolf_text"] == "inline" for p in plants),
            "markitdown_inline": sum(p["markitdown"] == "inline" for p in plants),
            "hidden_channels": receipt["hidden_content"]["hidden_channels"],
        },
    }


# ---------------------------------------------------------------------------------------------
# Share card
# ---------------------------------------------------------------------------------------------

BG, CARD, LINE, INK, MUTED, AMBER = "#050506", "#111113", "#2a2a2e", "#eef1f5", "#9a9aa0", "#fbbf24"


def font(size: int, weight: str = "Regular") -> ImageFont.FreeTypeFont:
    face = ImageFont.truetype(str(FONT), size)
    face.set_variation_by_name(weight)
    return face


def wrap(draw: ImageDraw.ImageDraw, text: str, face, width: int) -> list[str]:
    lines, line = [], ""
    for word in text.split():
        trial = f"{line} {word}".strip()
        if draw.textlength(trial, font=face) <= width or not line:
            line = trial
        else:
            lines.append(line)
            line = word
    return [*lines, line]


def ink_blocks(page: Image.Image) -> list[tuple[int, int, int]]:
    """Return (top, bottom, right) for each block of inked rows, merging rows less than 12 px apart."""
    gray = page.convert("L")
    width, height = gray.size
    pixels = gray.load()
    blocks: list[list[int]] = []
    for y in range(height):
        ink = [x for x in range(width) if pixels[x, y] < 200]
        if not ink:
            continue
        if blocks and y - blocks[-1][1] < 12:
            blocks[-1][1] = y
            blocks[-1][2] = max(blocks[-1][2], ink[-1])
        else:
            blocks.append([y, y, ink[-1]])
    return [(top, bottom, right) for top, bottom, right in blocks]


def dashed_rectangle(draw: ImageDraw.ImageDraw, box: tuple[float, float, float, float], fill: str) -> None:
    left, top, right, bottom = box
    for start in range(int(left), int(right), 8):
        end = min(start + 4, right)
        draw.line((start, top, end, top), fill=fill, width=1)
        draw.line((start, bottom, end, bottom), fill=fill, width=1)
    for start in range(int(top), int(bottom), 8):
        end = min(start + 4, bottom)
        draw.line((left, start, left, end), fill=fill, width=1)
        draw.line((right, start, right, end), fill=fill, width=1)


def make_card(docx: dict) -> dict:
    plant = next(p for p in docx["plants"] if p["planted_as"] == "hidden_run")
    label = plant["wolf_label"]
    card = Image.new("RGB", (1200, 675), BG)
    draw = ImageDraw.Draw(card)
    draw.text((56, 44), "Text a reviewer never sees, labelled for the agent", font=font(40, "Bold"), fill=INK)
    draw.text(
        (56, 98),
        "Run wolf extract to label hidden text channels with visible: false and a reason.",
        font=font(22),
        fill=MUTED,
    )

    # Left: the rendered page, from the top of the invoice through the paragraph that ends in the
    # hidden run, stopping before the tracked-deletion paragraph. The tallest ink block is the line
    # item table; the payment-terms paragraph holding the hidden run is the next block.
    draw.text((56, 160), "ON THE RENDERED PAGE", font=font(15, "SemiBold"), fill=MUTED)
    page = Image.open(OUT / docx["reviewer"]["image"]).convert("RGB")
    blocks = ink_blocks(page)
    table = max(range(len(blocks)), key=lambda i: blocks[i][1] - blocks[i][0])
    if table + 2 >= len(blocks):
        raise SystemExit("invoice render layout changed: expected two paragraphs below the table")
    hidden_paragraph, deletion_paragraph = blocks[table + 1], blocks[table + 2]
    bottom = deletion_paragraph[0] - 4
    left, top, right = 70, 70, 870
    scale = 520 / (right - left)
    crop = page.crop((left, top, right, bottom))
    crop = crop.resize((520, round(crop.height * scale)), Image.LANCZOS)
    mask = Image.new("L", crop.size, 0)
    ImageDraw.Draw(mask).rounded_rectangle((0, 0, *crop.size), 12, fill=255)
    card.paste(crop, (56, 190), mask)
    # Mark where the hidden run sits: right after the last drawn character of that paragraph.
    marker = font(11, "SemiBold")
    marker_text = "hidden run here"
    mx = 56 + (hidden_paragraph[2] - left) * scale + 6
    mid = 190 + ((hidden_paragraph[0] + hidden_paragraph[1]) / 2 - top) * scale
    mw, mh = draw.textlength(marker_text, font=marker) + 12, 15
    dashed_rectangle(draw, (mx, mid - mh / 2, mx + mw, mid + mh / 2), "#b45309")
    draw.text((mx + 6, mid), marker_text, font=marker, fill="#b45309", anchor="lm")

    # Right: the hidden sentence as extracted, with its label.
    x, width = 616, 528
    draw.text((x, 160), "IN THE EXTRACTED TEXT", font=font(15, "SemiBold"), fill=MUTED)
    body = font(25, "Medium")
    lines = wrap(draw, plant["text"], body, width - 48)
    box_h = 40 + len(lines) * 36
    draw.rounded_rectangle((x, 190, x + width, 190 + box_h), 12, fill=CARD, outline=LINE)
    for index, line in enumerate(lines):
        draw.text((x + 24, 210 + index * 36), line, font=body, fill=AMBER)
    y = 190 + box_h + 28
    draw.text((x, y), "WOLF LABEL", font=font(15, "SemiBold"), fill=MUTED)
    y += 30
    chip = font(22, "SemiBold")
    cx = x
    for text in (f"visible: {str(label['visible']).lower()}", f"reason: {label['reason']}"):
        w = draw.textlength(text, font=chip) + 32
        draw.rounded_rectangle((cx, y, cx + w, y + 44), 22, outline=AMBER, width=2)
        draw.text((cx + 16, y + 9), text, font=chip, fill=AMBER)
        cx += w + 12
    y += 64
    draw.text((x, y), f"locator: {label['locator']}", font=font(19), fill=MUTED)
    draw.text((56, 620), "wolfsdk.com/examples/hidden-text", font=font(19, "SemiBold"), fill=INK)
    draw.text(
        (1144, 620),
        "The labels are evidence. The text output stays unchanged.",
        font=font(17),
        fill=MUTED,
        anchor="ra",
    )
    card.save(OUT / "card.png", optimize=False)
    return {"image": "card.png", "width": 1200, "height": 675, "source_format": "docx", "plant": plant["id"]}


def main() -> None:
    if not WOLF:
        raise SystemExit("set WOLF_BIN to the wolf binary")
    plants = json.loads((OUT / "plants.json").read_text())
    version = run([WOLF, "--version"], OUT).stdout.strip()
    poppler = subprocess.run([PDFTOPPM, "-v"], capture_output=True, text=True).stderr.splitlines()[0]
    results: dict = {
        "generated_by": "scripts/examples/hidden-text/run_demo.py",
        "fixtures_by": "scripts/examples/hidden-text/make_fixtures.py",
        "environment": {
            "wolf": version,
            "wolf_sha256": sha256(Path(WOLF)),
            "platform": f"{platform.system()} {platform.machine()}",
            "python": platform.python_version(),
            "markitdown": metadata.version("markitdown"),
            "pillow": metadata.version("pillow"),
            "pdftoppm": poppler,
            "fixtures": plants["environment"],
        },
        "formats": {},
    }
    with tempfile.TemporaryDirectory(prefix="hidden-text-") as tmp:
        for fmt, fixture in plants["fixtures"].items():
            work = Path(tmp) / fmt
            work.mkdir()
            results["formats"][fmt] = measure(fmt, fixture, work)
            print(fmt, results["formats"][fmt]["counts"])
    formats = results["formats"].values()
    results["totals"] = {
        key: sum(f["counts"][key] for f in formats)
        for key in ("planted", "labelled", "unlabelled", "wolf_text_inline", "markitdown_inline", "hidden_channels")
    }
    results["card"] = make_card(results["formats"]["docx"])
    (OUT / "results.json").write_text(json.dumps(results, indent=1, ensure_ascii=False) + "\n")
    for script in ("make_fixtures.py", "run_demo.py"):
        if (HERE / script) != (OUT / script):
            shutil.copy(HERE / script, OUT / script)
    files = sorted(p for p in OUT.iterdir() if p.is_file() and p.name != "SHA256SUMS")
    (OUT / "SHA256SUMS").write_text("".join(f"{sha256(p)}  {p.name}\n" for p in files))
    print("totals", results["totals"])


if __name__ == "__main__":
    main()
