#!/usr/bin/env python3
from __future__ import annotations

import argparse
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterator, Optional, Sequence


EXCLUDED_DIRECTORIES = {".git", "build", "dist", "node_modules", "tmp"}
FENCE_PATTERN = re.compile(r"^\s*(`{3,}|~{3,})")
INLINE_CODE_PATTERN = re.compile(r"`[^`]*`")
INLINE_LINK_PATTERN = re.compile(r"!?\[[^]]*\]\(([^)]*)\)")
REFERENCE_LINK_PATTERN = re.compile(
    r"^\s*\[[^]]+\]:\s*(?:<([^>]+)>|([^\s]+))"
)


@dataclass(frozen=True)
class MarkdownLink:
    line: int
    target: str


@dataclass(frozen=True)
class BrokenLink:
    source: Path
    line: int
    target: str
    resolved_target: Path


@dataclass
class LinkCheckResult:
    markdown_files: int = 0
    total_links: int = 0
    valid_links: int = 0
    skipped_links: int = 0
    broken_links: list[BrokenLink] = field(default_factory=list)


def extract_links(text: str) -> Iterator[MarkdownLink]:
    fence_character: Optional[str] = None

    for line_number, raw_line in enumerate(text.splitlines(), start=1):
        fence_match = FENCE_PATTERN.match(raw_line)
        if fence_match:
            marker_character = fence_match.group(1)[0]
            if fence_character is None:
                fence_character = marker_character
            elif marker_character == fence_character:
                fence_character = None
            continue
        if fence_character is not None:
            continue

        line = INLINE_CODE_PATTERN.sub("", raw_line)
        for match in INLINE_LINK_PATTERN.finditer(line):
            yield MarkdownLink(line_number, match.group(1).strip())

        reference_match = REFERENCE_LINK_PATTERN.match(line)
        if reference_match:
            target = reference_match.group(1) or reference_match.group(2)
            yield MarkdownLink(line_number, target.strip())


def iter_markdown_files(root: Path) -> Iterator[Path]:
    for path in sorted(root.rglob("*.md")):
        relative_path = path.relative_to(root)
        if path.name.endswith(".template.md"):
            continue
        if any(part in EXCLUDED_DIRECTORIES for part in relative_path.parts[:-1]):
            continue
        if path.is_file():
            yield path


def _local_target(target: str) -> Optional[str]:
    target = target.strip()
    if target.startswith("<") and target.endswith(">"):
        target = target[1:-1].strip()

    target = target.split("#", 1)[0]
    if not target:
        return None

    lower_target = target.lower()
    if lower_target.startswith(("http://", "https://", "mailto:")):
        return None
    if target.startswith("/en/docs/"):
        return None
    return target


def _resolve_target(root: Path, source: Path, target: str) -> Path:
    if target.startswith("/"):
        return (root / target.lstrip("/")).resolve()
    return (source.parent / target).resolve()


def check_repository(root: Path) -> LinkCheckResult:
    root = root.resolve()
    markdown_files = list(iter_markdown_files(root))
    result = LinkCheckResult(markdown_files=len(markdown_files))

    for source in markdown_files:
        text = source.read_text(encoding="utf-8")
        for link in extract_links(text):
            result.total_links += 1
            local_target = _local_target(link.target)
            if local_target is None:
                result.skipped_links += 1
                continue

            resolved_target = _resolve_target(root, source, local_target)
            if resolved_target.exists():
                result.valid_links += 1
                continue

            result.broken_links.append(
                BrokenLink(source, link.line, link.target, resolved_target)
            )

    return result


def _display_path(path: Path, root: Path) -> str:
    try:
        return path.relative_to(root).as_posix()
    except ValueError:
        return str(path)


def print_report(result: LinkCheckResult, root: Path) -> None:
    root = root.resolve()
    print(f"Playbook root: {root}")
    print(f"Markdown files: {result.markdown_files}")

    for broken in result.broken_links:
        source = _display_path(broken.source, root)
        target = _display_path(broken.resolved_target, root)
        print(f"BROKEN: {source}:{broken.line}")
        print(f"  Link: {broken.target}")
        print(f"  Target: {target}")

    print(f"Total links: {result.total_links}")
    print(f"Valid links: {result.valid_links}")
    print(f"Skipped links: {result.skipped_links}")
    print(f"Broken links: {len(result.broken_links)}")


def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Check local links in Markdown files.")
    parser.add_argument(
        "--root",
        type=Path,
        default=Path(__file__).resolve().parents[2],
        help="repository root to scan (default: inferred from this script)",
    )
    return parser.parse_args(argv)


def _configure_output() -> None:
    for stream in (sys.stdout, sys.stderr):
        reconfigure = getattr(stream, "reconfigure", None)
        if reconfigure is not None:
            reconfigure(errors="backslashreplace")


def main(argv: Optional[Sequence[str]] = None) -> int:
    _configure_output()
    args = parse_args(argv)
    root = args.root.resolve()
    if not root.is_dir():
        print(f"ERROR: root directory does not exist: {root}")
        return 2

    result = check_repository(root)
    print_report(result, root)
    if result.broken_links:
        print("Document link check failed.")
        return 1

    print("All document links passed.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
