"""utils/extractor_utils.py - Markdown extraction utilities."""

from dataclasses import dataclass
import re

CANONICAL_LIST_HEADING = "## Canonical Identifier List (Alphabetical, with Notes)"
IDENTIFIER_RE = re.compile(r"^[A-Z][A-Z0-9]*\.[A-Z0-9_]+(?:\.[A-Z0-9_]+)*$")


@dataclass(frozen=True)
class Requirement:
    """A canonical requirement identifier and explanatory note."""

    id: str
    note: str
    normative_source: str


def assert_alphabetical_order(
    identifiers: list[str],
    *,
    source_name: str,
) -> None:
    """Assert that identifiers are alphabetical."""
    if identifiers != sorted(identifiers):
        raise ValueError(f"{source_name} identifiers are not alphabetical.")


def assert_exact_identifier_coverage(
    *,
    canonical_ids: list[str],
    found_ids: list[str],
    source_name: str,
) -> None:
    """Assert exact coverage of canonical identifiers."""
    canonical_set = set(canonical_ids)
    found_set = set(found_ids)

    missing = sorted(canonical_set - found_set)
    unknown = sorted(found_set - canonical_set)

    if missing:
        raise ValueError(f"{source_name} is missing identifiers: {missing}")

    if unknown:
        raise ValueError(f"{source_name} has unknown identifiers: {unknown}")

    assert_unique(found_ids, source_name=source_name)


def assert_unique(identifiers: list[str], *, source_name: str) -> None:
    """Assert that identifiers are unique."""
    duplicates = sorted(
        identifier
        for identifier in set(identifiers)
        if identifiers.count(identifier) > 1
    )
    if duplicates:
        raise ValueError(f"{source_name} has duplicate identifiers: {duplicates}")


def extract_heading_identifiers(
    markdown: str,
    *,
    expected_prefix: str,
) -> list[str]:
    """Extract level-2 requirement headings from Markdown."""
    identifiers: list[str] = []

    for line in markdown.splitlines():
        stripped = line.strip()

        if not stripped.startswith("## "):
            continue

        heading = stripped.removeprefix("## ").strip()

        if heading.startswith(f"{expected_prefix}."):
            validate_identifier(heading, expected_prefix)
            identifiers.append(heading)

    return identifiers


def extract_identifier_notes(
    identifiers_markdown: str,
    *,
    expected_prefix: str,
) -> list[Requirement]:
    """Extract canonical identifiers and notes from IDENTIFIERS.md."""
    lines = identifiers_markdown.splitlines()
    start_index = _canonical_list_start(lines)
    requirements = _extract_requirements_from_canonical_list(
        lines,
        start_index=start_index,
        expected_prefix=expected_prefix,
    )

    identifiers = [requirement.id for requirement in requirements]

    if not identifiers:
        raise ValueError("No canonical identifiers found.")

    assert_unique(identifiers, source_name="IDENTIFIERS.md")
    assert_alphabetical_order(identifiers, source_name="IDENTIFIERS.md")

    return requirements


def _canonical_list_start(lines: list[str]) -> int:
    """Return the first line index after the canonical identifier heading."""
    for index, line in enumerate(lines):
        if line.strip() == CANONICAL_LIST_HEADING:
            return index + 1

    raise ValueError("No canonical identifiers found.")


def _extract_requirements_from_canonical_list(
    lines: list[str],
    *,
    start_index: int,
    expected_prefix: str,
) -> list[Requirement]:
    """Extract requirement records from the canonical identifier section."""
    requirements: list[Requirement] = []
    index = start_index

    while index < len(lines):
        line = lines[index].strip()

        if _is_next_section_heading(line):
            break

        if _is_identifier_line(line, expected_prefix):
            requirement, index = _parse_requirement_at(
                lines,
                index=index,
                expected_prefix=expected_prefix,
            )
            requirements.append(requirement)
            continue

        index += 1

    return requirements


def _is_next_section_heading(line: str) -> bool:
    """Return true when the canonical identifier section has ended."""
    return line.startswith("## ")


def _is_identifier_line(line: str, expected_prefix: str) -> bool:
    """Return true when a line begins with the expected identifier prefix."""
    return line.startswith(f"{expected_prefix}.")


def _parse_requirement_at(
    lines: list[str],
    *,
    index: int,
    expected_prefix: str,
) -> tuple[Requirement, int]:
    """Parse one identifier plus its required single note bullet."""
    identifier = lines[index].strip()
    validate_identifier(identifier, expected_prefix)

    note_index = _next_nonblank_index(lines, index + 1)
    if note_index >= len(lines):
        raise ValueError(f"Missing note for {identifier}")

    note = _parse_single_note(lines[note_index].strip(), identifier)

    following_index = _next_nonblank_index(lines, note_index + 1)
    _assert_no_second_note(lines, following_index, identifier)

    return (
        Requirement(
            id=identifier,
            note=note,
            normative_source=f"SPEC.md#{identifier}",
        ),
        following_index,
    )


def _next_nonblank_index(lines: list[str], start_index: int) -> int:
    """Return the next nonblank line index, or len(lines)."""
    index = start_index
    while index < len(lines) and not lines[index].strip():
        index += 1
    return index


def _parse_single_note(note_line: str, identifier: str) -> str:
    """Parse the required single note bullet for an identifier."""
    if not note_line.startswith("- "):
        raise ValueError(f"Identifier {identifier} must have one note bullet.")

    note = note_line.removeprefix("- ").strip()
    if not note:
        raise ValueError(f"Identifier {identifier} has an empty note.")

    return note


def _assert_no_second_note(
    lines: list[str],
    following_index: int,
    identifier: str,
) -> None:
    """Reject a second note bullet immediately after the first note."""
    if following_index >= len(lines):
        return

    following_line = lines[following_index].strip()
    if following_line.startswith("- "):
        raise ValueError(f"Identifier {identifier} has multiple notes.")


def validate_identifier(identifier: str, expected_prefix: str) -> None:
    """Validate a requirement identifier."""
    if not IDENTIFIER_RE.fullmatch(identifier):
        raise ValueError(f"Invalid identifier: {identifier}")

    expected_start = f"{expected_prefix}."
    if not identifier.startswith(expected_start):
        raise ValueError(
            f"Identifier {identifier} does not use expected prefix {expected_prefix}."
        )
