#!/usr/bin/env python3
"""Parse the InDesign HTML export of *A la fuente cada día*.

The output is deliberately source-only: it contains no civil-date mapping.
That mapping belongs in the separate, reviewable liturgical calendar JSON.
"""

from __future__ import annotations

import argparse
import html
import json
import re
from pathlib import Path


TITLE_SOLEMNITY = "V-T-tulo-2-l-nea"
TITLE_OR_CITATION = "V-T-tulo-L-a-S"
GOSPEL = "Sangr-a-2-de-t--independiente"
MOTTO = "V-P-rrafo-2"
PARAGRAPH = "V-P-rrafo"
CITATION_RE = re.compile(r"\((?:Mt|Mc|Lc|Jn)\b[^)]*\)", re.IGNORECASE)
MANUAL_CITATIONS = {
    # Three malformed/omitted references in the InDesign export, resolved from
    # the printed Gospel text in the same source entry.
    "Miércoles de la 32ª semana Lc 17,11-19)": "Lc 17,11-19",
    "22 de Julio": "Jn 20,1-2.11-18",
    "2 de noviembre": "Jn 11,21-44",
    "DOMINGO IV DE ADVIENTO (C)": "Lc 1,39-45",
    "Sábado de la 7ª semana (Mc 10 13-16)": "Mc 10,13-16",
    "8 de diciembre": "Lc 1,26-38",
    "1 de noviembre": "Mt 5,1-12a",
    "DOMINGO DE RAMOS (A)": "Mt 26,14-27,66",
    "DOMINGO DE RAMOS (B)": "Mc 14,1-15,47",
    "DOMINGO DE RAMOS (C)": "Lc 22,14-23,56",
    "VIERNES SANTO": "Jn 18,1-19,42",
    "DOMINGO IV TIEMPO ORDINARIO (A)": "Mt 5,1-12a",
}


def text(fragment: str) -> str:
    """Return normalized text from the small, well-formed HTML fragments."""
    fragment = re.sub(r"<[^>]+>", "", fragment)
    return re.sub(r"\s+", " ", html.unescape(fragment).replace("\xa0", " ")).strip()


def css_base(class_name: str) -> str:
    return class_name.split(" ", 1)[0]


def is_citation(value: str) -> bool:
    return bool(CITATION_RE.fullmatch(value.strip()))


def clean_citation(value: str) -> str:
    match = CITATION_RE.search(value)
    return (match.group(0) if match else value).strip().removeprefix("(").removesuffix(")").strip()


def new_entry(title: str, kind: str) -> dict[str, object]:
    return {
        "source_index": 0,
        "kind": kind,
        "title": title,
        "citation": "",
        "gospel": "",
        "motto": "",
        "paragraphs": [],
        "editorial_notes": [],
    }


def set_citation_from(value: str, entry: dict[str, object]) -> None:
    """Capture a reference embedded in a weekday or feast-title line."""
    if entry["citation"]:
        return
    match = CITATION_RE.search(value)
    if match:
        entry["citation"] = clean_citation(match.group(0))
    elif "pasión según" in value.lower():
        entry["citation"] = value.strip().removeprefix("(").removesuffix(")")


def parse(source: Path) -> list[dict[str, object]]:
    raw = source.read_text(encoding="utf-8")
    blocks = re.findall(r'<p class="([^"]+)"[^>]*>(.*?)</p>', raw, re.S)
    entries: list[dict[str, object]] = []
    current: dict[str, object] | None = None

    def finish() -> None:
        nonlocal current
        if current is not None:
            current["source_index"] = len(entries) + 1
            entries.append(current)
        current = None

    for class_name, fragment in blocks:
        role, value = css_base(class_name), text(fragment)
        if not value:
            continue

        if role == TITLE_SOLEMNITY:
            finish()
            current = new_entry(value, "solemnity")
            set_citation_from(value, current)
            continue

        if role == TITLE_OR_CITATION and not is_citation(value):
            # This class is overloaded. After an already complete entry it is
            # the next weekday title (and also the Holy Family title). Right
            # after a large title it is instead a rubric such as "INMACULADA"
            # or "Pasión según Mt", so it must remain part of that entry.
            if current is None or current["motto"] or current["paragraphs"]:
                finish()
                current = new_entry(value, "weekday")
                set_citation_from(value, current)
                continue

        if current is None:
            continue

        if role == TITLE_OR_CITATION and is_citation(value):
            current["citation"] = clean_citation(value)
        elif role == TITLE_OR_CITATION:
            set_citation_from(value, current)
        elif role == GOSPEL:
            current["gospel"] = value
        elif role == MOTTO:
            current["motto"] = value
        elif role == PARAGRAPH:
            if not current["gospel"] and not current["motto"] and value.startswith("Si toca alguna semana anterior"):
                current["editorial_notes"].append(value)
                continue
            # One source inconsistency (Easter VI, cycle A) styles the Gospel as
            # a normal paragraph. Before the motto, that position is unambiguous.
            if not current["gospel"] and not current["motto"]:
                current["gospel"] = value
            else:
                current["paragraphs"].append(value)

    finish()
    for entry in entries:
        if entry["title"] in MANUAL_CITATIONS:
            entry["citation"] = MANUAL_CITATIONS[entry["title"]]
    return entries


def audit(entries: list[dict[str, object]]) -> list[str]:
    issues: list[str] = []
    for entry in entries:
        missing = [field for field in ("citation", "gospel", "motto") if not entry[field]]
        if not entry["paragraphs"]:
            missing.append("paragraphs")
        if missing:
            issues.append(f"#{entry['source_index']} {entry['title']}: missing {', '.join(missing)}")
    return issues


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("source", type=Path)
    parser.add_argument("output", type=Path)
    args = parser.parse_args()

    entries = parse(args.source)
    issues = audit(entries)
    # The earlier exploratory JSON had 504 records, but four were artificial
    # citation-only splits (Holy Family and Baptism A/B/C). The source has 500
    # complete, independently publishable comments.
    if len(entries) != 500:
        raise SystemExit(f"expected 500 complete entries; got {len(entries)}")
    if issues:
        raise SystemExit("source audit failed:\n" + "\n".join(issues))

    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(json.dumps(entries, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(f"wrote {len(entries)} complete entries to {args.output}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
