"""Standing link board: parse and prune the multi-writer link log.

STDLIB ONLY, ON PURPOSE. This lives apart from app.py because the `booth` CLI
needs it and the CLI must not require the service's venv — importing app.py
drags in FastAPI, so a shell tool that only wants to delete a line would need
a web framework installed. The board is a text file; its logic should cost a
text file's worth of dependencies.
"""

from __future__ import annotations

import fcntl
import hashlib
import os
import re
from pathlib import Path

# ---- the standing link board ------------------------------------------------
#
# One booth (`links` by convention) is a MULTI-WRITER append log: every agent
# session on the fleet posts operator-facing URLs to it so they outlive the
# terminal scrollback that would otherwise bury them. That makes it the one
# booth where "delete the whole folder" is the wrong granularity — a single
# dead link has to be removable without taking the other thirty with it.
#
# Entries are identified by a CONTENT HASH, never by line number. Indexes are
# racy here by construction: another session can append between the moment you
# list the board and the moment you remove a row, and index-based removal would
# then delete the wrong line. A content id is stable against concurrent
# appends — the worst case is that the row is already gone, which is reported
# rather than silently deleting a neighbour.
LINKS_FILE = "links.md"
LINK_LOCK = ".links.lock"

# - [description](url) <sub>· who · when</sub>
_LINK_RE = re.compile(
    r"^- \[(?P<desc>.*?)\]\((?P<url>[^)]*)\)"
    r"(?:\s*<sub>·\s*(?P<who>[^·]*?)\s*·\s*(?P<when>[^<]*?)\s*</sub>)?\s*$"
)


def link_entry_id(raw: str) -> str:
    """Stable short id for a board row. Content-addressed, so it survives
    concurrent appends by other sessions and cannot drift like an index."""
    return hashlib.sha1(raw.strip().encode()).hexdigest()[:8]


def parse_link_entries(text: str) -> list[dict]:
    """Rows of the standing link board, newest last (posting order).

    Non-matching lines (a heading someone added by hand, a blank) are skipped
    rather than rejected: the board is a plain markdown file the operator is
    explicitly allowed to edit, so the parser must tolerate prose around the
    rows it understands.
    """
    out: list[dict] = []
    for i, raw in enumerate(text.splitlines()):
        m = _LINK_RE.match(raw.strip())
        if not m:
            continue
        out.append({
            "id": link_entry_id(raw),
            "raw": raw,
            "line": i,
            "desc": (m.group("desc") or "").strip(),
            "url": (m.group("url") or "").strip(),
            "who": (m.group("who") or "").strip(),
            "when": (m.group("when") or "").strip(),
        })
    return out


def remove_link_entry(board: Path, entry_id: str) -> dict | None:
    """Remove one row by content id. Returns the removed entry, or None.

    Held under an exclusive flock on a sidecar lock file for the whole
    read-modify-write, and the CLI's append path takes the same lock — so a
    concurrent `booth link` cannot be lost to this rewrite. Written to a temp
    file and os.replace'd, so a crash mid-write cannot truncate the board.
    """
    path = board / LINKS_FILE
    if not path.exists():
        return None
    lock = board / LINK_LOCK
    lock.touch(exist_ok=True)
    with lock.open("r+") as lf:
        fcntl.flock(lf, fcntl.LOCK_EX)
        try:
            text = path.read_text()
            kept, removed = [], None
            for raw in text.splitlines(keepends=True):
                if removed is None and link_entry_id(raw) == entry_id:
                    m = _LINK_RE.match(raw.strip())
                    if m:
                        removed = {"id": entry_id, "raw": raw.rstrip("\n"),
                                   "desc": (m.group("desc") or "").strip(),
                                   "url": (m.group("url") or "").strip()}
                        continue
                kept.append(raw)
            if removed is None:
                return None
            tmp = path.with_suffix(path.suffix + ".tmp")
            tmp.write_text("".join(kept))
            os.replace(tmp, path)
            return removed
        finally:
            fcntl.flock(lf, fcntl.LOCK_UN)
