#!/usr/bin/env python3
"""Offline semantic audit of the pinned NDL primary-source capture."""

from __future__ import annotations

import hashlib
import html
import json
from pathlib import Path

HERE = Path(__file__).resolve().parent
PID = "10996926"
BID = "86617e18-d467-432b-a41c-ffaef86a8d29"
ARTICLE_CID = "81a4a6fd-3042-47e1-b790-b89eb209a05c"
TARGET_CID = "a2337306-be89-471c-929b-fb5db315ef08"
TARGET_INDEX = 85

# Query -> exact OCR offset of the intended match in frame/content index 85.
EXPECTED_STARTS = {
    "minimal": 629,
    '"denote by f(t)"': 669,
    '"the number of distances from any points"': 684,
    '"Assume that the distance occurs"': 725,
    '"for largertthe determination"': 772,
    "complicated": 825,
    "really": 848,
    '"But here is a perhaps difficul1"': 866,
    '"Let y1,,large) be any sequence"': 906,
    '"points with di,)1.Is it true"': 940,
    '"hat fornt the number of distances"': 969,
    '"number of distances is less"': 983,
    '"with aquality perhaps only"': 1033,
    '"perhaps only for the triangular lattice"': 1047,
    '"In particular, is it true that"': 1088,
    '"the number f distancesis less"': 1119,
    '"This if true"': 1155,
    '"can surely be proved but we certainly"': 1168,
    '"but we certainly id no"': 1189,
    '"Perhaps the following stronger conjecture"': 1259,
    '"stronger conjecture holds"': 1281,
    '"be the set of distances occurring"': 1315,
    '"set of distances occurring in the triangular lattice"': 1322,
    '"there is an enso that for every"': 1397,
    '"so that for every et"': 1411,
    '"for every et y1,,with"': 1419,
    '"with dith"': 1436,
    '"is less than f"': 1469,
}


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


def assert_metadata() -> None:
    obj = json.loads((HERE / "item_api.json").read_text(encoding="utf-8"))
    item = obj["item"]
    assert item["pid"] == f"info:ndljp/pid/{PID}"
    assert item["itemId"] == PID
    assert item["permission"]["type"] == "ooc"
    assert item["permission"]["search"] == "ALLOW"
    assert item["collections"] == ["A00002"]
    meta = item["meta"]
    assert meta["0001Dtct"] == ["Mathematica Japonica"]
    assert meta["0007Dtct"] == ["46(3)(186);1997・11"]
    assert meta["0058Dod"] == ["1997-11"]
    assert meta["0093Dkck"] == ["0025-5513"]
    assert meta["0114Dkck"] == ["10.11501/10996926"]

    bundles = item["contentsBundles"]
    assert len(bundles) == 1 and bundles[0]["id"] == BID
    bundle = bundles[0]
    article = [
        x for x in bundle["indices"]
        if x["index"].startswith("Some of my favourite unsolved problems/")
    ]
    assert article == [{
        "index": "Some of my favourite unsolved problems/P. ERDOÖS/527～537",
        "contentId": ARTICLE_CID,
        "articleId": None,
        "pageNum": None,
    }]
    article_start = next(i for i, c in enumerate(bundle["contents"]) if c["id"] == ARTICLE_CID)
    assert article_start == 82
    assert bundle["contents"][TARGET_INDEX]["id"] == TARGET_CID
    assert bundle["contents"][TARGET_INDEX]["originalFileName"] == "0086.jp2"

    permission = json.loads(
        (HERE / "snippet_permission_api.json").read_text(encoding="utf-8")
    )
    assert permission == {"value": True}

    # Independent NDL Search (SRU/DC-NDL) metadata: two records (digitized
    # issue and indexed article), exact PID/DOI, date, title, pages and rights.
    sru = html.unescape((HERE / "ndl_search_sru.xml").read_text(encoding="utf-8"))
    required = [
        "<numberOfRecords>2</numberOfRecords>",
        "https://ndlsearch.ndl.go.jp/books/R100000002-I000000118424-d10996926",
        "https://doi.org/10.11501/10996926",
        "46(3)(186);1997・11",
        "Some of my favourite unsolved problems/P. ERDOÖS/527～537",
        "SOME OF MY FAVOURITE UNSOLVED PROBLEMS",
        "1997-11-01",
        "図書館・個人送信対象",
        "https://cir.nii.ac.jp/crid/1570291224709361536",
    ]
    for text in required:
        assert text in sru, text


def reconstruct() -> str:
    capture = json.loads(
        (HERE / "snippet_api_capture.json").read_text(encoding="utf-8")
    )
    assert capture["pid"] == PID
    assert capture["item_url"] == f"https://dl.ndl.go.jp/pid/{PID}"
    records = capture["captures"]
    assert {r["query"] for r in records} == set(EXPECTED_STARTS)

    chars: dict[int, str] = {}
    used = []
    for record in records:
        query = record["query"]
        request = record["request"]
        assert request["targets"] == [{"pid": PID, "bids": [BID]}]
        assert request["mode"] == "SNIPPET"
        result = record["response"][PID]
        candidates = []
        for content in result["contents"]:
            if content["index"] == TARGET_INDEX:
                assert content["cid"] == TARGET_CID
                candidates.extend(content["matches"])
        start = EXPECTED_STARTS[query]
        matches = [m for m in candidates if m["start"] == start]
        assert len(matches) == 1, (query, [(m["start"], m["word"]) for m in candidates])
        match = matches[0]
        assert match["end"] == start + len(match["word"])
        assert match["x"] is None and match["y"] is None

        head = match["head"]
        # The service uses a leading ellipsis only when it splits the fixed
        # context budget around multiple terms. It is presentation, not OCR.
        if head.startswith("…"):
            head = head[1:]
        text = head + match["word"] + match["tail"]
        interval_start = start - len(head)
        for offset, character in enumerate(text, interval_start):
            previous = chars.get(offset)
            assert previous is None or previous == character, (
                query, offset, previous, character
            )
            chars[offset] = character
        used.append((query, interval_start, interval_start + len(text)))

    lo, hi = min(chars), max(chars) + 1
    assert (lo, hi) == (583, 1526)
    missing = [i for i in range(lo, hi) if i not in chars]
    assert not missing, missing[:10]
    reconstruction = "".join(chars[i] for i in range(lo, hi))
    expected = (HERE / "reconstructed_ocr.txt").read_text(encoding="utf-8").rstrip("\n")
    assert reconstruction == expected

    # These exact phrases establish that the maintained prose comes from the
    # primary 1997 article rather than a later conjectural reconstruction.
    phrases = [
        "Consider thetriangular lattice the minimal distance between two points",
        "denote by f(t) the number of distances from any points",
        "Let y1,,large) be any sequence of points with di,)1",
        "the number of distances is less than or equal to f(t)",
        "aquality perhaps only for the triangular lattice?",
        "In particular, is it true that",
        "Perhaps the following stronger conjecture holds",
        "be the set of distances occurring in the triangular lattice",
    ]
    for phrase in phrases:
        assert phrase in reconstruction, phrase
    return reconstruction


def main() -> None:
    assert_metadata()
    reconstruction = reconstruct()
    print("[source-audit] PASS: NDL metadata identifies Er97e and pp.527-537")
    print("[source-audit] PASS: 28 primary OCR snippets overlap without conflict")
    print("[source-audit] reconstructed OCR offsets 583..1525:")
    print(reconstruction)
    for name in [
        "item_api.json",
        "ndl_search_sru.xml",
        "snippet_permission_api.json",
        "snippet_api_capture.json",
        "reconstructed_ocr.txt",
    ]:
        path = HERE / name
        print(f"{sha256(path)}  {name}")


if __name__ == "__main__":
    main()
