"""Completed example: survey a practice folder and file it safely.

Practical Workflow Automation, modules 1 and 2, on real files. This is the
finished version of starter_organise_inbox.py. Read it after you have
attempted the starter.

    py complete_organise_inbox.py --folder inbox            dry run (default)
    py complete_organise_inbox.py --folder inbox --apply    do it

It never deletes anything. Files it sets aside are moved, not removed.
"""

import argparse
import hashlib
import os
import pathlib
import sys


def content_hash(path):
    """SHA-256 of a file's bytes, read in chunks so size does not matter."""
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(65536), b""):
            digest.update(chunk)
    return digest.hexdigest()


def filed_name(path):
    """The filed name: lowercase, spaces to hyphens, suffix lowercased."""
    return path.stem.lower().replace(" ", "-") + path.suffix.lower()


def survey(folder):
    """Return (files, folders) for one folder, sorted, files only at this level.

    is_dir() asks the filesystem what the entry is; the name is not a claim
    about anything. iterdir() does not descend, which is deliberate: the
    question is what is in this folder, not what is underneath it.
    """
    files, folders = [], []
    for entry in sorted(folder.iterdir()):
        if entry.is_dir():
            folders.append(entry.name)
        else:
            files.append(entry.name)
    return files, folders


def plan_renames(files):
    """Return (moves, collisions) without touching anything.

    The collision check runs over the finished plan, because that is the only
    place it exists: a per-file exists() test cannot see that a later file
    wants the name this one is about to take.
    """
    moves = sorted((path.name, filed_name(path)) for path in files)
    wanted_by = {}
    for source, target in moves:
        wanted_by.setdefault(target, []).append(source)
    collisions = sorted(target for target, sources in wanted_by.items() if len(sources) > 1)
    return moves, collisions


def plan_duplicates(folder, files):
    """Return (keep, duplicates) by comparing contents, not names.

    Keeping the first name in each sorted group is a policy, not a fact, and
    it is one line so that somebody can disagree with it.
    """
    groups = {}
    for name in sorted(files):
        groups.setdefault(content_hash(folder / name), []).append(name)
    keep, duplicates = [], []
    for names in sorted(groups.values()):
        keep.append(names[0])
        duplicates.extend(names[1:])
    return sorted(keep), sorted(duplicates)


def main(argv=None):
    parser = argparse.ArgumentParser(description="Survey and file a practice inbox.")
    parser.add_argument("--folder", required=True, help="the practice folder to work in")
    parser.add_argument(
        "--apply",
        action="store_true",
        help="actually move files; without this the program only reports",
    )
    args = parser.parse_args(argv)

    folder = pathlib.Path(args.folder).resolve()
    if not folder.is_dir():
        print(f"refused: {folder} is not a directory")
        return 2

    files, folders = survey(folder)
    print(f"surveyed {folder}")
    print(f"  files: {len(files)}")
    print(f"  folders: {len(folders)}")

    keep, duplicates = plan_duplicates(folder, files)
    moves, collisions = plan_renames([folder / name for name in keep])

    for name in duplicates:
        print(f"  would quarantine {name} (same contents as a file being kept)")
    for source, target in moves:
        if source != target:
            print(f"  would rename {source} -> {target}")

    if collisions:
        for target in collisions:
            print(f"refused: more than one file wants the name {target}")
        return 2

    if not args.apply:
        print("dry run: nothing was changed. Add --apply to do it.")
        return 0

    quarantine = folder.parent / "quarantine"
    quarantine.mkdir(exist_ok=True)
    for name in duplicates:
        os.replace(folder / name, quarantine / name)
    for source, target in moves:
        if source != target:
            os.replace(folder / source, folder / target)
    print(f"applied: {len(duplicates)} quarantined, {sum(1 for s, t in moves if s != t)} renamed")
    return 0


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