"""Pure parsers for skill files and string formats (#82 Phase 1).

No external dependencies beyond the standard library — anything
imported here must remain stdlib-only so the `:parsers` py_library
stays a true leaf in the Bazel dep graph and unit tests of these
helpers run without pulling in github / mcp / pydantic.
"""
from __future__ import annotations

import re

# ----- Skill version / changelog parsing (#60) ------------------------------

# Version pattern accepts PEP-440-style pre-release suffixes
# (e.g. "2.0.0a2", "2.0.0b1", "2.0.0rc1") in addition to plain
# numeric semver. Only `a` / `b` / `rc` qualifiers are accepted —
# matches what the skill changelog actually emits (#159).
_VERSION_PAT = r"[0-9]+(?:\.[0-9]+){1,2}(?:(?:a|b|rc)[0-9]+)?"

_VERSION_HEADER_RE = re.compile(
    rf"^\*\*Version:\*\*\s*({_VERSION_PAT})\s*$", re.MULTILINE
)

_CHANGELOG_SECTION_RE = re.compile(
    r"##\s+Changelog\s*\n(.*?)(?=\n##\s+|\Z)", re.DOTALL
)

_CHANGELOG_ENTRY_RE = re.compile(
    rf"^- \*\*({_VERSION_PAT})\*\*\s*"
    r"\(([0-9]{4}-[0-9]{2}-[0-9]{2})\)\s*"
    r"—\s*(.+?)"
    r"(?=\n- \*\*|\Z)",
    re.MULTILINE | re.DOTALL,
)


def parse_skill_version(content: str) -> str | None:
    """Extract the X.Y.Z from a `**Version:** X.Y.Z` header line."""
    if not content:
        return None
    m = _VERSION_HEADER_RE.search(content)
    return m.group(1) if m else None


def parse_changelog(content: str) -> list[dict]:
    """Parse a `## Changelog` section into ordered entries.

    Each entry: {"version": str, "date": str, "summary": str}
    Order matches the file — typically newest first.
    """
    if not content:
        return []
    section_match = _CHANGELOG_SECTION_RE.search(content)
    if not section_match:
        return []
    section_text = section_match.group(1)
    entries: list[dict] = []
    for m in _CHANGELOG_ENTRY_RE.finditer(section_text):
        version, date, summary = m.group(1), m.group(2), m.group(3).strip()
        entries.append({"version": version, "date": date, "summary": summary})
    return entries


_PRE_RELEASE_RE = re.compile(r"^(a|b|rc)([0-9]+)$")
# Pre-release rank: a < b < rc < final. Final = no suffix; encoded as
# (rank=3, n=0) so a comparison against any pre-release is greater.
_PRE_RANK = {"a": 0, "b": 1, "rc": 2}


def _split_version(s: str) -> tuple[tuple[int, int, int], tuple[int, int]]:
    """Split a version string into ((major, minor, patch), (pre_rank, pre_n)).

    Plain numeric versions get pre = (3, 0) so they sort after every
    pre-release of the same numeric tuple. Pre-release strings get
    pre = (rank, n) where rank ∈ {0,1,2} for a/b/rc.
    """
    head, _, tail = s.partition(".")
    parts = s.split(".")
    # Last numeric segment may carry a pre-release suffix (e.g. "0a2").
    last = parts[-1]
    pre = (3, 0)  # final
    m = re.match(r"^([0-9]+)((?:a|b|rc)[0-9]+)?$", last)
    if m and m.group(2):
        parts[-1] = m.group(1)
        pm = _PRE_RELEASE_RE.match(m.group(2))
        if pm:
            pre = (_PRE_RANK[pm.group(1)], int(pm.group(2)))
    nums = [int(x) for x in parts]
    while len(nums) < 3:
        nums.append(0)
    return (nums[0], nums[1], nums[2]), pre


def semver_gt(a: str, b: str) -> bool:
    """Return True if version `a` is strictly greater than version `b`.

    Treats missing components as 0 (e.g. "1.2" → (1, 2, 0)). Pre-release
    suffixes (a/b/rc) sort before the corresponding final release —
    so "2.0.0" > "2.0.0rc1" > "2.0.0b3" > "2.0.0a2" > "2.0.0a1" (#159).
    """
    return _split_version(a) > _split_version(b)


def parse_owner_repo(full_name: str) -> tuple[str, str]:
    """Split 'owner/repo' into (owner, repo). Raises ValueError if invalid."""
    parts = full_name.split("/")
    if len(parts) != 2 or not parts[0] or not parts[1]:
        raise ValueError(
            f"Expected 'owner/repo' format, got: {full_name!r}"
        )
    return parts[0], parts[1]
