"""Rows and links for the ids a report cites but does not define.

The human HTML renders from ``data.json``, and every id the record defines
gets an anchor there. Two id families are cited on almost every page and
defined on none of them:

* the brief's end-state ids (``EB-001``, ``PB-001``, ``EO-001``) — the
  denominator of every requirement-coverage table. The sentence behind each
  id exists only in the task brief; no phase repeats it.
* the clarification ids a previous run settled (``C-005``) — the record points
  at that run through ``clarificationCarryIn.sourceFile`` and cites the ids
  in its prose, but only the lead's ``carriedDecisions`` put a row for them
  in this record, and a run that carried none leaves the ids as dead text.

Both homes are okstra-owned files inside the same task directory, so the
renderer follows the two pointers the run pinned — ``taskBriefPath`` is
always ``<task>/instruction-set/task-brief.md`` (``path_hints.py``), and the
carry-in record is named by the report itself — and only to give a cited id
a place to land. Neither read is a precondition: a missing or unreadable file
yields no rows and no links, never a render failure.
"""
from __future__ import annotations

import os
import re
from collections.abc import Container
from pathlib import Path

from ..final_report_paths import final_report_data_path
from ..json_boundary import JsonBoundaryError, load_owned_object
from ..report_view_artifacts import html_view_path
from ..scope_provenance import brief_end_state_rows

_USER_RESPONSE_RE = re.compile(r"^user-response-(?P<task_type>.+)-(?P<seq>\d{3,})\.md$")


def _task_dir(data_path: Path) -> Path | None:
    """The task directory a report lives under, or None outside the layout.

    A report sits at ``<task>/runs/<type>/reports/`` — or one level deeper
    for an implementation stage, ``runs/implementation/stage-<N>/reports/`` —
    so the nearest ``runs`` ancestor names the task directory either way.
    """
    for parent in data_path.resolve().parents:
        if parent.name == "runs":
            return parent.parent
    return None


def _project_root(data_path: Path) -> Path | None:
    """The project root, or None when the report is not under ``.okstra/``.

    Every okstra-owned artifact lives under ``<PROJECT_ROOT>/.okstra/``, and a
    carry-in ``sourceFile`` is recorded relative to that root.
    """
    for parent in data_path.resolve().parents:
        if parent.name == ".okstra":
            return parent.parent
    return None


def brief_end_states(data_path: Path) -> list[dict[str, str]]:
    """The brief's end-state rows, in brief order, as template-ready dicts."""
    task_dir = _task_dir(data_path)
    if task_dir is None:
        return []
    brief = task_dir / "instruction-set" / "task-brief.md"
    return [
        {"id": row.id, "section": row.section, "statement": row.statement}
        for row in brief_end_state_rows(brief)
    ]


def _carry_in_record(source: Path) -> Path | None:
    """The report record a carry-in pointer resolves to.

    The pointer names either the prior run's record itself or the
    user-responses sidecar exported from that run's page; the sidecar sits in
    ``runs/<type>/user-responses/`` beside the run's ``reports/`` directory
    and carries the run's task type and seq in its name.
    """
    if source.name.endswith(".data.json"):
        return source
    if source.name.startswith("final-report-") and source.name.endswith(".md"):
        # 승인 계획은 열람본(`.md`)으로 가리킨다; 레코드는 그 형제다.
        return final_report_data_path(source)
    match = _USER_RESPONSE_RE.match(source.name)
    if match is None or source.parent.name != "user-responses":
        return None
    task_type, seq = match.group("task_type"), match.group("seq")
    return source.parent.parent / "reports" / f"final-report-{task_type}-{seq}.data.json"


def _clarification_ids(record: Path) -> list[str]:
    """The clarification ids the carry-in record defines, or none.

    The record is read through the owned-JSON boundary like every report
    record; a record that fails it is a record this page cannot link into,
    not a reason to refuse this page.
    """
    try:
        payload = load_owned_object(record, artifact="carry-in report record")
    except JsonBoundaryError:
        return []
    rows = payload.get("clarificationItems")
    if not isinstance(rows, list):
        return []
    return [
        row["id"]
        for row in rows
        if isinstance(row, dict) and isinstance(row.get("id"), str) and row["id"]
    ]


def carry_in_links(
    data: dict, data_path: Path, *, exclude: Container[str] = ()
) -> dict[str, str]:
    """Map each clarification id the carry-in record defines to its anchor
    on that record's HTML page, as an href relative to this report's page.

    ``exclude`` names the ids this document already anchors — a row the lead
    did carry keeps its in-page link, and the prior run's page is only for
    the ids this page has no row for.
    """
    carry_in = data.get("clarificationCarryIn")
    source_value = carry_in.get("sourceFile") if isinstance(carry_in, dict) else None
    if not isinstance(source_value, str) or not source_value.strip():
        return {}
    root = _project_root(data_path)
    if root is None:
        return {}
    record = _carry_in_record(root / source_value.strip())
    if record is None or not record.is_file():
        return {}
    page = html_view_path(record)
    href = Path(os.path.relpath(page, data_path.resolve().parent)).as_posix()
    return {
        cid: f"{href}#id-{cid}"
        for cid in _clarification_ids(record)
        if cid not in exclude
    }


# The brief headings `scope_provenance.brief_end_state_rows` records as `section`;
# a coverage-only row gets the heading its id family belongs to.
_END_STATE_SECTIONS = {"EB": "Expected Behavior", "PB": "Preserved Behavior", "EO": "Expected Outcome"}


def end_state_table(brief_rows: list[dict[str, str]], data: dict) -> list[dict[str, object]]:
    """The end-state section's rows: the brief's statements joined with this
    run's `endStateCoverage` verdicts.

    Brief order first, then ids the coverage table judges that the brief (or a
    task with no brief file) did not list, so every `EB`/`PB`/`EO` the record
    cites has a row here whether or not the brief was readable. A row without
    a brief statement carries the coverage rationale as its text.
    """
    coverage: dict[str, dict] = {}
    for row in data.get("endStateCoverage") or []:
        if isinstance(row, dict) and isinstance(row.get("id"), str) and row["id"]:
            coverage.setdefault(row["id"], row)
    out: list[dict[str, object]] = []
    seen: set[str] = set()
    for row in brief_rows:
        seen.add(row["id"])
        out.append({**row, "coverage": coverage.get(row["id"])})
    for row_id, row in coverage.items():
        if row_id in seen:
            continue
        out.append({
            "id": row_id,
            "section": _END_STATE_SECTIONS.get(row_id.split("-")[0], ""),
            "statement": "",
            "coverage": row,
        })
    return out


def selected_direction_links(
    data: dict, data_path: Path, *, exclude: Container[str] = ()
) -> dict[str, str]:
    """Map the planning report's selected direction id to its card on the
    option-selection page it came from.

    A plan built with `--selected-direction` names that report in
    `implementationPlanning.selectedDirectionRef.sourceReport` (task-relative)
    and cites the direction as `IO-NNN` throughout — four bare mentions on
    the 2026-09-05 dev-10626 planning page. The link exists only when the
    source record is on disk; a plan whose source moved keeps the id as text.
    """
    reference = (data.get("implementationPlanning") or {}).get("selectedDirectionRef")
    if not isinstance(reference, dict):
        return {}
    option_id = reference.get("optionId")
    source = reference.get("sourceReport")
    if not (isinstance(option_id, str) and option_id and isinstance(source, str) and source.strip()):
        return {}
    if option_id in exclude:
        return {}
    task_dir = _task_dir(data_path)
    if task_dir is None:
        return {}
    # `sourceReport` names the reading copy (`.md`); the record is its sibling.
    pointer = task_dir / source.strip()
    record = pointer if pointer.name.endswith(".data.json") else final_report_data_path(pointer)
    if not record.is_file():
        return {}
    page = html_view_path(record)
    href = Path(os.path.relpath(page, data_path.resolve().parent)).as_posix()
    return {option_id: f"{href}#id-{option_id}"}


def approved_plan_links(
    data: dict, data_path: Path, *, exclude: Container[str] = ()
) -> dict[str, str]:
    """Map the ids of the approved plan an implementation report executed to
    their anchors on that plan's page.

    An implementation report cites the plan's checklist rows (`VC-NNN`), its
    invariants (`PI-NNN`) and its steps throughout — the 2026-09-05 dev-10626
    stage-1 page had eight such bare mentions — while defining none of them.
    `implementation.approvedPlanReference.planFile` names the plan (project-
    or task-relative, reading copy or record); when that record exists, every
    id it defines that this page does not links to the plan's page.
    """
    reference = (data.get("implementation") or {}).get("approvedPlanReference")
    plan_file = reference.get("planFile") if isinstance(reference, dict) else None
    if not isinstance(plan_file, str) or not plan_file.strip():
        return {}
    pointer = Path(plan_file.strip())
    if not pointer.name.endswith(".data.json"):
        pointer = final_report_data_path(pointer)
    task_dir = _task_dir(data_path)
    project_root = _project_root(data_path)
    record = next(
        (
            root / pointer
            for root in (project_root, task_dir)
            if root is not None and (root / pointer).is_file()
        ),
        None,
    )
    if record is None:
        return {}
    try:
        plan = load_owned_object(record, artifact="approved plan record")
    except JsonBoundaryError:
        return {}
    from .common import anchor_index
    from .view_models.implementation_planning import ANCHORED_FIELDS

    page = html_view_path(record)
    href = Path(os.path.relpath(page, data_path.resolve().parent)).as_posix()
    return {
        row_id: f"{href}#{anchor}"
        for row_id, anchor in anchor_index(plan, ANCHORED_FIELDS).items()
        if row_id not in exclude
    }
