"""게이트를 읽는다 — 아직 열려 있는 것이 무엇인가.

`scan_approval_gate` 와 `scan_open_user_input` 을 run-prep · 위저드 ·
user-response CLI 가 공유하므로 세 곳이 서로 다른 답을 낼 수 없다.
판정은 fail-closed 다: 확신을 갖고 읽지 못하면 `unreadable_reason` 을 남기고
호출자는 soft-pass 대신 승인을 거절해야 한다.
"""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from .parsing import (
    ClarificationItem,
    _LOOSE_SECTION_1_RE,
    _section_1_slice,
    _walk_section_1_table,
)
from .dispositions import (
    APPROVAL_BLOCKS,
    PROCEEDING_DISPOSITIONS,
    USER_INPUT_BLOCKS,
    incorporated_clarification_ids,
    row_blocks_progress,
)
from .rows import (
    _read_report_text,
    _structured_report_data,
    _v2_row,
)
from .sidecars import (
    sidecar_dispositions,
)


@dataclass(frozen=True)
class ClarificationScan:
    """Fail-closed read of the clarification rows blocking on one `Blocks`
    value set.

    ``unreadable_reason`` is ``None`` only when the scan is confident: the
    rows parsed cleanly (or the report is the legitimate table-less
    placeholder) and ``blockers`` is therefore authoritative. A non-None
    reason means the caller must refuse to act — never soft-pass.
    """
    blockers: list[ClarificationItem]
    unreadable_reason: Optional[str]


def scan_approval_gate(report_path: Path) -> ClarificationScan:
    """Scan for ``Blocks=approval`` rows that still block progress.

    A recorded user proceeding disposition (`accept-risk` / `select` /
    `answer`), including one that lives only in the sidecar, does not block.
    ``request-revision`` / ``reject`` still block unless this report's
    ``supersessionLedger`` already incorporated that id. The scan refuses to
    guess whenever the schema drifted.
    """
    return scan_clarification_blockers(
        report_path,
        APPROVAL_BLOCKS,
        honor_sidecar_answers=True,
        sidecar_unblocks_proceeding_only=True,
    )


def scan_open_user_input(report_path: Path) -> ClarificationScan:
    """Scan for every unresolved row that still owes the user an answer
    (``Blocks`` in ``{approval, next-phase}``)."""
    return scan_clarification_blockers(
        report_path, USER_INPUT_BLOCKS, honor_sidecar_answers=True
    )


def scan_clarification_blockers(
    report_path: Path,
    blocking_values: frozenset[str],
    *,
    honor_sidecar_answers: bool,
    sidecar_unblocks_proceeding_only: bool = False,
) -> ClarificationScan:
    """Shared fail-closed clarification walk for both gates above — schema-v2
    reads its rows from the data sibling and schema-v1 from the §1 table.
    ``honor_sidecar_answers`` hides rows the sidecar already answered.
    ``sidecar_unblocks_proceeding_only`` keeps an unincorporated
    ``request-revision`` / ``reject`` as a blocker so a return choice cannot
    start the next phase. A ledger entry for that id means this report already
    absorbed the return.
    """
    v2_scan = _scan_v2_blockers(report_path, blocking_values)
    scan = (
        v2_scan if v2_scan is not None
        else scan_section_1_blockers(_read_report_text(report_path), blocking_values)
    )
    return (
        _resolve_blockers_answered_by_user(
            report_path,
            scan,
            proceeding_only=sidecar_unblocks_proceeding_only,
        )
        if honor_sidecar_answers
        else scan
    )


def _resolve_blockers_answered_by_user(
    report_path: Path,
    scan: ClarificationScan,
    *,
    proceeding_only: bool = False,
) -> ClarificationScan:
    """사용자가 사이드카로 답한 행을 blocker 에서 뺀 스캔.

    답의 정본은 사용자의 `user-responses/` 사이드카다. 리포트의 `Status` 는 그
    run 이 스스로 적어둔 값이고, 답이 사이드카로만 들어오는 경로(HTML 뷰의
    `Export user response`, `okstra user-response write`)에서는 갱신되지 않는다.
    사용자 입력 목록은 사이드카 답변이 있는 항목을 다시 묻지 않는다. 승인
    게이트는 진행 처분만 차단에서 뺀다.

    fail-closed 는 그대로다: 행 자체를 못 읽은 스캔(`unreadable_reason`)은
    어떤 id 가 blocker 인지 모르는 상태이므로 사이드카로 덮지 않는다.
    """
    if scan.unreadable_reason is not None or not scan.blockers:
        return scan
    dispositions = sidecar_dispositions(report_path)
    if not dispositions:
        return scan
    answered_ids = {
        row_id
        for row_id, disposition in dispositions.items()
        if (not proceeding_only) or disposition in PROCEEDING_DISPOSITIONS
    }
    if not answered_ids:
        return scan
    return ClarificationScan(
        [b for b in scan.blockers if b.row_id not in answered_ids], None
    )


def scan_section_1_blockers(
    report_text: str, blocking_values: frozenset[str]
) -> ClarificationScan:
    """Fail-closed §1 walk over any markdown carrying the table — a schema-v1
    report, or the carry-in body a reconciliation just produced."""
    section = _section_1_slice(report_text)
    if section is None:
        if _LOOSE_SECTION_1_RE.search(report_text):
            reason = (
                "`## 1. Clarification Items` heading exists but does not match "
                "the schema heading format (anchor/format drift)"
            )
        else:
            expected = "/".join(sorted(blocking_values))
            reason = (
                "report has no `## 1. Clarification Items` section — the gate "
                f"cannot confirm there are no unresolved `Blocks={expected}` rows"
            )
        return ClarificationScan([], reason)
    table = _walk_section_1_table(section)
    if table.items is None:
        if table.has_pipe_lines:
            return ClarificationScan([], (
                "§1 contains a table but its header row is not the schema "
                "header (`| ... | Statement | Expected form | User input |`)"
            ))
        # Renderer's emptyState placeholder: heading is intact and no table
        # was emitted — confidently "no blocking items".
        return ClarificationScan([], None)
    if table.unparsed_row_count:
        return ClarificationScan([], (
            f"§1 table has {table.unparsed_row_count} row(s) whose metadata "
            "cell could not be parsed (Blocks/Status markers missing or "
            "malformed)"
        ))
    blockers = [
        it for it in table.items
        if it.blocks in blocking_values and row_blocks_progress(it.status)
    ]
    return ClarificationScan(blockers, None)


def _scan_v2_blockers(
    report_path: Path, blocking_values: frozenset[str]
) -> Optional[ClarificationScan]:
    """schema-v2 data.json 기준 스캔, 이 리포트가 v2 가 아니면 ``None``.
    필수 필드가 빠진 행은 §1 의 unparsed row 와 같이 fail-closed 로 다룬다."""
    data = _structured_report_data(report_path)
    if data is None:
        return None
    entries = data.get("clarificationItems")
    if entries is None:
        return ClarificationScan([], None)
    if not isinstance(entries, list):
        return ClarificationScan([], (
            "schema-v2 data.json `clarificationItems` is not an array — the "
            "gate cannot read the clarification rows"
        ))
    rows = [_v2_row(e) if isinstance(e, dict) else None for e in entries]
    unparsed = sum(1 for row in rows if row is None)
    if unparsed:
        return ClarificationScan([], (
            f"schema-v2 data.json has {unparsed} `clarificationItems` row(s) "
            "missing id/blocks/status"
        ))
    incorporated = incorporated_clarification_ids(data)
    blockers = [
        row["item"] for row in rows
        if row["item"].blocks in blocking_values
        and row_blocks_progress(
            row["item"].status,
            str(row.get("disposition") or ""),
            incorporated=row["item"].row_id in incorporated,
        )
    ]
    return ClarificationScan(blockers, None)
