#!/usr/bin/env python3
"""
Pre-commit hook: Validate all [[...]] wiki-links in wiki/*.md files.

Link format: [[path/to/file.md]] or [[path/to/file.md|Label]]
Resolution: SOURCE-RELATIVE (matches Forgejo/Gitea Markdown rendering behavior)
- Links resolve relative to the current file's directory
- [[foo.md]] in wiki/architecture/bar.md → wiki/architecture/foo.md
- [[../tools/foo.md]] in wiki/architecture/bar.md → wiki/tools/foo.md
- [[concepts/llm/foo.md]] in wiki/concepts/llm/bar.md → wiki/concepts/llm/concepts/llm/foo.md (BROKEN)

The third case is the "path confusion" bug — links that look root-relative but
double-prefix when interpreted source-relative. This script flags them so they
can be rewritten using either same-directory basename or proper ../-prefix.

Usage:
  python3 scripts/check-wiki-links.py           # check + report
  python3 scripts/check-wiki-links.py --strict  # exit 1 on warnings too
"""

import os
import re
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
WIKI_DIR = REPO_ROOT / "wiki"
LINK_RE = re.compile(r'\[\[([^\]|]+)(?:\|([^\]]+))?\]\]')


def find_wiki_files():
    """Find all .md files under wiki/."""
    files = []
    for root, dirs, filenames in os.walk(WIKI_DIR):
        for f in filenames:
            if f.endswith('.md'):
                files.append(Path(root) / f)
    return sorted(files)


def strip_code_spans(content: str) -> str:
    """Remove inline-code spans (between backticks) so we don't check links inside them.
    Also removes fenced code blocks (``` ... ```).
    """
    # Remove fenced code blocks first
    lines = content.split('\n')
    out = []
    in_fence = False
    for line in lines:
        if line.strip().startswith('```'):
            in_fence = not in_fence
            continue
        if not in_fence:
            out.append(line)
    content = '\n'.join(out)
    # Remove inline code spans
    content = re.sub(r'`[^`\n]+`', '', content)
    return content


def resolve_link(link_path: str, source_file: Path) -> Path:
    """
    Resolve a wiki link source-relative (matching Forgejo behavior).
    Also tries with .md suffix appended if needed.
    """
    if link_path.startswith('http'):
        return None

    # Strip anchor
    if '#' in link_path:
        link_path = link_path.split('#', 1)[0]

    # Strip trailing slash
    link_path = link_path.rstrip('/')

    # Try as-is (relative to source file's directory)
    target = (source_file.parent / link_path).resolve()
    if target.exists():
        return target

    # Try with .md suffix
    if not link_path.endswith('.md'):
        target = (source_file.parent / (link_path + '.md')).resolve()
        if target.exists():
            return target

    return None


def has_path_confusion(link_path: str, source_file: Path) -> bool:
    """
    Detect 'path confusion': link like [[concepts/llm/foo.md]] from a file in
    wiki/concepts/llm/bar.md would resolve to wiki/concepts/llm/concepts/llm/foo.md.
    Returns True if the link looks root-relative but is used from inside a subdir.
    """
    if link_path.startswith('http') or link_path.startswith('../') or '/' not in link_path:
        return False
    # Check if link_path starts with a directory the source file is already in
    try:
        rel = source_file.relative_to(WIKI_DIR)
        source_dir_parts = rel.parent.parts
        link_parts = Path(link_path).parts
        # If link starts with the same subdirectory chain
        if len(link_parts) >= len(source_dir_parts):
            if link_parts[:len(source_dir_parts)] == source_dir_parts:
                return True
    except ValueError:
        pass
    return False


def check_all_links():
    files = find_wiki_files()
    errors = []
    warnings = []
    stats = {
        'total_links': 0,
        'valid_links': 0,
        'broken_links': 0,
        'confusion_warnings': 0,
        'files_checked': 0,
    }

    for source_file in files:
        stats['files_checked'] += 1
        rel_source = source_file.relative_to(REPO_ROOT)
        content = source_file.read_text(encoding='utf-8')
        # Strip code spans so we don't false-positive on example syntax in backticks
        content = strip_code_spans(content)

        for match in LINK_RE.finditer(content):
            link_path = match.group(1).strip()
            stats['total_links'] += 1

            if link_path.startswith('http'):
                stats['valid_links'] += 1
                continue

            # Resolve source-relative
            target = resolve_link(link_path, source_file)

            if target:
                stats['valid_links'] += 1
            else:
                # Skip template placeholders like {slug}
                if '{' in link_path:
                    stats['valid_links'] += 1
                    continue
                # Try alternative suggestions
                basename = Path(link_path).name
                matches = [m for m in WIKI_DIR.rglob(basename) if 'test-fixtures' not in str(m)]
                error = {
                    'source': str(rel_source),
                    'link': link_path,
                    'suggestions': [str(m.relative_to(REPO_ROOT)) for m in matches[:3]],
                }
                errors.append(error)
                stats['broken_links'] += 1

            # Check for path confusion (link works repo-relative but broken source-relative)
            # Skip template placeholders like {slug}
            if '{' not in link_path and has_path_confusion(link_path, source_file):
                # Only warn if the link is actually broken source-relative
                if not resolve_link(link_path, source_file):
                    correct_basename = Path(link_path).name
                    warnings.append({
                        'source': str(rel_source),
                        'link': link_path,
                        'note': f"Path confusion — link starts with {link_path.split('/')[0]}/ which doubles the source-directory prefix. Use same-directory basename or proper ../-prefix.",
                        'suggestion': correct_basename,
                    })
                    stats['confusion_warnings'] += 1

    return errors, warnings, stats


def main():
    strict = '--strict' in sys.argv

    errors, warnings, stats = check_all_links()

    print("Wiki Link Report")
    print("================")
    print(f"Files checked:        {stats['files_checked']}")
    print(f"Total links:          {stats['total_links']}")
    print(f"Valid links:          {stats['valid_links']}")
    print(f"Broken links:         {stats['broken_links']}")
    print(f"Path confusion warns: {stats['confusion_warnings']}")
    print()

    if errors:
        print(f"BROKEN LINKS ({len(errors)}):")
        print("-" * 60)
        for e in errors[:30]:
            print(f"  {e['source']}")
            print(f"    Link: [[{e['link']}]]")
            if e['suggestions']:
                print(f"    Did you mean: {e['suggestions'][0]}")
            print()
        if len(errors) > 30:
            print(f"  ... and {len(errors) - 30} more")

    if warnings:
        print(f"\nPATH CONFUSION WARNINGS ({len(warnings)}):")
        print("-" * 60)
        for w in warnings[:20]:
            print(f"  {w['source']}")
            print(f"    Link: [[{w['link']}]]")
            print(f"    {w['note']}")
            print(f"    Suggestion: [[{w['suggestion']}]] (same dir) or [[../...]]")
            print()
        if len(warnings) > 20:
            print(f"  ... and {len(warnings) - 20} more")

    failed = False
    if stats['broken_links'] > 0:
        print(f"\n❌ {stats['broken_links']} broken link(s) — commit blocked.")
        failed = True

    if stats['confusion_warnings'] > 0:
        print(f"\n⚠️  {stats['confusion_warnings']} path confusion warning(s).")
        if strict:
            print("Strict mode: commit blocked.")
            failed = True

    if not failed:
        print("\n✅ All wiki links valid.")

    sys.exit(1 if failed else 0)


if __name__ == '__main__':
    main()