#!/usr/bin/env python3
"""Sphinx toctree globパターン展開スクリプト。

概要:
    Sphinxのtoctreeにおけるglobパターンを展開し、指定されたファイル群を自動的に挿入します。

詳細説明:
    親のreStructuredTextファイル内の `.. toctree::` ディレクティブにおいて、
    ファイル名パターン（glob形式）が指定された場合、そのパターンに合致するファイルを
    自動的に見つけ出し、toctreeのエントリとして挿入します。
    Markdown (`.md`) と reStructuredText (`.rst`) ファイルの両方に対応します。
    重複エントリの除外、エントリのソート、および子ファイルのセクションタイトルを
    ラベルとして使用するオプションをサポートします。

関連リンク:
    :doc:`burst_sphinx_index_usage`
"""
import argparse
import re
from pathlib import Path
import shutil
import sys
from typing import List, Optional


RST_UNDERLINE_RE = re.compile(r'^[=\-~`^"\'#*+]+$')
MD_H1_RE = re.compile(r'^\s*#\s+(.+?)\s*$')


def log(verbose: bool, message: str):
    """verboseモードが有効な場合に標準エラー出力に情報メッセージを出力する。

    :param verbose: bool - メッセージを出力するかどうかを決定するフラグ。
    :param message: str - 出力する情報メッセージ。
    :returns: None
    """
    if verbose:
        print(f"[INFO] {message}", file=sys.stderr)


def warn(message: str):
    """標準エラー出力に警告メッセージを出力する。

    :param message: str - 出力する警告メッセージ。
    :returns: None
    """
    print(f"[WARN] {message}", file=sys.stderr)


def indent_width(line: str) -> int:
    """行頭のインデント幅を計算して返す。

    詳細説明:
        タブ文字 (`\t`) は4文字分のスペースとして扱って計算します。

    :param line: str - インデント幅を計算する対象の行文字列。
    :returns: int - 行頭のインデント幅。
    """
    expanded = line.expandtabs(4)
    return len(expanded) - len(expanded.lstrip(" "))


def extract_first_section_title(path: Path, verbose: bool = False) -> Optional[str]:
    """指定されたファイルから最初のセクションタイトルを抽出する。

    詳細説明:
        reStructuredText形式の下線見出し (`----`, `====` など) または
        Markdown形式のH1見出し (`# Title`) のいずれかをサポートします。

    :param path: Path - タイトルを抽出する対象ファイルのパス。
    :param verbose: bool - 詳細ログを出力するかどうか。デフォルトは `False`。
    :returns: Optional[str] - 抽出されたタイトル文字列。タイトルが見つからない場合やエラーが発生した場合は `None`。
    """
    try:
        with path.open(encoding="utf-8") as f:
            lines = f.readlines()

        # Markdown H1
        for line in lines:
            m = MD_H1_RE.match(line.rstrip("\n"))
            if m:
                title = m.group(1).strip()
                if title:
                    return title

        # reStructuredText underline style
        for i in range(len(lines) - 1):
            title = lines[i].rstrip()
            underline = lines[i + 1].rstrip()
            if title and RST_UNDERLINE_RE.match(underline):
                return title

    except Exception as e:
        log(verbose, f"title extraction failed: {path} ({e})")

    return None


def glob_with_extensions(parent_dir: Path, pattern: str, verbose: bool = False) -> List[Path]:
    """Sphinxのtoctreeのglobルールに沿ってファイルを検索する。

    詳細説明:
        パターンに拡張子がない場合、`.rst` と `.md` の両方を補完してファイルを検索します。
        拡張子が明示されている場合は、その拡張子のみで検索します。

    :param parent_dir: Path - 検索の基準となる親ディレクトリのパス。
    :param pattern: str - 検索するファイル名のglobパターン。
    :param verbose: bool - 詳細ログを出力するかどうか。デフォルトは `False`。
    :returns: List[Path] - パターンにマッチしたファイルのパスのリスト。
    """
    p = Path(pattern)
    if p.suffix:
        paths = list(parent_dir.glob(pattern))
        log(verbose, f"glob pattern={pattern!r} matched {len(paths)} path(s) with explicit suffix")
    else:
        paths = []
        for ext in (".rst", ".md"):
            matched = list(parent_dir.glob(pattern + ext))
            log(verbose, f"glob pattern={pattern + ext!r} matched {len(matched)} path(s)")
            paths.extend(matched)
    return paths


def expand_glob_in_toctree(parent_rst: Path, sort_flag: bool, label_flag: bool, verbose: bool = False) -> str:
    """reStructuredTextファイル内のtoctreeにおけるglobパターンを展開して置換する。

    詳細説明:
        複数の `.. toctree::` ディレクティブに対応し、globパターンに合致する子ファイルを
        toctreeエントリとして挿入した新しい内容を返します。
        展開されたエントリのソートやラベル付けもサポートします。

    :param parent_rst: Path - globパターンを展開する対象の親reStructuredTextファイルのパス。
    :param sort_flag: bool - 展開されたエントリをファイル名でソートするかどうか。`True` の場合ソートされる。
    :param label_flag: bool - 展開されたエントリに子ファイルの最初のセクションタイトルをラベルとして使用するかどうか。`True` の場合ラベルが使用される。
    :param verbose: bool - 詳細ログを出力するかどうか。デフォルトは `False`。
    :returns: str - globパターンが展開された後のreStructuredTextファイルの内容。
    """
    lines = parent_rst.read_text(encoding="utf-8").splitlines()
    parent_dir = parent_rst.parent

    out: List[str] = []
    in_toctree = False
    toctree_indent = 0
    seen_paths_in_block = set()

    for lineno, line in enumerate(lines, start=1):
        stripped = line.strip()
        current_indent = indent_width(line)

        # toctree 開始
        if re.match(r"\s*\.\.\s+toctree::", line):
            in_toctree = True
            toctree_indent = current_indent
            seen_paths_in_block = set()
            log(verbose, f"entered toctree at line {lineno}, indent={toctree_indent}")
            out.append(line)
            continue

        if in_toctree:
            # toctree の本文ブロックを抜けたか判定
            # 空行はブロックの一部として許可する。
            if stripped != "" and current_indent <= toctree_indent:
                in_toctree = False
                log(verbose, f"left toctree before line {lineno}")
                # fall through to normal line handling
            else:
                # toctree 内のオプション行
                if stripped.startswith(":"):
                    out.append(line)
                    continue

                # toctree 内の空行
                if stripped == "":
                    out.append(line)
                    continue

                # toctree 内のエントリ行
                pattern = stripped
                paths = glob_with_extensions(parent_dir, pattern, verbose=verbose)
                paths = [p for p in paths if p.suffix in [".rst", ".md"]]

                # 重複除去（出現順保持）
                unique_paths: List[Path] = []
                local_seen = set()
                for p in paths:
                    resolved = p.resolve()
                    if resolved not in local_seen:
                        local_seen.add(resolved)
                        unique_paths.append(p)
                paths = unique_paths

                if sort_flag:
                    paths = sorted(paths, key=lambda p: p.name)

                if not paths:
                    warn(f"No matches for toctree entry {pattern!r} in {parent_rst} (line {lineno})")
                    continue

                for p in paths:
                    rel = p.relative_to(parent_dir).as_posix()
                    if rel in seen_paths_in_block:
                        log(verbose, f"skip duplicated entry: {rel}")
                        continue
                    seen_paths_in_block.add(rel)

                    if label_flag:
                        title = extract_first_section_title(p, verbose=verbose)
                        if title:
                            out.append(f"   {title} <{rel}>")
                            log(verbose, f"added labeled entry: {title} <{rel}>")
                            continue
                        log(verbose, f"title not found for {rel}, fallback to plain path")

                    out.append(f"   {rel}")
                    log(verbose, f"added entry: {rel}")

                # 元の glob/path 行は出力しない
                continue

        # 通常行
        out.append(line)

    if in_toctree:
        log(verbose, "reached EOF while still inside toctree block")

    return "\n".join(out) + "\n"


def main():
    """コマンドライン引数に基づいてSphinx toctreeのglobパターンを展開するメイン処理。

    詳細説明:
        指定された親reStructuredTextファイルを読み込み、`.. toctree::` ディレクティブ内の
        globパターンを展開します。展開された内容は、元のファイルに書き戻すか、
        標準出力に出力されます。バックアップ作成、ソート、ラベル付け、詳細ログの
        オプションをサポートします。

    :returns: None
    """
    parser = argparse.ArgumentParser(description="Expand Sphinx toctree glob patterns.")
    parser.add_argument("parent", help="Parent .rst file")
    parser.add_argument("--sort", type=int, default=1, help="1=sort entries, 0=keep original glob order")
    parser.add_argument("--label", type=int, default=0, help="1=use section title labels, 0=paths only")
    parser.add_argument("--backup", type=int, default=1, help="1=create backup, 0=do not create backup")
    parser.add_argument("--verbose", type=int, default=0, help="1=verbose log to stderr, 0=quiet")
    args = parser.parse_args()

    parent_rst = Path(args.parent).resolve()

    if not parent_rst.exists():
        raise FileNotFoundError(f"Parent file not found: {parent_rst}")

    if args.backup == 1:
        backup_path = parent_rst.with_suffix(parent_rst.suffix + ".backup")
        shutil.copy2(parent_rst, backup_path)
        log(args.verbose == 1, f"backup created: {backup_path}")

    original = parent_rst.read_text(encoding="utf-8")
    result = expand_glob_in_toctree(
        parent_rst,
        sort_flag=(args.sort == 1),
        label_flag=(args.label == 1),
        verbose=(args.verbose == 1),
    )

    if result != original:
        parent_rst.write_text(result, encoding="utf-8")
        log(args.verbose == 1, f"updated file: {parent_rst}")
    else:
        log(args.verbose == 1, f"no changes detected: {parent_rst}")

    print(result)


if __name__ == "__main__":
    main()