"""Batch-rename files with a sequence prefix — safely.

The naive version of this script has three bugs that bite in production:

  1. It renames while iterating the same directory listing, so on some
     filesystems freshly renamed files get visited again.
  2. It is not idempotent. Run it twice and you get 'rapor_0_rapor_0_x.pdf'.
  3. It silently destroys data when the target name already exists.

This version snapshots the listing first, skips files that already carry the
prefix, refuses to overwrite, and defaults to a dry run so you can look before
anything moves.

    python3 file_organiser.py ./reports --prefix rapor          # preview
    python3 file_organiser.py ./reports --prefix rapor --apply  # commit

Standard library only.
"""

from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path


def already_prefixed(name: str, prefix: str) -> bool:
    return re.match(rf"^{re.escape(prefix)}_\d+_", name) is not None


def plan_renames(folder: Path, prefix: str, start: int) -> list[tuple[Path, Path]]:
    """Build the full rename plan before touching anything on disk."""
    # Snapshot and sort so the numbering is deterministic across runs.
    entries = sorted(p for p in folder.iterdir() if p.is_file())

    plan: list[tuple[Path, Path]] = []
    counter = start
    for path in entries:
        if already_prefixed(path.name, prefix):
            continue
        target = folder / f"{prefix}_{counter}_{path.name}"
        plan.append((path, target))
        counter += 1
    return plan


def conflicts(plan: list[tuple[Path, Path]]) -> list[Path]:
    """Targets that already exist, or that two sources both want."""
    existing_targets = {src for src, _ in plan}
    seen: set[Path] = set()
    clashes: list[Path] = []
    for _, target in plan:
        # A target that exists and is not itself being renamed away is a clash.
        if (target.exists() and target not in existing_targets) or target in seen:
            clashes.append(target)
        seen.add(target)
    return clashes


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Batch-rename files with a sequence prefix.")
    parser.add_argument("folder", type=Path, help="Directory to process")
    parser.add_argument("--prefix", default="rapor", help="Prefix to apply (default: rapor)")
    parser.add_argument("--start", type=int, default=1, help="First sequence number")
    parser.add_argument("--apply", action="store_true", help="Actually rename (default: dry run)")
    args = parser.parse_args(argv)

    folder: Path = args.folder
    if not folder.is_dir():
        print(f"{folder} is not a directory", file=sys.stderr)
        return 1

    plan = plan_renames(folder, args.prefix, args.start)
    if not plan:
        print("Nothing to do — every file is already prefixed.")
        return 0

    clashes = conflicts(plan)
    if clashes:
        print("Refusing to run, these targets would be overwritten:", file=sys.stderr)
        for target in clashes:
            print(f"  {target.name}", file=sys.stderr)
        return 1

    for source, target in plan:
        print(f"{source.name}  ->  {target.name}")

    if not args.apply:
        print(f"\nDry run: {len(plan)} file(s) would be renamed. Re-run with --apply.")
        return 0

    for source, target in plan:
        source.rename(target)
    print(f"\nRenamed {len(plan)} file(s).")
    return 0


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