"""Regex-based extraction for the deterministic header fields of a surat sprint.

Targets header fields whose layout is highly standardized across Polri units:

  - Nomor sprint, e.g. "Sprin / 123 / IV / 2025 / Reskrim"
  - Tanggal (date the sprint was issued)
  - Satuan penerbit (issuing unit)
  - Perihal
  - Dasar (numbered list of legal/operational basis)

Personnel table extraction is intentionally NOT done here — that needs
PP-Structure + cell-aware logic and lives in `pipeline/table.py` (Phase 3).
"""

from __future__ import annotations

import re
from datetime import date

from ocr_sprint.schemas.extraction import HeaderFields, Signatory

# ---------- regex patterns ----------

# Nomor sprint, tolerant of spacing and OCR noise.
# Examples it should match:
#   "Sprin / 123 / IV / 2025 / Reskrim"
#   "SPRIN/345/X/2024"
#   "Nomor : Sprin/12/I/2025/Sat Intelkam"
_RE_NOMOR_SPRINT = re.compile(
    r"\bSPRIN[\s./-]*\d+[\s./-]*[IVXLCDM]+[\s./-]*\d{2,4}(?:[\s./-]*[\w .-]+?)?",
    re.IGNORECASE,
)

# Indonesian month names.
_BULAN_MAP: dict[str, int] = {
    "JANUARI": 1,
    "FEBRUARI": 2,
    "MARET": 3,
    "APRIL": 4,
    "MEI": 5,
    "JUNI": 6,
    "JULI": 7,
    "AGUSTUS": 8,
    "SEPTEMBER": 9,
    "OKTOBER": 10,
    "NOVEMBER": 11,
    "DESEMBER": 12,
}

# Date in Indonesian, e.g. "21 April 2025" or "21 - April - 2025"
_RE_TANGGAL_ID = re.compile(
    r"\b(\d{1,2})\s*[-./\s]\s*(" + "|".join(_BULAN_MAP.keys()) + r")\s*[-./\s]\s*(\d{4})\b",
    re.IGNORECASE,
)

# Polri letterhead pieces. The full letterhead spans multiple lines that are
# often broken across separate OCR rows like:
#
#     KEPOLISIAN NEGARA REPUBLIK INDONESIA
#     DAERAH JAWA BARAT
#     RESOR CIMAHI
#
# We capture each individual level so we can reconstruct the most-specific
# unit (RESOR / SEKTOR > DAERAH > NEGARA) — a downstream consumer cares
# about *which* unit issued the sprint, not just that some Polri unit did.
_RE_LEVEL_NEGARA = re.compile(
    r"KEPOLISIAN\s+NEGARA\s+REPUBLIK\s+INDONESIA",
    re.IGNORECASE,
)
_RE_LEVEL_DAERAH = re.compile(
    r"(?:KEPOLISIAN\s+)?DAERAH\s+([A-Z][A-Z .'/-]{1,60}?)(?:$|\s*\n)",
    re.IGNORECASE | re.MULTILINE,
)
_RE_LEVEL_RESOR = re.compile(
    r"(?:KEPOLISIAN\s+)?RESORT?\s+([A-Z][A-Z .'/-]{1,60}?)(?:$|\s*\n)",
    re.IGNORECASE | re.MULTILINE,
)
_RE_LEVEL_SEKTOR = re.compile(
    r"(?:KEPOLISIAN\s+)?SEKTOR\s+([A-Z][A-Z .'/-]{1,60}?)(?:$|\s*\n)",
    re.IGNORECASE | re.MULTILINE,
)
_RE_LEVEL_MABES = re.compile(r"MABES\s+POLRI\b", re.IGNORECASE)

# "Perihal : ...." up to end of line.
_RE_PERIHAL = re.compile(r"PERIHAL\s*[:\-]\s*(.+)", re.IGNORECASE)
# Many sprint docs (especially Polres-level) use 'Pertimbangan' as the
# single-paragraph rationale block instead of (or alongside) 'Perihal'.
# When `perihal` is missing we fall back to the first non-empty line under
# 'Pertimbangan :' so the LLM doesn't have to guess and so a downstream
# audit trail still has *something* in the perihal slot.
_RE_PERTIMBANGAN_LABEL = re.compile(r"^\s*PERTIMBANGAN\b", re.IGNORECASE)

# A dasar entry typically begins with a number and dot, e.g. "1. UU No. 2 Tahun 2002 ..."
_RE_DASAR_ITEM = re.compile(r"^\s*(\d+)\s*[.)]\s*(.+)$")
# OCR sometimes splits the number from its content across two lines:
#     1.
#      Undang-Undang Nomor 2 Tahun 2002 ...
# We detect a bare-number line and merge with the next non-empty line.
_RE_DASAR_BARE_NUMBER = re.compile(r"^\s*(\d+)\s*[.)]\s*$")
# Generic 'untuk' bullet — same shape as a dasar item.
_RE_UNTUK_ITEM = re.compile(r"^\s*(\d+)\s*[.)]\s*(.+)$")

# Signatory NRP — Polri NRPs are 8 digits, civil servant NIPs are 18 digits.
_RE_NRP = re.compile(r"\b(NRP|NIP)\s*[.:]?\s*(\d{8,20})\b", re.IGNORECASE)


def find_nomor_sprint(text: str) -> str | None:
    """Return the first nomor sprint found, normalized (no extra spaces)."""
    match = _RE_NOMOR_SPRINT.search(text)
    if not match:
        return None
    return " ".join(match.group(0).split())


def find_tanggal(text: str) -> date | None:
    """Find the issuance date.

    Surat sprint typically contains multiple dates: one or more in the 'Dasar'
    section (citing prior documents) and one near the signatory at the bottom
    (the actual issuance date, usually formatted as 'Tempat, DD Month YYYY').
    We prefer the **last** date in the document since the issuance date appears
    after the dasar items in the standard layout.
    """
    matches = list(_RE_TANGGAL_ID.finditer(text))
    if not matches:
        return None
    last = matches[-1]
    day_s, bulan, year_s = last.group(1), last.group(2).upper(), last.group(3)
    try:
        return date(int(year_s), _BULAN_MAP[bulan], int(day_s))
    except (KeyError, ValueError):
        return None


def _clean_unit_tail(tail: str) -> str:
    """Strip trailing punctuation/noise from the captured place name."""
    return " ".join(tail.split()).strip(" .,;:'\"")


def find_satuan(text: str) -> str | None:
    """Return the issuing unit, preferring the most-specific letterhead level.

    Polri letterheads are hierarchical (Negara > Daerah > Resor/Sektor). The
    actual *issuing* unit is the deepest level present in the letterhead, not
    the topmost generic 'KEPOLISIAN NEGARA REPUBLIK INDONESIA' line. We scan
    for each level independently and pick the most specific one available;
    if only the generic Negara line is present we return that.

    Examples
    --------
    >>> find_satuan("KEPOLISIAN NEGARA REPUBLIK INDONESIA\\n"
    ...             "DAERAH JAWA BARAT\\nRESOR CIMAHI")
    'KEPOLISIAN RESOR CIMAHI'
    >>> find_satuan("KEPOLISIAN NEGARA REPUBLIK INDONESIA")
    'KEPOLISIAN NEGARA REPUBLIK INDONESIA'
    """
    # We only look at the document head — letterheads always sit at the
    # very top, and constraining the search prevents false positives from
    # body text like '... Polres Cimahi ...' deep in a paragraph.
    head = "\n".join(text.splitlines()[:25])

    sektor = _RE_LEVEL_SEKTOR.search(head)
    if sektor:
        return f"KEPOLISIAN SEKTOR {_clean_unit_tail(sektor.group(1))}"
    resor = _RE_LEVEL_RESOR.search(head)
    if resor:
        return f"KEPOLISIAN RESOR {_clean_unit_tail(resor.group(1))}"
    daerah = _RE_LEVEL_DAERAH.search(head)
    if daerah:
        return f"KEPOLISIAN DAERAH {_clean_unit_tail(daerah.group(1))}"
    if _RE_LEVEL_MABES.search(head):
        return "MABES POLRI"
    if _RE_LEVEL_NEGARA.search(head):
        return "KEPOLISIAN NEGARA REPUBLIK INDONESIA"
    return None


def find_perihal(text: str) -> str | None:
    """Return the first 'Perihal: ...' line, trimmed to that line only.

    Falls back to the first non-empty line under a 'Pertimbangan' label
    (a common variant in Polres-level surat sprint that doesn't have a
    distinct 'Perihal' field). We deliberately keep this in regex-land
    rather than deferring to the LLM because the LLM tends to hallucinate
    perihal content from arbitrary paragraphs.
    """
    for line in text.splitlines():
        m = _RE_PERIHAL.search(line)
        if m:
            return m.group(1).strip()

    lines = text.splitlines()
    for idx, line in enumerate(lines):
        if _RE_PERTIMBANGAN_LABEL.match(line):
            for follow in lines[idx + 1 : idx + 5]:
                stripped = follow.strip(" :\t")
                if stripped and stripped != ":":
                    return stripped
            break
    return None


def _collect_numbered_section(
    lines: list[str],
    start_idx: int,
    terminators: tuple[str, ...],
) -> list[str]:
    """Walk forward from ``start_idx`` collecting numbered list items.

    Robust to OCR splitting the number marker onto its own line:
        '1.'  ->   buffer ``pending_index=1``
        next non-empty line starts the item body.

    Continuation lines (non-empty, no leading number, after a started item)
    are appended to the current item. Stops at any line whose uppercase form
    starts with one of ``terminators``.
    """
    items: list[str] = []
    pending_marker = False
    blank_run = 0
    for raw_line in lines[start_idx:]:
        line = raw_line.strip()
        upper = line.upper()
        if any(upper.startswith(term) for term in terminators):
            break
        if not line:
            blank_run += 1
            # Two consecutive blank lines reliably mark the end of a section.
            # A single blank line is tolerated because OCR sprinkles them.
            if blank_run >= 2 and items and not pending_marker:
                break
            continue
        blank_run = 0
        bare = _RE_DASAR_BARE_NUMBER.match(line)
        if bare:
            pending_marker = True
            continue
        m = _RE_DASAR_ITEM.match(line)
        if m:
            items.append(m.group(2).strip())
            pending_marker = False
            continue
        if pending_marker:
            items.append(line)
            pending_marker = False
            continue
        if items:
            items[-1] = (items[-1] + " " + line).strip()
    return items


def find_dasar_list(text: str) -> list[str]:
    """Extract numbered 'Dasar' items from the text.

    Heuristic: locate a line containing 'DASAR' (Indonesian: "DASAR :") and
    delegate to ``_collect_numbered_section`` which handles three OCR
    artefacts:

    1. Inline numbered items: ``"1. Undang-Undang ..."``.
    2. Bare-number lines (the OCR engine puts the number alone on a line):
       ``"1.\\n Undang-Undang ..."``.
    3. Continuation lines (a line that is the wrapped tail of the previous
       item gets appended back onto it).
    """
    lines = text.splitlines()
    section_terminators = ("DIPERINTAHKAN", "UNTUK", "DASAR HUKUM", "PERIHAL")
    for idx, raw_line in enumerate(lines):
        if re.match(r"^\s*DASAR\b", raw_line.strip(), re.IGNORECASE):
            return _collect_numbered_section(lines, idx + 1, section_terminators)
    return []


def find_untuk_list(text: str) -> list[str]:
    """Extract numbered 'Untuk' / 'DIPERINTAHKAN' bullets from the text.

    The 'Untuk' section follows 'DIPERINTAHKAN' / 'Kepada' and lists the
    tasks assigned to the personnel. Same OCR shape as Dasar, so we reuse
    the collector but with different terminators.
    """
    lines = text.splitlines()
    # Stop conditions: 'Selesai' (boilerplate), 'Dikeluarkan di' (signature
    # block), 'Tembusan' (carbon-copy section).
    terminators = ("SELESAI", "DIKELUARKAN", "TEMBUSAN", "PADA TANGGAL")
    for idx, raw_line in enumerate(lines):
        if re.match(r"^\s*UNTUK\b", raw_line.strip(), re.IGNORECASE):
            return _collect_numbered_section(lines, idx + 1, terminators)
    return []


def find_signatory(text: str) -> Signatory:
    """Best-effort extraction of the signatory block (last NRP in the document)."""
    matches = list(_RE_NRP.finditer(text))
    if not matches:
        return Signatory()
    last = matches[-1]
    return Signatory(nrp=last.group(2))


def extract_header(text: str) -> HeaderFields:
    """Run all header-level regex extractors and return a populated schema."""
    return HeaderFields(
        nomor_sprint=find_nomor_sprint(text),
        tanggal=find_tanggal(text),
        satuan_penerbit=find_satuan(text),
        perihal=find_perihal(text),
        dasar=find_dasar_list(text),
    )
