#!/usr/bin/env python3
"""Re-check every value the private-online-photo-gallery table rests on.

Reads dataset.json next to this file. Each result row names one input and the
sentence that supports its value; the script fetches that input today and
reports whether the sentence still appears. Rows marked "Not documented" also
carry notOn, a word the option would use; the script fails if that word appears
on the page, so an absence is checked rather than assumed.

Viallo rows read the repository through `git show origin/main:<path>`. Outside
a Viallo clone they are skipped and reported, never passed.

    python3 check.py            # fetch every input, check every row
    python3 check.py --recheck  # same; there is no lab step to skip

Exit 0 when every checked row holds, 1 otherwise, 2 when dataset.json cannot be
read. Standard library only.
"""

import html
import json
import re
import subprocess
import sys
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
DATASET = HERE / "dataset.json"
TIMEOUT = 45
UA = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36"
)

REPLACE = {
    0x2018: "'",
    0x2019: "'",
    0x201A: "'",
    0x201C: '"',
    0x201D: '"',
    0x201E: '"',
    0x2013: "-",
    0x2014: "-",
    0x2026: "...",
    0x00A0: " ",
    0x200B: "",
    0x00AD: "",
}


def normalise(text):
    """Whitespace, quote marks and entity spacing made uniform on both sides."""
    text = html.unescape(text)
    text = re.sub(r"(?is)<(script|style)\b.*?</\1>", " ", text)
    text = re.sub(r"<[^>]+>", " ", text)
    text = text.translate(REPLACE)
    text = re.sub(r"\s+", " ", text).strip()
    # Tag stripping leaves a space before a comma or period; the stored quotes
    # are written as a reader sees them.
    return re.sub(r"\s+([,.;:!?])", r"\1", text)


def fetch(url):
    # Only these two headers: Apple's CDN answers 404 when Accept or
    # Accept-Encoding is sent, and Google Help varies its body by platform.
    request = urllib.request.Request(
        url, headers={"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"}
    )
    with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
        raw = response.read()
        charset = response.headers.get_content_charset() or "utf-8"
    return raw.decode(charset, errors="replace")


def repo_root():
    result = subprocess.run(
        ["git", "-C", str(HERE), "rev-parse", "--show-toplevel"], capture_output=True
    )
    if result.returncode != 0:
        return None
    root = result.stdout.decode().strip()
    probe = subprocess.run(
        ["git", "-C", root, "cat-file", "-e", "origin/main:packages/shared/src/subscription/tiers.ts"],
        capture_output=True,
    )
    return root if probe.returncode == 0 else None


def text_of_input(dataset_input, root):
    kind = dataset_input["kind"]
    if kind == "url":
        return normalise(fetch(dataset_input["url"]))
    if kind == "zendesk":
        # The rendered help page answers 403 to a script; the same article comes
        # from the help centre's own JSON API, so its body and title are used.
        article = json.loads(fetch(dataset_input["url"]))["article"]
        return normalise(article["title"] + " " + article["body"])
    if kind == "repo":
        if root is None:
            raise RuntimeError("needs a Viallo clone with origin/main")
        shown = subprocess.run(
            ["git", "-C", root, "show", "origin/main:" + dataset_input["path"]],
            capture_output=True,
            text=True,
        )
        if shown.returncode != 0:
            raise RuntimeError(shown.stderr.strip())
        return normalise(shown.stdout)
    raise ValueError("unknown input kind: " + kind)


def main(argv):
    unknown = [arg for arg in argv[1:] if arg != "--recheck"]
    if unknown:
        print("unknown argument: %s" % " ".join(unknown))
        return 1
    try:
        data = json.loads(DATASET.read_text(encoding="utf-8"))
    except OSError as exc:
        print("cannot read dataset.json: %s" % exc)
        return 2

    mode = "recheck" if "--recheck" in argv else "check"
    print("%s: %s, %s, checked %s" % (mode, data["slug"], data["method"], data["checkedAt"]))
    if "--recheck" in argv:
        print("--recheck: no lab step in this dataset, so no step is skipped")

    inputs = {item["id"]: item for item in data["inputs"]}
    root = repo_root()
    texts = {}
    failures = 0
    skipped = 0

    for row in data["results"]:
        row_id = row["id"]
        if not row.get("quote"):
            skipped += 1
            print("skip %-14s %-13s %-16s %s" % (row_id, row["option"], row["property"], row["value"]))
            continue
        try:
            if row["input"] not in texts:
                texts[row["input"]] = text_of_input(inputs[row["input"]], root)
            source = texts[row["input"]]
        except Exception as exc:  # a page that refuses us is a failed row, never a guess
            failures += 1
            print("FAIL %-14s %-13s %-16s input unreadable: %s" % (row_id, row["option"], row["property"], exc))
            continue

        if normalise(row["quote"]) not in source:
            failures += 1
            print("FAIL %-14s %-13s %-16s quote not on %s" % (row_id, row["option"], row["property"], row["input"]))
            print("     quote: %s" % row["quote"])
            continue

        absent_ok = True
        for absence in row.get("notOn", []):
            if absence["input"] not in texts:
                try:
                    texts[absence["input"]] = text_of_input(inputs[absence["input"]], root)
                except Exception as exc:
                    absent_ok = False
                    failures += 1
                    print("FAIL %-14s absence input %s unreadable: %s" % (row_id, absence["input"], exc))
                    continue
            if absence["word"].lower() in texts[absence["input"]].lower():
                absent_ok = False
                failures += 1
                print("FAIL %-14s %-13s %-16s word '%s' now appears on %s"
                      % (row_id, row["option"], row["property"], absence["word"], absence["input"]))

        if absent_ok:
            print("ok   %-14s %-13s %-16s %s" % (row_id, row["option"], row["property"], row["value"]))

    checked = len(data["results"]) - skipped
    print("-" * 72)
    if failures:
        print("%d of %d checked rows failed" % (failures, checked))
        return 1
    print("all %d checked rows hold%s" % (checked, ", %d skipped" % skipped if skipped else ""))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
