"""The ONE wikilink parser — regexes, frontmatter, aliases, link extraction.

Two graph builders sit on this: :mod:`vault_core.graph` (the engine/spider view —
``{outlinks, backlinks, unresolved}``, warm-cached by the MCP service) and
:mod:`vault_core.query.vaultgraph` (the retrieval view —
``(notes, by_name, inbound, read_s)``). **Those two output shapes stay separate
by design**; they answer different questions for different callers. What must
never diverge again is how a link is *read out of a file*, which is what lives
here.

They diverged before: identical regexes were copy-pasted into both modules and
then drifted in the one place it mattered — frontmatter. ``vaultgraph`` scanned
``area:``/``parent:``/``related:`` wikilinks as edges, ``graph`` split
frontmatter off and never looked, so ``links_get``/``graph_get``/``think_deep``
were blind to the structural edges of a PEAK vault while ``vaultq_neighbors``
saw them (2026-07-26 review, docs/MCP-TOOL-SURFACE.md §7).

A frontmatter wikilink is an edge because it is a wikilink **in the bytes of the
file** — that is a file-format convention, not a property of any editor being
installed. It holds identically on a laptop vault and a hosted brain that has no
Obsidian and no local folder.
"""
import re
import unicodedata

# Strip fenced blocks first, then inline code — a link inside either is a
# quotation of a link, not an edge. (Neither ``~~~`` fences nor 4-space indented
# blocks are handled; that gap is shared with every caller and predates this
# module.)
FENCE = re.compile(r"```.*?```", re.S)
INLINE = re.compile(r"`[^`]*`")
WIKILINK = re.compile(r"(!?)\[\[([^\]]+)\]\]")

_FM_KEY = re.compile(r"^([A-Za-z0-9_\-]+):\s?(.*)$")


def strip_code(body: str) -> str:
    """Body with fenced and inline code blanked out."""
    return INLINE.sub(" ", FENCE.sub(" ", body))


def clean_target(raw: str) -> str:
    """A raw wikilink inner → its bare target: display alias dropped (including
    the ``\\|`` table escape) and the ``#heading`` / ``#^blockid`` anchor cut."""
    return raw.replace("\\|", "|").split("|")[0].split("#")[0].strip()


def links_in(body: str):
    """``[(is_embed, raw_inner), ...]`` for every wikilink in a body, code-stripped.

    Returns the RAW inner text; callers decide whether they want the alias and
    anchor (``parse_wikilinks``) or just the target (``clean_target``).
    """
    return [(embed == "!", t) for embed, t in WIKILINK.findall(strip_code(body))]


def targets_in(body: str):
    """Bare link targets in a body, in document order, code-stripped."""
    return [t for t in (clean_target(raw) for _e, raw in links_in(body)) if t]


def split_frontmatter(text: str):
    """``(fm, body, terminated, present)`` — light YAML, top-level scalars only.

    ``terminated`` is False for an unterminated ``---`` block, in which case the
    whole text is returned as body (so its pseudo-frontmatter still gets scanned
    for links — matching both original implementations).
    """
    if not text.startswith("---"):
        return {}, text, True, False
    end = text.find("\n---", 3)
    if end == -1:
        return {}, text, False, True
    fm = {}
    for line in text[3:end].splitlines():
        m = _FM_KEY.match(line)
        if m:
            fm[m.group(1)] = m.group(2).strip()
    return fm, text[end + 4:], True, True


def frontmatter_block(text: str) -> str:
    """The raw text between the ``---`` fences, or ``""`` if there is no
    terminated frontmatter. Unterminated frontmatter yields ``""`` here because
    ``split_frontmatter`` already hands that text back as body — scanning it in
    both places would double-count every link in it."""
    if not text.startswith("---"):
        return ""
    end = text.find("\n---", 3)
    return "" if end == -1 else text[3:end]


def frontmatter_targets(text: str):
    """Bare link targets in a note's frontmatter (``area:``, ``parent:``,
    ``related:`` …). These are real edges — see the module docstring."""
    return [t for t in (clean_target(raw)
                        for _e, raw in WIKILINK.findall(frontmatter_block(text))) if t]


def resolve_key(target: str) -> str:
    """A link target (or a note's basename) → the key both sides of a lookup use.

    Two long-standing resolution bugs live here, and they were shared by every
    implementation rather than being a divergence between them, so unifying the
    parsers would not have found either:

    - **Unicode normal form.** macOS stores ``Café.md`` decomposed (NFD) while a
      link typed in an editor is usually composed (NFC). Same name, different
      bytes, so ``[[Café]]`` did not resolve to the file sitting next to it.
      Both sides are folded to NFC here.
    - **``[[Foo.md]]``.** The extension used to be stripped only when the target
      contained ``/``, so a bare ``[[Home.md]]`` never matched ``Home.md``.

    Only ``.md`` is stripped — ``[[img.png]]`` keeps its extension so attachment
    suppression can still recognise it.
    """
    t = unicodedata.normalize("NFC", str(target or "")).strip()
    if "/" in t:
        t = t.rsplit("/", 1)[-1]
    if t.lower().endswith(".md"):
        t = t[:-3]
    return t.lower()


def parse_aliases(av: str):
    """An ``aliases:`` value → the alias list. Quote-aware: a quoted ``"A, B"``
    stays ONE alias; a bare scalar is a single alias."""
    av = (av or "").strip()
    if not av:
        return []
    if av.startswith("["):
        inner = av[1:-1] if av.endswith("]") else av[1:]
        quoted = re.findall(r'"([^"]*)"|\'([^\']*)\'', inner)
        vals = [a or b for a, b in quoted] if quoted else inner.split(",")
    else:
        vals = [av]
    return [v.strip().strip("\"'") for v in vals if v.strip().strip("\"'")]


def parse_wikilinks(text: str):
    """Wikilinks in one note body → dicts of ``target``, ``heading``, ``alias``,
    ``embed``. Body only: frontmatter edges come from ``frontmatter_targets``."""
    out = []
    for is_embed, inner in links_in(text):
        raw = inner.strip().replace("\\|", "|")
        target, alias = (raw.split("|", 1) + [None])[:2]
        target, heading = (target.split("#", 1) + [None])[:2]
        target = (target or "").strip()
        if not target:
            continue
        out.append({
            "target": target,
            "heading": heading.strip() if heading else None,
            "alias": alias.strip() if alias else None,
            "embed": is_embed,
        })
    return out
