"""Post-reframe TOC body composer (#237, per Discussion #235).

After the TOC reframe (Decided ✓ 2026-04-28), the live Project Index
Discussion body has a fixed shape:

  # Project Index — <project name>
  <preamble paragraphs verbatim, e.g. ## Design Record, ## Cross-Repo
  Convention — caller-supplied>

  ## Releases
  <caller-supplied markdown>

  ## Milestones
  <caller-supplied markdown — per-milestone narrative summaries per #235 OQ2>

  ## Discussions
  <caller-supplied markdown table>

  <pointer line — Open Issues → board, Closed Issues → milestone page,
   active scope → planning Discussion>

  ## Cross-Repo Activity
  <caller-supplied markdown table; #235 OQ4 made this a live tracker
   for sibling/cousin/neighbor work without paired authoritative-dev
   Issues>

  ## Project Board
  <caller-supplied URL line>

  ## Repos
  <caller-supplied markdown bullets>

This module defines the canonical layout and concatenates caller-
supplied per-section content. It does not produce section content
itself — that's the caller's job (typically Claude during a `sweep`
or one-shot prune session, gathering data from CHANGELOG.md, GraphQL
milestone reads, etc.).

Pure stdlib, IO-free. Tests cover the layout enforcement; data-
assembly logic stays in the calling session.
"""
from __future__ import annotations

# Canonical pointer line per Discussion #235 §"What the live TOC keeps".
# Substitute `{board_url}` with the project's board URL via str.format.
DEFAULT_POINTER_LINE = (
    "Open Issues — see the [project board]({board_url}). "
    "Closed Issues — see the corresponding milestone page. "
    "For active-scope details and rationale, see the planning "
    "Discussion linked in the Milestones section above."
)


def compose_post_reframe_toc(
    *,
    project_name: str,
    preamble: str = "",
    releases: str = "",
    milestones: str = "",
    discussions: str = "",
    cross_repo_activity: str = "",
    project_board_url: str = "",
    repos: str = "",
    pointer_line: str | None = None,
) -> str:
    """Concatenate per-section markdown content into the canonical
    post-reframe TOC body layout (#235 / #237).

    Sections are emitted in this order:
      1. Title (`# Project Index — <project_name>`)
      2. `preamble` (verbatim — typically ## Design Record + ## Cross-Repo Convention)
      3. ## Releases
      4. ## Milestones
      5. ## Discussions
      6. Pointer line (default per #235, or caller-supplied via `pointer_line`)
      7. ## Cross-Repo Activity
      8. ## Project Board (single URL line)
      9. ## Repos

    Empty section bodies are still emitted with their `## Heading` so
    the structure is consistent across projects of varying maturity —
    a project with no Cross-Repo Activity yet still has the section
    header so future entries land in the right place.

    Args:
      project_name: Canonical short name for the title line.
      preamble: Markdown content between title and ## Releases. Empty
        string is fine. Caller composes; this module does not.
      releases: Markdown body for ## Releases. Empty fine.
      milestones: Markdown body for ## Milestones. Per #235 OQ2, the
        caller composes per-milestone narrative summaries here; this
        module just places the section.
      discussions: Markdown body for ## Discussions (typically a
        table). Empty fine.
      cross_repo_activity: Markdown body for ## Cross-Repo Activity.
        Per #235 OQ4 this is a live tracker; empty when no entries.
      project_board_url: URL emitted under ## Project Board. Also
        substituted into the default pointer line (`{board_url}`).
      repos: Markdown bullets for ## Repos. Empty fine.
      pointer_line: If provided, used verbatim. If None (default),
        DEFAULT_POINTER_LINE is rendered with `project_board_url`
        substituted in.

    Returns:
      The composed markdown body as a single string. Trailing newline
      included for diff-cleanliness.
    """
    if not project_name.strip():
        raise ValueError("project_name must not be empty.")

    if pointer_line is None:
        pointer_line_rendered = DEFAULT_POINTER_LINE.format(
            board_url=project_board_url or "[project board URL]"
        )
    else:
        pointer_line_rendered = pointer_line

    parts: list[str] = [f"# Project Index — {project_name}", ""]

    if preamble.strip():
        parts.extend([preamble.rstrip(), ""])

    parts.extend(["## Releases", "", releases.rstrip() if releases else "", ""])
    parts.extend(["## Milestones", "", milestones.rstrip() if milestones else "", ""])
    parts.extend(["## Discussions", "", discussions.rstrip() if discussions else "", ""])

    # Pointer line goes between Discussions and Cross-Repo Activity per #235's
    # "What the live TOC keeps" §Issues bullet (replacement for the dropped
    # ## Issues table).
    parts.extend([pointer_line_rendered, ""])

    parts.extend([
        "## Cross-Repo Activity",
        "",
        cross_repo_activity.rstrip() if cross_repo_activity else "",
        "",
    ])

    parts.extend([
        "## Project Board",
        "",
        project_board_url or "",
        "",
    ])

    parts.extend(["## Repos", "", repos.rstrip() if repos else "", ""])

    # Collapse runs of blank lines to at most one (clean output even
    # when sections are empty), then ensure a single trailing newline.
    body = "\n".join(parts)
    while "\n\n\n" in body:
        body = body.replace("\n\n\n", "\n\n")
    return body.rstrip() + "\n"


def compose_milestone_narrative(
    *,
    version: str,
    url: str,
    status: str,
    summary: str,
) -> str:
    """Compose a single milestone bullet for ## Milestones (#235 OQ2).

    Per OQ2 resolution: narrative format for both shipped and active
    milestones, consistent with the TOC's existing convention. The
    caller decides whether to include numeric stats (open/closed
    counts, completion %) — those are dynamic and live on the
    milestone page itself.

    Args:
      version: e.g. "2.3.0".
      url: GitHub milestone URL.
      status: e.g. "shipped 2026-04-28", "planned", "active". Free-form.
      summary: Free-form narrative body — themes, Issue refs, scope
        notes. Multi-line allowed.

    Returns:
      A single milestone bullet ready to embed in the ## Milestones
      section markdown. Format:

        - **[<version>](<url>)** (<status>) — <summary>
    """
    if not version.strip():
        raise ValueError("version must not be empty.")
    if not url.strip():
        raise ValueError("url must not be empty.")
    return f"- **[{version}]({url})** ({status}) — {summary.strip()}"


def compose_discussions_row(
    *,
    title: str,
    url: str,
    category: str,
    status: str,
    linked_issues: str = "—",
) -> str:
    """Compose a single ## Discussions table row (#235).

    The Discussions table stays in the live TOC post-reframe (per #235
    "What the live TOC keeps") — Discussions don't have a board-
    equivalent surface that filters/groups them.

    Args:
      title: Discussion title (link text).
      url: Discussion URL.
      category: e.g. "Architecture", "Ideas", "Announcements".
      status: e.g. "Active", "Decided ✓", "Parked".
      linked_issues: Comma-separated linked Issue refs, or "—" if none.

    Returns:
      A single Markdown table row including leading/trailing pipes.
    """
    if not title.strip():
        raise ValueError("title must not be empty.")
    if not url.strip():
        raise ValueError("url must not be empty.")
    return f"| [{title}]({url}) | {category} | {status} | {linked_issues} |"


def compose_discussions_table(rows: list[str]) -> str:
    """Wrap discussions rows with the canonical header (#235).

    Returns:
      The full table markdown including header + separator + rows.
      Empty rows list yields just the header (consistent structure
      even on bootstrapped projects with no Discussions yet).
    """
    header = "| Title | Category | Status | Linked Issues |"
    separator = "|-------|----------|--------|---------------|"
    if not rows:
        return f"{header}\n{separator}"
    return "\n".join([header, separator, *rows])
