"""MkDocs hook for ADR page custom rendering.

Injects a custom header (dates, status badge, tier) and a hidden timeline
element into every ADR page.  The companion ``adr-page.js`` script moves
the timeline into the left sidebar at runtime.

Also auto-generates the ADR inventory tables on the ``adr/index.md`` page
by scanning all ADR source files for their YAML front-matter.
"""

from __future__ import annotations

import logging
import re
from datetime import date, datetime
from pathlib import Path
from typing import Any

import yaml

log = logging.getLogger("mkdocs.hooks.adr")

# ── status definitions ──────────────────────────────────────────────────

VALID_STATUSES = {
    "draft",
    "proposed",
    "accepted",
    "rejected",
    "superseded",
    "deprecated",
}

STATUS_LABELS = {
    "draft": "Draft",
    "proposed": "Proposed",
    "accepted": "Accepted",
    "rejected": "Rejected",
    "superseded": "Superseded",
    "deprecated": "Deprecated",
}

# ── SVG icons (Material Design, same as log4brains) ────────────────────

ICON_CALENDAR = (
    '<svg class="adr-icon" viewBox="0 0 24 24" aria-hidden="true">'
    '<path d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2'
    "L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2z"
    'm3 18H5V8h14v11z"/></svg>'
)

ICON_PERSON = (
    '<svg class="adr-icon" viewBox="0 0 24 24" aria-hidden="true">'
    '<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 '
    '4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>'
)

# ── regex to match the old inline metadata block ────────────────────────

# Matches lines like:  **Status:** Accepted  \n**Date:** ...  up to the
# blank line before ## Context (or the next heading).
_OLD_META_RE = re.compile(
    r"^(\*\*Status:\*\*.*?\n)"  # first metadata line
    r"((?:\*\*[A-Za-z()]+:\*\*.*?\n)*)"  # subsequent metadata lines
    r"\s*\n",  # trailing blank line
    re.MULTILINE,
)


# ── helpers ─────────────────────────────────────────────────────────────


def _normalise_date(raw: Any) -> str:
    """Return a YYYY-MM-DD string from various input types."""
    if isinstance(raw, date):
        return raw.isoformat()
    if isinstance(raw, datetime):
        return raw.date().isoformat()
    return str(raw).strip()


def _format_date_display(iso: str) -> str:
    """Convert '2026-02-16' to 'Feb 16, 2026'."""
    try:
        d = datetime.strptime(iso, "%Y-%m-%d")
        return d.strftime("%b %d, %Y")
    except (ValueError, TypeError):
        return iso


def _normalise_people(raw: Any) -> str:
    """Return a comma-separated string of people from various input."""
    if raw is None:
        return ""
    if isinstance(raw, list):
        return ", ".join(str(p) for p in raw)
    return str(raw).strip()


def _status_key(status: str) -> str:
    """Lowercase status key for CSS class names."""
    return status.strip().lower()


def _is_adr_page(page: Any) -> bool:
    """Return True if the page is an individual ADR (not the index)."""
    src = page.file.src_path  # e.g. "adr/ADR-001-layered-architecture.md"
    return src.startswith("adr/ADR-") and src.endswith(".md") and "index" not in src


def _is_adr_index(page: Any) -> bool:
    """Return True if the page is the ADR index page."""
    return page.file.src_path == "adr/index.md"


def _extract_adr_number(page: Any) -> str | None:
    """Extract '001' from 'adr/ADR-001-layered-architecture.md'."""
    m = re.search(r"ADR-(\d+)", page.file.src_path)
    return m.group(1) if m else None


# ── HTML builders ───────────────────────────────────────────────────────


def _build_status_badge(status: str, extra_class: str = "") -> str:
    key = _status_key(status)
    label = STATUS_LABELS.get(key, status)
    cls = f"adr-status-badge adr-status-{key}"
    if extra_class:
        cls += f" {extra_class}"
    return f'<span class="{cls}">{label}</span>'


def _resolve_adr_link(adr_number: int, files: Any) -> str | None:
    """Find the relative URL for an ADR by its number."""
    prefix = f"adr/ADR-{adr_number:03d}"
    for f in files:
        if f.src_path.startswith(prefix) and f.src_path.endswith(".md"):
            return f.src_path.replace("adr/", "", 1)  # relative to current adr/ dir
    return None


def _build_header(
    earliest_date: str,
    last_date: str,
    last_status: str,
    tier: int | None,
    tier_title: str,
    superseded_by: int | None,
    authors: str,
    files: Any,
) -> str:
    """Build the subtitle-style header injected after the H1 title.

    Line 1: Tier + Drafted date (earliest entry in status_history)
    Line 2: Current status badge + date  (+ superseded-by link when applicable)
    Line 3: Authors (optional, from explicit front-matter field)
    """
    earliest_display = _format_date_display(earliest_date)
    last_display = _format_date_display(last_date)
    badge = _build_status_badge(last_status)
    status_key = _status_key(last_status)

    # ── line 1: tier + drafted date ─────────────────────────────────
    tier_part = ""
    if tier is not None and tier_title:
        tier_part = (
            f'<span class="adr-tier-badge">Tier {tier}</span>'
            f'<span class="adr-tier-title">{tier_title}</span>'
            f'<span class="adr-label"> · </span>'
        )

    line1 = (
        f'<div class="adr-header-line">'
        f"{tier_part}"
        f'<span class="adr-label">Drafted:</span> '
        f'<span class="adr-value">{earliest_display}</span>'
        f"</div>"
    )

    # ── line 2: current status + date (+ superseded-by link) ────────
    superseded_part = ""
    if superseded_by is not None and status_key == "superseded":
        link = _resolve_adr_link(superseded_by, files)
        if link:
            superseded_part = (
                f' <span class="adr-label">—</span> '
                f'<span class="adr-label">superseded by</span> '
                f'<a href="{link}">ADR-{superseded_by:03d}</a>'
            )
        else:
            superseded_part = (
                f' <span class="adr-label">'
                f"— superseded by ADR-{superseded_by:03d}</span>"
            )

    line2 = (
        f'<div class="adr-header-line">'
        f'<span class="adr-label">Status:</span> '
        f"{badge}"
        f'<span class="adr-label"> as of </span>'
        f'<span class="adr-value">{last_display}</span>'
        f"{superseded_part}"
        f"</div>"
    )

    # ── line 3: authors (optional) ──────────────────────────────────
    line3 = ""
    if authors:
        line3 = (
            f'<div class="adr-header-line">'
            f'<span class="adr-label">Authors:</span> '
            f'<span class="adr-value">{authors}</span>'
            f"</div>"
        )

    return (
        f'\n<div class="adr-header" markdown="0">\n'
        f"  {line1}\n"
        f"  {line2}\n"
        f"  {line3}\n"
        f"</div>\n\n"
    )


def _build_timeline(status_history: list) -> str:
    """Build the hidden timeline HTML that JS moves into the sidebar."""
    entries_html = []
    total = len(status_history)

    for i, entry in enumerate(status_history):
        entry_date = _normalise_date(entry[0])
        entry_status = str(entry[1]).strip()
        entry_people = _normalise_people(entry[2] if len(entry) > 2 else None)
        status_key = _status_key(entry_status)
        date_display = _format_date_display(entry_date)
        is_last = i == total - 1

        dot_cls = f"adr-timeline-dot adr-dot-{status_key}"
        if is_last:
            dot_cls += " adr-dot-current"

        people_html = ""
        if entry_people:
            people_html = (
                f'<div class="adr-timeline-person">{ICON_PERSON}{entry_people}</div>'
            )

        connector_line = '<div class="adr-timeline-line"></div>' if not is_last else ""

        badge = _build_status_badge(entry_status)

        entries_html.append(
            f'<div class="adr-timeline-entry">\n'
            f'  <div class="adr-timeline-date-col">\n'
            f'    <span class="adr-timeline-date">{date_display}</span>\n'
            f"  </div>\n"
            f'  <div class="adr-timeline-connector">\n'
            f'    <div class="{dot_cls}"></div>\n'
            f"    {connector_line}\n"
            f"  </div>\n"
            f'  <div class="adr-timeline-content">\n'
            f"    {badge}\n"
            f"    {people_html}\n"
            f"  </div>\n"
            f"</div>"
        )

    inner = "\n".join(entries_html)
    return (
        f'\n<div class="adr-timeline" style="display:none" markdown="0">\n'
        f"{inner}\n"
        f"</div>\n\n"
    )


# ── Generated tail sections (Related ADRs + Acceptance) ────────────────

# Regex to strip leftover ## Related ADRs / ## Acceptance from the body
# (defensive — the migration should have removed them already).
_RELATED_SECTION_RE = re.compile(r"\n## Related ADRs\s*\n.*", re.DOTALL)
_ACCEPTANCE_SECTION_RE = re.compile(r"\n## Acceptance\s*\n.*", re.DOTALL)


def _build_related_adrs_section(
    related_adrs: list[dict],
    files: Any,
) -> str:
    """Generate the ``## Related ADRs`` markdown section from front-matter.

    Each entry in *related_adrs* has ``number``, ``title``, and
    ``relationship``.  The ADR link is resolved via the MkDocs *files*
    collection so that MkDocs can validate and rewrite the URL.
    """
    if not related_adrs:
        return ""

    lines = [
        "## Related ADRs",
        "",
        "| ADR | Title | Relationship |",
        "|-----|-------|-------------|",
    ]

    for entry in related_adrs:
        num = entry.get("number")
        title = entry.get("title", "")
        relationship = entry.get("relationship", "")

        if num is None:
            continue

        # Resolve the file link via the files collection
        link_path = _resolve_adr_link(int(num), files)
        if link_path:
            adr_col = f"[ADR-{int(num):03d}]({link_path})"
        else:
            adr_col = f"ADR-{int(num):03d}"

        lines.append(f"| {adr_col} | {title} | {relationship} |")

    return "\n".join(lines) + "\n"


def _build_acceptance_section(acceptance: dict) -> str:
    """Generate the ``## Acceptance`` markdown section from front-matter.

    *acceptance* contains ``votes_for``, ``votes_against``, and
    ``abstentions`` — each a list of ``{voter, comment}`` dicts.
    """
    if not acceptance:
        return ""

    def _votes_table(entries: list[dict]) -> str:
        rows = [
            "| Voter | Comment |",
            "|-------|---------|",
        ]
        for e in entries:
            voter = e.get("voter", "")
            comment = e.get("comment", "")
            rows.append(f"| {voter} | {comment} |")
        return "\n".join(rows)

    votes_for = acceptance.get("votes_for", [])
    votes_against = acceptance.get("votes_against", [])
    abstentions = acceptance.get("abstentions", [])

    parts = [
        "## Acceptance",
        "",
        "### Votes For",
        "",
        _votes_table(votes_for),
        "",
        f"**Total: {len(votes_for)}**",
        "",
        "### Votes Against",
        "",
        _votes_table(votes_against),
        "",
        f"**Total: {len(votes_against)}**",
        "",
        "### Abstentions",
        "",
        _votes_table(abstentions),
        "",
        f"**Total: {len(abstentions)}**",
    ]

    return "\n".join(parts) + "\n"


# ── ADR inventory (for the index page) ─────────────────────────────────

_INVENTORY_PLACEHOLDER = "<!-- ADR_INVENTORY -->"
_TIMELINE_PLACEHOLDER = "<!-- ADR_TIMELINE -->"


def _collect_adr_inventory(files: Any) -> list[dict]:
    """Scan all ADR source files and collect front-matter metadata.

    Returns a list of dicts sorted by ``adr_number``, each containing
    ``number``, ``title``, ``tier``, ``status``, ``status_badge`` (HTML),
    ``filename``, and ``status_history`` (the raw list of transitions).
    """
    adrs: list[dict] = []
    for f in files:
        src = f.src_path
        if not src.startswith("adr/ADR-") or not src.endswith(".md"):
            continue

        try:
            content = Path(f.abs_src_path).read_text(encoding="utf-8")
        except OSError:
            continue

        fm_match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
        if not fm_match:
            continue

        try:
            meta = yaml.safe_load(fm_match.group(1)) or {}
        except yaml.YAMLError:
            continue

        adr_number = meta.get("adr_number")
        title = meta.get("title", "")
        tier = meta.get("tier")
        status_history = meta.get("status_history", [])

        if adr_number is None or not status_history:
            continue

        last_status = str(status_history[-1][1]).strip()

        # Relative link from adr/index.md to adr/ADR-NNN-xxx.md
        filename = src.replace("adr/", "", 1)

        adrs.append(
            {
                "number": int(adr_number),
                "title": title,
                "tier": tier,
                "status": last_status,
                "status_badge": _build_status_badge(last_status),
                "filename": filename,
                "status_history": status_history,
            }
        )

    adrs.sort(key=lambda a: a["number"])
    return adrs


def _generate_inventory_markdown(adrs: list[dict], tiers_config: dict) -> str:
    """Generate tier-grouped inventory tables as markdown.

    Each tier produces an ``### Tier N — Title`` sub-heading followed by
    the tier description and a table of ADRs with their number (linked),
    title, and current status rendered as a coloured badge.
    """
    # Group by tier
    grouped: dict[int | None, list[dict]] = {}
    for adr in adrs:
        t = adr.get("tier")
        grouped.setdefault(t, []).append(adr)

    sections: list[str] = []

    def _tier_table(tier_adrs: list[dict]) -> list[str]:
        rows = [
            "| ADR | Title | Status |",
            "|-----|-------|--------|",
        ]
        for adr in tier_adrs:
            num_str = f"ADR-{adr['number']:03d}"
            link = f"[{num_str}]({adr['filename']})"
            rows.append(f"| {link} | {adr['title']} | {adr['status_badge']} |")
        return rows

    # Emit tiers in numeric order
    for tier_num in sorted(t for t in grouped if t is not None):
        tier_info = tiers_config.get(tier_num, {})
        tier_title = (
            tier_info.get("title", f"Tier {tier_num}")
            if isinstance(tier_info, dict)
            else str(tier_info)
        )
        tier_desc = (
            tier_info.get("description", "") if isinstance(tier_info, dict) else ""
        )

        lines: list[str] = [f"### Tier {tier_num} — {tier_title}", ""]
        if tier_desc:
            lines += [tier_desc, ""]
        lines += _tier_table(grouped[tier_num])
        sections.append("\n".join(lines))

    # Handle ADRs without a tier (should not happen, but be defensive)
    if None in grouped:
        lines = ["### Uncategorized", ""]
        lines += _tier_table(grouped[None])
        sections.append("\n".join(lines))

    return "\n\n".join(sections)


# ── SVG icon for ADR link in combined timeline ─────────────────────────

ICON_ADR = (
    '<svg class="adr-icon" viewBox="0 0 24 24" aria-hidden="true">'
    '<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 '
    "2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 "
    '9H13z"/></svg>'
)


def _md_to_url(filename: str) -> str:
    """Convert ``ADR-001-layered-architecture.md`` to a directory URL.

    MkDocs builds each ``.md`` file into ``<stem>/index.html``, so
    relative links in raw HTML must use ``<stem>/`` rather than the
    source ``.md`` extension.
    """
    if filename.endswith(".md"):
        return filename[:-3] + "/"
    return filename


def _build_combined_timeline(adrs: list[dict]) -> str:
    """Build a unified timeline of every status transition across all ADRs.

    Entries are sorted chronologically (then by ADR number for ties).
    Instead of the person column used on individual pages, each entry
    shows a clickable link to the originating ADR.
    """
    # Flatten every status_history entry with its owning ADR metadata.
    flat: list[dict] = []
    for adr in adrs:
        num = adr["number"]
        title = adr["title"]
        url = _md_to_url(adr["filename"])
        label = f"ADR-{num:03d}: {title}"
        for entry in adr.get("status_history", []):
            entry_date = _normalise_date(entry[0])
            entry_status = str(entry[1]).strip()
            flat.append(
                {
                    "date_iso": entry_date,
                    "status": entry_status,
                    "adr_number": num,
                    "adr_label": label,
                    "adr_url": url,
                }
            )

    # Sort chronologically, then by ADR number for same-date entries.
    flat.sort(key=lambda e: (e["date_iso"], e["adr_number"]))

    total = len(flat)
    entries_html: list[str] = []

    for i, item in enumerate(flat):
        status_key = _status_key(item["status"])
        date_display = _format_date_display(item["date_iso"])
        is_last = i == total - 1

        dot_cls = f"adr-timeline-dot adr-dot-{status_key}"
        if is_last:
            dot_cls += " adr-dot-current"

        badge = _build_status_badge(item["status"])

        adr_link_html = (
            f'<div class="adr-timeline-adr-link">'
            f'{ICON_ADR}<a href="{item["adr_url"]}">{item["adr_label"]}</a>'
            f"</div>"
        )

        connector_line = '<div class="adr-timeline-line"></div>' if not is_last else ""

        entries_html.append(
            f'<div class="adr-timeline-entry">\n'
            f'  <div class="adr-timeline-date-col">\n'
            f'    <span class="adr-timeline-date">{date_display}</span>\n'
            f"  </div>\n"
            f'  <div class="adr-timeline-connector">\n'
            f'    <div class="{dot_cls}"></div>\n'
            f"    {connector_line}\n"
            f"  </div>\n"
            f'  <div class="adr-timeline-content">\n'
            f"    {badge}\n"
            f"    {adr_link_html}\n"
            f"  </div>\n"
            f"</div>"
        )

    inner = "\n".join(entries_html)
    return (
        f'\n<div class="adr-timeline adr-combined-timeline" markdown="0">\n'
        f"{inner}\n"
        f"</div>\n\n"
    )


# ── MkDocs hook entry-point ────────────────────────────────────────────


def on_page_markdown(
    markdown: str,
    page: Any,
    config: Any,
    files: Any,
    **kwargs: Any,
) -> str:
    """Transform ADR page markdown.

    - **Index page** (``adr/index.md``): replaces the ``<!-- ADR_INVENTORY -->``
      placeholder with auto-generated tier-grouped tables.
    - **Individual ADR pages**: strips old metadata, injects header + timeline.
    """
    # ── ADR index: auto-generate inventory + combined timeline ─────
    if _is_adr_index(page):
        needs_inventory = _INVENTORY_PLACEHOLDER in markdown
        needs_timeline = _TIMELINE_PLACEHOLDER in markdown

        if needs_inventory or needs_timeline:
            tiers_config = config.get("extra", {}).get("adr_tiers", {})
            adrs = _collect_adr_inventory(files)

            if needs_inventory:
                inventory = _generate_inventory_markdown(adrs, tiers_config)
                markdown = markdown.replace(_INVENTORY_PLACEHOLDER, inventory)

            if needs_timeline:
                timeline = _build_combined_timeline(adrs)
                markdown = markdown.replace(_TIMELINE_PLACEHOLDER, timeline)

        return markdown

    if not _is_adr_page(page):
        return markdown

    meta = page.meta or {}
    status_history = meta.get("status_history")

    if not status_history or not isinstance(status_history, list):
        log.warning(
            "ADR %s has no valid status_history in front-matter", page.file.src_path
        )
        return markdown

    # ── derive dates and status ─────────────────────────────────────
    # Earliest date = first entry in the status history
    earliest_date = _normalise_date(status_history[0][0])

    last_entry = status_history[-1]
    last_date = _normalise_date(last_entry[0])
    last_status = str(last_entry[1]).strip()

    # ── authors (explicit optional front-matter field) ──────────────
    authors = _normalise_people(meta.get("authors"))

    # ── tier info ───────────────────────────────────────────────────
    tier = meta.get("tier")
    tier_title = ""
    if tier is not None:
        tiers_config = config.get("extra", {}).get("adr_tiers", {})
        tier_info = tiers_config.get(tier, {})
        if isinstance(tier_info, dict):
            tier_title = tier_info.get("title", "")

    # ── superseded_by ───────────────────────────────────────────────
    superseded_by = meta.get("superseded_by")

    # ── strip old inline metadata block ─────────────────────────────
    markdown = _OLD_META_RE.sub("", markdown, count=1)

    # ── generate the H1 from adr_number + title front-matter ────────
    adr_number = meta.get("adr_number")
    adr_title = meta.get("title")

    if adr_number is not None and adr_title:
        # Remove any existing H1 line from the markdown body
        markdown = re.sub(r"^#\s+.+?\n+", "", markdown, count=1)
        generated_h1 = f"# ADR-{int(adr_number):03d}: {adr_title}\n"
    else:
        # Fallback: keep whatever H1 already exists
        h1_match = re.match(r"(#\s+.+?\n)", markdown)
        generated_h1 = ""  # leave existing H1 or use empty

    # ── build header + timeline ─────────────────────────────────────
    header_html = _build_header(
        earliest_date,
        last_date,
        last_status,
        tier,
        tier_title,
        superseded_by,
        authors,
        files,
    )
    timeline_html = _build_timeline(status_history)

    if adr_number is not None and adr_title:
        # H1 was stripped; prepend generated H1 + header + timeline
        markdown = generated_h1 + "\n" + header_html + timeline_html + markdown
    else:
        # Insert after the existing H1 line
        h1_match = re.match(r"(#\s+.+?\n)", markdown)
        if h1_match:
            insert_pos = h1_match.end()
            markdown = (
                markdown[:insert_pos]
                + "\n"
                + header_html
                + timeline_html
                + markdown[insert_pos:]
            )
        else:
            markdown = header_html + timeline_html + markdown

    # ── generate Related ADRs + Acceptance from front-matter ────────
    # Defensively strip any leftover hand-written sections first.
    markdown = _RELATED_SECTION_RE.sub("", markdown)
    markdown = _ACCEPTANCE_SECTION_RE.sub("", markdown)
    markdown = markdown.rstrip() + "\n"

    related_adrs = meta.get("related_adrs", [])
    acceptance = meta.get("acceptance", {})

    tail_sections: list[str] = []

    if related_adrs:
        tail_sections.append(_build_related_adrs_section(related_adrs, files))
    if acceptance:
        tail_sections.append(_build_acceptance_section(acceptance))

    if tail_sections:
        markdown += "\n" + "\n".join(tail_sections)

    return markdown
