"""Sort a messy folder into subfolders by file type.

Fixes over the naive version: it never silently overwrites a file that already
exists at the destination, it skips the category folders it creates (so a second
run is a no-op rather than a reshuffle), and it previews by default.

    python3 downloads_sorter.py ~/Downloads          # preview
    python3 downloads_sorter.py ~/Downloads --apply  # commit

Standard library only.
"""

from __future__ import annotations

import argparse
import shutil
import sys
from pathlib import Path

RULES: dict[str, set[str]] = {
    "Documents":  {".pdf", ".docx", ".doc", ".txt", ".xlsx", ".xls", ".csv", ".pptx"},
    "Images":     {".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic", ".svg"},
    "Video":      {".mp4", ".mkv", ".avi", ".mov", ".webm"},
    "Audio":      {".mp3", ".wav", ".m4a", ".flac"},
    "Archives":   {".zip", ".rar", ".7z", ".tar", ".gz"},
    "Installers": {".dmg", ".pkg", ".exe", ".msi"},
}

EXT_TO_CATEGORY = {ext: cat for cat, exts in RULES.items() for ext in exts}


def unique_destination(target: Path) -> Path:
    """Never overwrite: append ' (2)', ' (3)', ... until the name is free."""
    if not target.exists():
        return target
    stem, suffix, parent = target.stem, target.suffix, target.parent
    counter = 2
    while True:
        candidate = parent / f"{stem} ({counter}){suffix}"
        if not candidate.exists():
            return candidate
        counter += 1


def plan_moves(folder: Path) -> list[tuple[Path, Path]]:
    plan: list[tuple[Path, Path]] = []
    for path in sorted(folder.iterdir()):
        # Skip directories, including the category folders from a previous run.
        if path.is_dir() or path.name.startswith("."):
            continue
        category = EXT_TO_CATEGORY.get(path.suffix.lower())
        if not category:
            continue
        plan.append((path, folder / category / path.name))
    return plan


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Sort a folder into subfolders by file type.")
    parser.add_argument("folder", type=Path, help="Folder to tidy")
    parser.add_argument("--apply", action="store_true", help="Actually move (default: dry run)")
    args = parser.parse_args(argv)

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

    plan = plan_moves(folder)
    if not plan:
        print("Nothing to sort.")
        return 0

    moved = 0
    for source, target in plan:
        final = unique_destination(target)
        note = "" if final == target else "  (renamed, name was taken)"
        print(f"{source.name}  ->  {final.parent.name}/{final.name}{note}")
        if args.apply:
            final.parent.mkdir(exist_ok=True)
            shutil.move(str(source), str(final))
            moved += 1

    if args.apply:
        print(f"\nMoved {moved} file(s).")
    else:
        print(f"\nDry run: {len(plan)} file(s) would move. Re-run with --apply.")
    return 0


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