#!/usr/bin/env python3
"""Require a notes file when a file under ``advanced_search_system/`` is deleted.

Any commit that deletes a ``.py`` file under
``src/local_deep_research/advanced_search_system/`` — or renames one out of
that tree, which removes it from the tracked module — must also add or
modify a ``.md`` file under ``docs/strategies/deleted/`` in the same commit.
Rationale and template live in ``docs/strategies/deleted/README.md``.

Exempt files whose deletion does not by itself remove a component: package
``__init__.py`` aggregators. The exemption list is intentionally narrow; if
a new infra file is added (e.g. a base class in a new location), update
``EXEMPT_FILENAMES`` below. ``base_*.py`` files are NOT exempt — deleting
a base class is a significant refactor that does warrant a notes file.
"""

import subprocess
import sys
from pathlib import PurePosixPath

SCOPE_DIR = "src/local_deep_research/advanced_search_system/"
DOCS_DIR = "docs/strategies/deleted/"
EXEMPT_FILENAMES = {"__init__.py"}

# Suggested notes-file name format. PR number isn't known at commit time
# so we accept a placeholder the author fills in before pushing.
SUGGESTED_FILENAME = "pr-<number>-<short-slug>.md"

TEMPLATE = """\
# PR #<n> — <title>

Components deleted in PR #<n> (see that PR for the full pre-deletion
code — this file only summarises what was novel).

## Component: `<ClassName>`

- File deleted: `<path>` (<N> LOC at deletion).
- Reachability: <e.g. "not in factory; only referenced by its own test">.
- Closest reachable successor: `<ClassName>` (`<path>`, factory key `"<X>"`).

### Useful ideas from the pre-deletion version

- **<short name>** — <1-2 sentences on what it did, why it was
  distinctive, whether it was validated>.
- **<short name>** — <...>.

### Why deletion was safe

<2-3 sentences mapping distinctive features to the successor, or
flagging at-risk items and why losing them is acceptable.>

### Recovery path

<1-2 sentences. Prefer "add a flag on the existing class" over
"restore the deleted file".>
"""


def staged_changes():
    """Yield ``(code, old_path, new_path)`` for each staged change.

    For add/modify/delete, ``old_path == new_path``. For rename (R) and
    copy (C), they differ; uniform unpacking lets callers use one code
    path. See ``git diff --name-status`` for the format.
    """
    result = subprocess.run(
        ["git", "diff", "--cached", "--name-status"],
        capture_output=True,
        text=True,
        check=True,
    )
    for line in result.stdout.splitlines():
        if not line.strip():
            continue
        parts = line.split("\t")
        if len(parts) < 2:
            continue
        code = parts[0][
            0
        ]  # A / M / D / R / C (R and C carry a similarity score suffix)
        if code in ("R", "C") and len(parts) >= 3:
            yield code, parts[1], parts[2]
        else:
            yield code, parts[1], parts[1]


def _is_exempt(path: str) -> bool:
    # .lower() defends against case-insensitive filesystems (macOS, Windows).
    return PurePosixPath(path).name.lower() in EXEMPT_FILENAMES


def main() -> int:
    deleted_files = []
    doc_changes = []

    for code, old_path, new_path in staged_changes():
        # A rename out of SCOPE_DIR removes the component from the tracked
        # tree even though the file survives elsewhere — treat as deletion.
        # Renames within SCOPE_DIR are legitimate refactors; don't fire.
        is_deletion_from_scope = (
            code == "D" and old_path.startswith(SCOPE_DIR)
        ) or (
            code == "R"
            and old_path.startswith(SCOPE_DIR)
            and not new_path.startswith(SCOPE_DIR)
        )
        if (
            is_deletion_from_scope
            and old_path.endswith(".py")
            and not _is_exempt(old_path)
        ):
            deleted_files.append(old_path)

        if (
            code in ("A", "M")
            and new_path.startswith(DOCS_DIR)
            and new_path.endswith(".md")
            and PurePosixPath(new_path).name.lower() != "readme.md"
        ):
            doc_changes.append(new_path)

    if deleted_files and not doc_changes:
        err = sys.stderr
        print(
            "\n  \033[31madvanced_search_system deletion without documentation\033[0m",
            file=err,
        )
        print("  " + "=" * 50, file=err)
        print(
            "\n  You are deleting (or renaming out of scope) these files:",
            file=err,
        )
        for path in deleted_files:
            print(f"    - {path}", file=err)
        print(
            f"\n  To unblock this commit, add a notes file at\n"
            f"      {DOCS_DIR}{SUGGESTED_FILENAME}\n"
            f"  (or extend an existing one under {DOCS_DIR}) with the following shape:\n",
            file=err,
        )
        for line in TEMPLATE.splitlines():
            print(f"      {line}" if line else "", file=err)
        print(
            '\n  Each bullet in "Useful ideas" should answer, in 1-2',
            file=err,
        )
        print(
            "  sentences:",
            file=err,
        )
        print(
            "    - what the component did that was different from the successor,",
            file=err,
        )
        print(
            "    - why that difference was interesting (heuristic, tuning, prompt",
            file=err,
        )
        print(
            "      trick, interface gap),",
            file=err,
        )
        print(
            "    - and whether it was validated or exploratory.",
            file=err,
        )
        print(
            "\n  Do NOT paste verbatim prompts, docstrings, or code blocks —",
            file=err,
        )
        print(
            "  git already stores them via the deletion PR/commit. Link, don't",
            file=err,
        )
        print(
            "  duplicate.",
            file=err,
        )
        print(
            f"\n  Full convention + rationale: {DOCS_DIR}README.md\n",
            file=err,
        )
        return 1
    return 0


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