#!/usr/bin/env python3
"""Проверяет локальные Markdown-ссылки в поддерживаемой документации."""
from __future__ import annotations

import argparse
import re
import sys
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import unquote


REPO_ROOT = Path(__file__).resolve().parents[2]
ROOT_MARKDOWN_FILES = ("README.md", "AGENTS.md", "CLAUDE.md", "QUICKSTART.md")
EXTERNAL_LINK_PREFIX = re.compile(r"^(?:https?://|mailto:)", re.IGNORECASE)


@dataclass(frozen=True)
class BrokenLink:
    """Одна ссылка, чей локальный путь не существует."""

    source: Path
    line: int
    target: str
    resolved: Path


def documentation_files(repository_root: Path) -> list[Path]:
    """Возвращает поддерживаемые Markdown-файлы в устойчивом порядке."""

    # Отчёты прогонов роя — доказательства работы агентов, а не документация:
    # они ссылаются на пути временных клонов (/private/tmp/fleet-*), которых
    # давно нет и не будет, поэтому обрывами документации не считаются.
    # Исключение намеренно узкое — только runs/, а не docs/agents/ целиком:
    # в docs/agents/ лежат замеры и задания, это документация и проверяться обязана.
    agents_runs_root = repository_root / "docs" / "agents" / "runs"

    files: set[Path] = set()
    docs_root = repository_root / "docs"
    if docs_root.is_dir():
        files.update(
            path
            for path in docs_root.rglob("*.md")
            if path.is_file()
            and not path.name.startswith("._")
            and not path.is_relative_to(agents_runs_root)
        )

    for name in ROOT_MARKDOWN_FILES:
        path = repository_root / name
        if path.is_file():
            files.add(path)

    agents_root = repository_root / "for-ai-agents"
    if agents_root.is_dir():
        files.update(
            path for path in agents_root.glob("*.md")
            if path.is_file() and not path.name.startswith("._")
        )

    return sorted(files, key=lambda path: path.relative_to(repository_root).as_posix())


def _find_closing_bracket(line: str, opening: int) -> int | None:
    """Находит неэкранированную закрывающую скобку Markdown-подписи."""

    position = opening + 1
    while position < len(line):
        if line[position] == "\\":
            position += 2
            continue
        if line[position] == "]":
            return position
        position += 1
    return None


def _find_closing_parenthesis(line: str, opening: int) -> int | None:
    """Находит конец назначения, не обрывая путь с круглыми скобками."""

    depth = 1
    position = opening + 1
    while position < len(line):
        character = line[position]
        if character == "\\":
            position += 2
            continue
        if character == "(":
            depth += 1
        elif character == ")":
            depth -= 1
            if depth == 0:
                return position
        position += 1
    return None


def _destination(payload: str) -> str:
    """Выделяет путь из назначения, отделяя необязательный Markdown-заголовок."""

    value = payload.strip()
    if not value:
        return ""
    if value.startswith("<"):
        closing = value.find(">", 1)
        if closing != -1:
            return value[1:closing]
    return value.split(maxsplit=1)[0]


def markdown_link_targets(line: str) -> Iterator[str]:
    """Итерирует назначения обычных inline-ссылок в одной строке."""

    position = 0
    while True:
        opening = line.find("[", position)
        if opening == -1:
            return
        if opening > 0 and line[opening - 1] in {"!", "\\"}:
            position = opening + 1
            continue

        label_end = _find_closing_bracket(line, opening)
        if label_end is None or label_end + 1 >= len(line) or line[label_end + 1] != "(":
            position = opening + 1
            continue

        target_end = _find_closing_parenthesis(line, label_end + 1)
        if target_end is None:
            position = label_end + 1
            continue

        yield _destination(line[label_end + 2:target_end])
        position = target_end + 1


def _links_outside_code_blocks(markdown_file: Path) -> Iterator[tuple[int, str]]:
    """Возвращает ссылки вне fenced-кода вместе с их исходными номерами строк."""

    in_code_block = False
    for line_number, line in enumerate(markdown_file.read_text(encoding="utf-8").splitlines(), start=1):
        # Ссылки в fenced-блоках — примеры команд и путей, а не навигация документа.
        if line.lstrip().startswith("```"):
            in_code_block = not in_code_block
            continue
        if in_code_block:
            continue
        for target in markdown_link_targets(line):
            yield line_number, target


def _resolved_local_path(source: Path, target: str) -> Path:
    """Разрешает путь ровно относительно файла, где стоит Markdown-ссылка."""

    path_part = unquote(target.split("#", maxsplit=1)[0])
    if not path_part:
        return source.resolve()
    return (source.parent / path_part).resolve()


def find_broken_links(repository_root: Path) -> list[BrokenLink]:
    """Проверяет локальные ссылки во всех документах заданного репозитория."""

    broken_links: list[BrokenLink] = []
    for markdown_file in documentation_files(repository_root):
        for line, target in _links_outside_code_blocks(markdown_file):
            if EXTERNAL_LINK_PREFIX.match(target):
                continue
            resolved = _resolved_local_path(markdown_file, target)
            # Каталог тоже может быть осмысленной точкой навигации, поэтому достаточно exists().
            if not resolved.exists():
                broken_links.append(BrokenLink(markdown_file, line, target, resolved))
    return broken_links


def main() -> int:
    """Печатает все нарушения, чтобы один запуск давал полный список для разбора."""

    parser = argparse.ArgumentParser(description="Проверяет локальные Markdown-ссылки.")
    parser.add_argument(
        "--root",
        type=Path,
        default=REPO_ROOT,
        help="корень дерева (по умолчанию — сам репозиторий; тесты подставляют поддельное дерево)",
    )
    args = parser.parse_args()

    broken_links = find_broken_links(args.root)
    for broken in broken_links:
        source = broken.source.relative_to(args.root).as_posix()
        print(f"{source}:{broken.line}: {broken.target!r} -> {broken.resolved}")
    return 1 if broken_links else 0


if __name__ == "__main__":
    sys.exit(main())
