#!/usr/bin/env python3
"""Інвентаризація завдань програми Bibletime.

Проходить буклети рівнів 0–4 і витягає кожне завдання. Маркер завдання на
різних рівнях різний, тому беремо об'єднання двох ознак:

* блок `Q:` — рівні 2, 3, 4 (на 3–4 після нього стоїть табуляція);
* речення, що починається наказовим дієсловом — рівні 0 і 1, де маркера
  немає взагалі, а завдання вплетені в текст історії.

Обережно: **Q — це ще й ім'я персонажа** (репліки «No, Q. The Bible tells
us…»), тож маркером вважаємо лише `Q:` з двокрапкою на початку рядка.

    .venv/bin/python ua_build/inventory.py <тека> --json data/inventory_en.json
"""
import argparse
import json
import pathlib
import re
import sys

import fitz

# Наказові дієслова, з яких починаються інструкції. Список виведено з самих
# буклетів (частота перших слів речень), а не придуманий.
VERBS = (
    "Colour", "Color", "Draw", "Write", "Fill", "Circle", "Complete", "Answer",
    "Join", "Use", "Unjumble", "Put", "Look", "Name", "Read", "Make", "Find",
    "Cross", "Underline", "Tick", "Choose", "Match", "Shade", "Copy", "Number",
    "Add", "Solve", "Decode", "Follow", "Trace", "Tell", "Learn", "Say",
    "Give", "List", "Insert", "Rearrange", "Print", "Mark", "Select", "Design",
    "Finish", "Fit", "Discover", "Reveal", "Identify", "Sort", "Count",
    "Rewrite", "Explain", "Describe", "Think", "Check", "Link", "Label",
    "Highlight", "Tidy", "Arrange", "Cut", "Stick", "Start", "Summarise",
)
QUESTION_WORDS = ("What", "Who", "Why", "When", "Where", "Which", "How", "Whose")

Q_MARK = re.compile(r"(?m)^[ \t]*Q:[ \t]*")
HEAD_RE = re.compile(r"(?im)^\s*(STORY|LESSON)\s+(\d+)\s*[:.]?\s*(.*)$")
SCORE_RE = re.compile(r"/\s*(\d{1,3})\b")
TOTAL_RE = re.compile(r"(?i)TOTAL\s*:?\s*/?\s*(\d+)")
VERB_RE = re.compile(r"^(?:%s)\b" % "|".join(VERBS))
QW_RE = re.compile(r"^(?:%s)\b" % "|".join(QUESTION_WORDS))

# Службові рядки, що є в кожному буклеті й завданнями не є.
BOILER = re.compile(
    r"(?i)^(name|age|address|class|marks|total|return|visit|registered|"
    r"read|key verse|bible reading|www\.|bibletime|level \d|"
    r"make sure your name)")

# Текст, адресований не дитині, а дорослому. У рівнях 0–1 він повний
# наказових дієслів («ask the children to…», «use the picture to…») і без
# фільтра дає до чверті хибних «завдань». Тягнеться до кінця блока, але
# **не завжди починає блок**: із 144 випадків лише 53 стоять на початку,
# решту витяг тексту приклеює до вступу історії. Тому ріжемо по маркеру,
# а не відкидаємо блок цілком.
GUIDANCE = re.compile(r"(?i)(adult guidance|teaching notes?|"
                      r"note to (the )?(adult|teacher|parent)|for the leader)")

# «Mark 10: 45» — книга Біблії, а не наказ «познач». Так само Name у посиланні.
BIBLE_REF = re.compile(r"^(Mark|Name)\s+\d")


def sentences(text):
    """Рядки сторінки, зібрані в речення. Переноси рядків — не межі речень."""
    flat = re.sub(r"[ \t]+", " ", text.replace("\n", " "))
    return [s.strip() for s in re.split(r"(?<=[.:?!])\s+", flat) if s.strip()]


def norm(s):
    """Нормалізація для порівняння формулювань між буклетами."""
    s = re.sub(r"\s+", " ", s).strip()
    s = re.sub(r"[’']", "'", s)
    return s


def scan(path):
    doc = fitz.open(path)
    rec = {
        "file": path.name,
        "level": int(path.name[5]),
        "series": path.name[7],
        "num": int(re.search(r"[ABC](\d+)\.pdf$", path.name).group(1)),
        "pages": doc.page_count,
        "headings": [],
        "q_marks": 0,
        "score_boxes": 0,
        "total": None,
        "tasks": [],
    }
    for pno, page in enumerate(doc, start=1):
        text = page.get_text()
        rec["q_marks"] += len(Q_MARK.findall(text))
        rec["score_boxes"] += len(SCORE_RE.findall(text))
        mt = TOTAL_RE.search(text)
        if mt and rec["total"] is None:
            rec["total"] = int(mt.group(1))
        for m in HEAD_RE.finditer(text):
            title = m.group(3).strip()
            if title:
                rec["headings"].append(
                    {"kind": m.group(1).upper(), "n": int(m.group(2)),
                     "title": title[:80]})

        seen = set()
        for block in page.get_text("blocks"):
            body = block[4]
            mg = GUIDANCE.search(body)
            if mg:
                rec["guidance_cut"] = rec.get("guidance_cut", 0) + 1
                body = body[:mg.start()]
            for s in sentences(Q_MARK.sub("", body)):
                if len(s) < 8 or len(s) > 220:
                    continue
                if BOILER.match(s) or BIBLE_REF.match(s):
                    continue
                if not (VERB_RE.match(s) or QW_RE.match(s)):
                    continue
                k = norm(s)
                if k.lower() in seen:
                    continue
                seen.add(k.lower())
                rec["tasks"].append({"page": pno, "text": k})
    doc.close()
    return rec


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("folder")
    ap.add_argument("--json", required=True)
    args = ap.parse_args()

    files = sorted(pathlib.Path(args.folder).glob("*.pdf"))
    if not files:
        sys.exit(f"немає PDF у {args.folder}")

    recs = []
    for f in files:
        try:
            recs.append(scan(f))
        except Exception as exc:                       # noqa: BLE001
            print(f"!! {f.name}: {exc}", file=sys.stderr)

    pathlib.Path(args.json).parent.mkdir(parents=True, exist_ok=True)
    pathlib.Path(args.json).write_text(
        json.dumps(recs, ensure_ascii=False, indent=1), encoding="utf-8")

    print(f"буклетів {len(recs)}   сторінок {sum(r['pages'] for r in recs)}   "
          f"завдань {sum(len(r['tasks']) for r in recs)}")
    print(f"→ {args.json}")


if __name__ == "__main__":
    main()
