#!/usr/bin/env python3
"""
rename_crossplatform_safe.py

Recursively (or non-recursively) rename files so that each name:
   Contains only basic ASCII (transliteration or removal)
   Is safe for Windows (no illegal chars, no reserved device names)
   Is safe for Linux ext4 (no '/' or NUL, =255 bytes, optional dot-trim)

Optional flags:
  --remove       strip non-ASCII characters instead of transliterating
  --keep-dot     preserve a leading '.' (otherwise it is stripped)
  --dry-run      show the renames that *would* be performed without touching the filesystem
  -r / --recursive  walk sub-directories and rename files there as well
"""

import argparse
import os
import sys
import unicodedata
from pathlib import Path

# ----------------------------------------------------------------------
# 1??  ASCII conversion helpers
# ----------------------------------------------------------------------
def transliterate(text: str) -> str:
    """Normalize Unicode ? closest ASCII (e.g. ι ? e)."""
    normalized = unicodedata.normalize("NFKD", text)
    ascii_bytes = normalized.encode("ascii", "ignore")
    return ascii_bytes.decode("ascii")


def remove_non_ascii(text: str) -> str:
    """Drop every character whose code point > 127."""
    return "".join(ch for ch in text if ord(ch) < 128)


# ----------------------------------------------------------------------
# 2??  Windows-specific sanitisation
# ----------------------------------------------------------------------
WIN_FORBIDDEN_CHARS = set(r'<>:"/\\|?*')
WIN_CONTROL_RANGE = set(chr(i) for i in range(32))
WIN_RESERVED_NAMES = {
    "CON", "PRN", "AUX", "NUL",
    *(f"COM{i}" for i in range(1, 10)),
    *(f"LPT{i}" for i in range(1, 10)),
}


def sanitize_windows_name(name: str) -> str:
    """Strip illegal Windows characters and protect reserved device names."""
    cleaned = [
        ch for ch in name
        if ch not in WIN_FORBIDDEN_CHARS and ch not in WIN_CONTROL_RANGE
    ]
    cleaned_name = "".join(cleaned).rstrip(" .")  # trim trailing space/period

    if cleaned_name.upper() in WIN_RESERVED_NAMES:
        cleaned_name += "_"

    return cleaned_name or "file"


# ----------------------------------------------------------------------
# 3??  Linux ext4 sanitisation
# ----------------------------------------------------------------------
MAX_EXT4_NAME_LEN = 255  # bytes, not characters (UTF-8 aware)


def sanitize_linux_ext4_name(name: str, keep_leading_dot: bool) -> str:
    """
    Ensure the name complies with ext4 rules:
      * No '/' (path separator) and no NUL byte.
      * Length = 255 bytes when encoded as UTF-8.
      * Optionally strip a leading '.' to avoid hidden files.
    """
    name = name.replace("/", "").replace("\0", "")

    if not keep_leading_dot and name.startswith("."):
        name = name.lstrip(".")

    encoded = name.encode("utf-8")
    if len(encoded) > MAX_EXT4_NAME_LEN:
        truncated = encoded[:MAX_EXT4_NAME_LEN]
        name = truncated.decode("utf-8", "ignore")

    return name or "file"


# ----------------------------------------------------------------------
# 4??  Rename helper (dry-run aware)
# ----------------------------------------------------------------------
def perform_rename(src: Path, dst: Path, dry_run: bool) -> bool:
    """
    Rename src ? dst.
    Returns True if the operation succeeded (or would succeed in dry-run mode).
    """
    if dry_run:
        print(f"[DRY-RUN] Would rename: {src} ? {dst}")
        return True

    try:
        src.rename(dst)
        print(f"Renamed: {src} ? {dst}")
        return True
    except FileExistsError:
        print(f"[??] Skipping: target already exists ? {dst}", file=sys.stderr)
    except PermissionError:
        print(f"[?] Permission error while renaming {src}", file=sys.stderr)
    except OSError as exc:
        print(f"[?] Unexpected error: {exc}", file=sys.stderr)
    return False


# ----------------------------------------------------------------------
# 5??  Core processing for a single file
# ----------------------------------------------------------------------
def process_file(
    entry: Path,
    args: argparse.Namespace,
) -> None:
    """
    Apply the full sanitisation pipeline to a single file and rename it
    (or simulate the rename when dry-run is active).
    """
    original_name = entry.name
    stem, suffix = os.path.splitext(original_name)

    # 1?? ASCII conversion
    ascii_stem = remove_non_ascii(stem) if args.remove else transliterate(stem)

    # 2?? Windows sanitisation
    win_safe = sanitize_windows_name(ascii_stem)

    # 3?? Linux ext4 sanitisation
    final_stem = sanitize_linux_ext4_name(win_safe, keep_leading_dot=args.keep_dot)

    new_name = f"{final_stem}{suffix}"
    new_path = entry.with_name(new_name)

    # Nothing to change?
    if new_path == entry:
        return

    # Conflict check (same directory)
    if new_path.exists():
        print(
            f"[??] Conflict: {new_path} already exists  leaving {original_name} unchanged.",
            file=sys.stderr,
        )
        return

    perform_rename(entry, new_path, dry_run=args.dry_run)


# ----------------------------------------------------------------------
# 6??  Main routine
# ----------------------------------------------------------------------
def main() -> None:
    parser = argparse.ArgumentParser(
        description="Rename files (optionally recursively) to ASCII-only, "
                    "cross-platform safe names (Windows + Linux ext4)."
    )
    parser.add_argument(
        "--remove",
        action="store_true",
        help="Remove non-ASCII characters instead of transliterating them.",
    )
    parser.add_argument(
        "--keep-dot",
        action="store_true",
        help="Preserve a leading '.' (do NOT strip it). Useful if you really need hidden files.",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Show what would be renamed without touching the filesystem.",
    )
    parser.add_argument(
        "-r",
        "--recursive",
        action="store_true",
        help="Walk sub-directories and rename files there as well.",
    )
    args = parser.parse_args()

    cwd = Path.cwd()
    print(f"Scanning directory: {cwd}")

    if args.recursive:
        # Walk the tree depth-first.  We only rename files; directories stay untouched.
        for root, _, _ in os.walk(cwd):
            for entry in Path(root).iterdir():
                if entry.is_file():
                    process_file(entry, args)
    else:
        # Non-recursive: only the top-level entries.
        for entry in cwd.iterdir():
            if entry.is_file():
                process_file(entry, args)

    if args.dry_run:
        print("\n? Dry-run completed  no files were modified.")
    else:
        print("\n? Done.")


if __name__ == "__main__":
    main()