"""schema-v1 의 `## 1. Clarification Items` 표를 읽는다.

행 하나가 `ClarificationItem` 이 되고, 그 표를 찾고 걷고 메타 셀을 푸는 것이
여기 전부다. 판정(무엇이 진행을 막는가)은 `.dispositions`, 파일을 여는 것은
`.rows` 다.
"""
from __future__ import annotations

from ..md_table import is_separator_row, split_pipe_row, to_cell_text
from dataclasses import dataclass
from typing import Optional
import re


SECTION_HEADING_PATTERN = re.compile(
    r'^##\s+1\.\s+Clarification Items\s*(?:<a id="[^"]+"></a>\s*)?$',
    re.MULTILINE,
)


NEXT_TOP_LEVEL_HEADING_PATTERN = re.compile(r"^##\s+(?!1\.)", re.MULTILINE)


@dataclass(frozen=True)
class ClarificationItem:
    """One row of the §1 table.

    ``raw_*`` fields preserve the exact cell text (after backtick stripping)
    for diagnostics; canonical lowercased versions live in ``blocks`` /
    ``status`` for predicate use.
    """
    row_id: str
    kind: str  # "material" | "decision" | "data-point" | other
    blocks: str  # canonical lowercase: "approval" | "next-phase" | "none" | other
    status: str  # canonical lowercase: "open" | "answered" | "resolved" | "obsolete" | other
    raw_blocks: str
    raw_status: str


_CELL_ANCHOR_RE = re.compile(r'<a id="([^"]*)"></a>')


def _strip_backticks(cell: str) -> str:
    s = _CELL_ANCHOR_RE.sub("", cell.strip()).strip()
    if s.startswith("`") and s.endswith("`") and len(s) >= 2:
        s = s[1:-1].strip()
    return s


def _split_pipe_row(line: str) -> list[str]:
    """``md_table.split_pipe_row`` + §1-specific cell normalization
    (scroll-anchor removal, outer-backtick unwrap)."""
    return [_strip_backticks(c) for c in split_pipe_row(line)]


def _section_1_slice(report_text: str) -> Optional[str]:
    """Return the substring spanning the §1 section (heading exclusive of the
    next ``##`` heading), or None if §1 is absent."""
    start_match = SECTION_HEADING_PATTERN.search(report_text)
    if not start_match:
        return None
    rest = report_text[start_match.end():]
    end_match = NEXT_TOP_LEVEL_HEADING_PATTERN.search(rest)
    return rest[: end_match.start()] if end_match else rest


_META_ID_RE = re.compile(r"([A-Za-z][A-Za-z0-9]*-\d+)")


def _meta_field(cell: str, key: str) -> str:
    """Extract a ``<key>: value`` field from a stacked §1 meta cell.

    The §1 table collapses the short columns into one ``<br>``-delimited
    metadata cell (``**C-101**<br>Ticket: `DEV-1`<br>Kind: `decision`<br>
    Blocks: `approval`<br>Status: open``). Values may be backtick-wrapped.
    """
    m = re.search(
        rf"{re.escape(key)}:\s*`?\s*([^`<|]+?)\s*`?\s*(?:<br\s*/?>|$)",
        cell,
        re.IGNORECASE,
    )
    return m.group(1).strip() if m else ""


def _meta_id(cell: str) -> str:
    """The bold headline ID of a §1 meta cell (the part before the first <br>)."""
    headline = re.split(r"<br\s*/?>", cell, maxsplit=1)[0]
    m = _META_ID_RE.search(headline)
    return m.group(1) if m else ""


def parse_meta_cell(cell: str) -> Optional[ClarificationItem]:
    """Parse one §1 stacked meta cell into a ``ClarificationItem``, or ``None``
    when the cell is not a §1 meta cell (no ``Blocks:``/``Status:`` markers —
    e.g. a header or an unrelated table). Shared by the approval-gate parser
    and the HTML view's form-attach pass so both read the cell identically.
    """
    raw_blocks = _meta_field(cell, "Blocks")
    raw_status = _meta_field(cell, "Status")
    if not (raw_blocks and raw_status):
        return None
    return ClarificationItem(
        row_id=_meta_id(cell),
        kind=_meta_field(cell, "Kind").lower(),
        blocks=raw_blocks.lower(),
        status=raw_status.lower(),
        raw_blocks=raw_blocks,
        raw_status=raw_status,
    )


@dataclass(frozen=True)
class _Section1Table:
    """Outcome of walking the §1 slice for its data table.

    ``items`` is ``None`` when no recognizable table header exists among the
    pipe lines. ``unparsed_row_count`` counts body rows whose metadata cell
    failed ``parse_meta_cell`` (all-empty filler rows excluded).
    ``has_pipe_lines`` distinguishes the renderer's legitimate table-less
    placeholder (emptyState bullet) from a table whose header drifted.
    """
    items: Optional[list[ClarificationItem]]
    unparsed_row_count: int
    has_pipe_lines: bool


def _walk_section_1_table(section: str) -> _Section1Table:
    lines = section.splitlines()
    has_pipe_lines = any(line.lstrip().startswith("|") for line in lines)
    # Locate the §1 data table by its header. The merged-meta layout collapses
    # ID/Ticket/Kind/Blocks/Status into one metadata cell and keeps the
    # English `Statement` + `User input` columns; detect on those two (any
    # other table — intro, legacy 5.1/5.2 — is rejected).
    header_idx = -1
    for idx, line in enumerate(lines):
        if not line.lstrip().startswith("|"):
            continue
        cells = [c.lower() for c in _split_pipe_row(line)]
        if "user input" in cells and any(c.startswith("statement") for c in cells):
            header_idx = idx
            break
    if header_idx < 0:
        return _Section1Table(None, 0, has_pipe_lines)

    items: list[ClarificationItem] = []
    unparsed = 0
    body_started = False
    for line in lines[header_idx + 1:]:
        if not line.lstrip().startswith("|"):
            if body_started:
                break
            continue
        if is_separator_row(line):
            body_started = True
            continue
        if not body_started:
            continue
        cells = _split_pipe_row(line)
        if not any(cells):
            continue
        item = parse_meta_cell(cells[0])
        if item is None:
            unparsed += 1
            continue
        items.append(item)
    return _Section1Table(items, unparsed, True)


def parse_clarification_items(report_text: str) -> Optional[list[ClarificationItem]]:
    """Return the list of §1 rows. ``None`` means "no §1 meta table detected"
    (missing section or unrecognized table header) — caller must NOT treat
    that as "table is empty".

    Lenient view-renderer contract: rows whose metadata cell fails to parse
    are skipped, not surfaced. The approval gate must use
    ``scan_approval_gate`` instead, which fail-closes on those rows.
    """
    section = _section_1_slice(report_text)
    if section is None:
        return None
    return _walk_section_1_table(section).items


def parse_section_1_rows(report_text: str) -> list[dict]:
    """§1 테이블 한 행마다 메타(ClarificationItem) + 원문 Statement / Expected
    form 셀.

    입력은 §1 을 담은 마크다운 본문이다 — 리포트 전문일 수도 있고, 다음 run 에
    첨부되는 carry-in 본문(``clarification_response_with_sidecars``)일 수도
    있다. §1 셀 추출의 단일 참조점 — 다른 모듈이 §1 레이아웃을 다시 훑지 않도록
    이 함수로 통일한다.
    """
    section = _section_1_slice(report_text)
    if section is None:
        return []
    lines = section.splitlines()
    header_idx = -1
    for idx, line in enumerate(lines):
        if not line.lstrip().startswith("|"):
            continue
        cells = [c.lower() for c in _split_pipe_row(line)]
        if "user input" in cells and any(c.startswith("statement") for c in cells):
            header_idx = idx
            break
    if header_idx < 0:
        return []
    header = [c.lower() for c in _split_pipe_row(lines[header_idx])]
    s_col = next((j for j, h in enumerate(header) if h.startswith("statement")), -1)
    e_col = next((j for j, h in enumerate(header) if h.startswith("expected form")), -1)
    rows: list[dict] = []
    body = False
    for line in lines[header_idx + 1:]:
        if not line.lstrip().startswith("|"):
            if body:
                break
            continue
        if is_separator_row(line):
            body = True
            continue
        if not body:
            continue
        cells = _split_pipe_row(line)
        item = parse_meta_cell(cells[0]) if cells else None
        if item is None:
            continue
        rows.append({
            "item": item,
            "statement": cells[s_col] if 0 <= s_col < len(cells) else "",
            "expected_form": cells[e_col] if 0 <= e_col < len(cells) else "",
            # v1 cells are strings; the structured options live only in v2's
            # data sibling. The key stays so callers never branch on schema.
            "options": [],
        })
    return rows


_LOOSE_SECTION_1_RE = re.compile(r"^##\s+1\.\s+Clarification Items\b", re.MULTILINE)


def section_1_present_but_unparsed(report_text: str) -> bool:
    """§1 헤딩이 느슨 탐지엔 잡히지만 엄격 SECTION_HEADING_PATTERN 에는 매칭하지
    못하는 경우 True — 헤딩 형태가 어긋나(앵커·포맷 drift) §1 슬라이스 자체가
    실패하는 상태다.

    이때 ``_section_1_slice`` 가 None 을 반환해 parse 가 통째로 None 이 되고 승인
    게이트가 "schema 없음 → soft-pass" 로 조용히 열린다. §1 앵커 버그가 정확히 이
    메커니즘으로 터졌다. 헤딩 자체가 없는 legacy 리포트(둘 다 불매칭)와, 엄격
    매칭에 성공하는 정상 헤딩(테이블이 없는 emptyState placeholder 포함)은 False —
    placeholder 는 헤딩이 멀쩡하므로 fail-closed 로 오인하지 않는다. 정규식만 넓혀 온
    과거 수정과 달리, 이 판별은 "헤딩 형태 drift" 자체를 차단해 재발 클래스를 닫는다."""
    if SECTION_HEADING_PATTERN.search(report_text):
        return False
    return bool(_LOOSE_SECTION_1_RE.search(report_text))
