#!/usr/bin/env python3
"""Copy brain — сборка brain-инфраструктуры Claude Code в один файл.

Рекурсивно находит все файлы, управляющие поведением Claude
(CLAUDE.md, .claude/skills|agents|commands|hooks|plugins, settings*.json,
.mcp.json и любые прочие файлы внутри .claude/), и собирает их verbatim
в один markdown-файл с оглавлением.
"""
from __future__ import annotations

import argparse
from dataclasses import dataclass, field
from pathlib import Path

# Явные одиночные файлы в корне проекта.
ROOT_FILES = ["CLAUDE.md", ".mcp.json"]

# Поддиректории внутри .claude/, которые сканируются рекурсивно.
CLAUDE_DIRS = ["skills", "agents", "commands", "hooks", "plugins"]

# Settings-файлы внутри .claude/.
CLAUDE_SETTINGS = ["settings.json", "settings.local.json"]

# Расширения, которые считаем бинарными (вставляем пометку вместо содержимого).
BINARY_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".woff", ".woff2",
               ".ico", ".webp", ".mp4", ".mp3", ".so", ".dylib", ".dll"}

LANG_BY_EXT = {
    ".md": "markdown",
    ".json": "json",
    ".py": "python",
    ".sh": "bash",
    ".bash": "bash",
    ".js": "javascript",
    ".mjs": "javascript",
    ".cjs": "javascript",
    ".ts": "typescript",
    ".yaml": "yaml",
    ".yml": "yaml",
    ".toml": "toml",
    ".txt": "text",
}


@dataclass
class Component:
    """Логическая группа brain-файлов для оглавления."""

    key: str
    title: str
    files: list[Path] = field(default_factory=list)


def fence_lang(path: Path) -> str:
    return LANG_BY_EXT.get(path.suffix.lower(), "")


def is_binary(path: Path) -> bool:
    return path.suffix.lower() in BINARY_EXTS


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


def discover(root: Path) -> list[Component]:
    """Найти все brain-файлы, сгруппированные по компонентам.

    Возвращает список компонентов в стабильном порядке; у отсутствующих
    компонентов список files пустой (для пометки NOT FOUND).
    """
    components: list[Component] = []

    # 1. Корневые одиночные файлы.
    root_comp = Component("root", "Корневые файлы (CLAUDE.md, .mcp.json)")
    for name in ROOT_FILES:
        p = root / name
        if p.is_file():
            root_comp.files.append(p)
    components.append(root_comp)

    claude_dir = root / ".claude"

    # 2. Поддиректории .claude/ (рекурсивно).
    for sub in CLAUDE_DIRS:
        comp = Component(sub, f".claude/{sub}/")
        d = claude_dir / sub
        if d.is_dir():
            comp.files.extend(sorted(p for p in d.rglob("*") if p.is_file()))
        components.append(comp)

    # 3. Settings-файлы .claude/.
    settings_comp = Component("settings", ".claude/settings*.json")
    for name in CLAUDE_SETTINGS:
        p = claude_dir / name
        if p.is_file():
            settings_comp.files.append(p)
    components.append(settings_comp)

    # 4. Любые прочие файлы под .claude/, не попавшие в группы выше.
    claimed: set[Path] = set()
    for comp in components:
        claimed.update(comp.files)
    other_comp = Component("other", "Прочие файлы под .claude/")
    if claude_dir.is_dir():
        for p in sorted(claude_dir.rglob("*")):
            if p.is_file() and p not in claimed:
                other_comp.files.append(p)
    components.append(other_comp)

    return components


def _read_verbatim(path: Path) -> tuple[str, bool]:
    """Прочитать файл. Возвращает (содержимое, is_binary_or_unreadable)."""
    if is_binary(path):
        size = path.stat().st_size
        return (f"[BINARY FILE — {size} bytes, содержимое не включено]", True)
    try:
        return (path.read_text(encoding="utf-8"), False)
    except UnicodeDecodeError:
        size = path.stat().st_size
        return (f"[NON-UTF8 / BINARY FILE — {size} bytes, содержимое не включено]", True)


def _pick_fence(content: str) -> str:
    """Подобрать ограждение длиннее любой последовательности бэктиков внутри."""
    max_ticks = 0
    run = 0
    for ch in content:
        if ch == "`":
            run += 1
            max_ticks = max(max_ticks, run)
        else:
            run = 0
    return "`" * max(3, max_ticks + 1)


def render(root: Path, components: list[Component]) -> str:
    all_files = [f for c in components for f in c.files]
    lines: list[str] = []

    lines.append("# Claude Brain Export")
    lines.append("")
    lines.append(f"Корень проекта: `{root}`")
    lines.append(f"Всего файлов: **{len(all_files)}**")
    lines.append("")
    lines.append("## Оглавление")
    lines.append("")

    idx = 0
    for comp in components:
        lines.append(f"### {comp.title}")
        lines.append("")
        if not comp.files:
            lines.append("- **NOT FOUND**")
            lines.append("")
            continue
        for f in comp.files:
            idx += 1
            rel = _rel(root, f)
            lines.append(f"- [{rel}](#file-{idx})")
        lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("## Содержимое файлов")
    lines.append("")

    idx = 0
    for f in all_files:
        idx += 1
        rel = _rel(root, f)
        content, binary = _read_verbatim(f)
        lang = "" if binary else fence_lang(f)
        suffix = f.suffix.lower() or "(без расширения)"

        lines.append("---")
        lines.append("")
        lines.append(f'<a id="file-{idx}"></a>')
        lines.append(f"### {idx}. `{rel}`")
        lines.append("")
        lines.append(f"- Тип: `{suffix}`")
        lines.append(f"- Размер: {f.stat().st_size} bytes")
        lines.append("")

        fence = _pick_fence(content)
        body = content.rstrip("\n")
        lines.append(f"{fence}{lang}")
        lines.append(body)
        lines.append(fence)
        lines.append("")

    return "\n".join(lines) + "\n"


def main() -> int:
    ap = argparse.ArgumentParser(description="Copy brain export")
    ap.add_argument("--root", default=".", help="корень проекта для сканирования")
    ap.add_argument(
        "--out",
        default="outputs/claude-brain-export.md",
        help="путь итогового файла",
    )
    args = ap.parse_args()

    root = Path(args.root).resolve()
    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)

    components = discover(root)
    out.write_text(render(root, components), encoding="utf-8")

    total = sum(len(c.files) for c in components)
    abs_out = out.resolve()
    print(f"Bundled {total} files -> {abs_out}")
    print(f"Download: file://{abs_out}")
    return 0


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