"""资料整理脚本：删除非视频/图片文件并清理空文件夹。

扫描脚本所在目录及其所有子目录，保留常见视频和图片资源文件，
删除其余文件，随后清理因删除而产生的空文件夹。

用法:
    python organize_media.py            # 预览 + 确认后执行
    python organize_media.py --yes      # 跳过确认直接执行（危险）
    python organize_media.py --dry-run  # 仅预览，不执行任何删除

参数:
    --yes      跳过交互确认，直接执行删除
    --dry-run  仅预览将要删除的内容，不实际删除任何文件或文件夹
"""

from __future__ import annotations

import argparse
import logging
import sys
from pathlib import Path

# 保留的图片扩展名（小写，含点号）
IMAGE_EXTENSIONS: frozenset[str] = frozenset({
    ".jpg", ".jpeg",".webp",".zip",".py",
})

# 保留的视频扩展名（小写，含点号）
VIDEO_EXTENSIONS: frozenset[str] = frozenset({
    ".mp4", ".avi", ".mkv", ".mov", ".wmv",
    ".flv", ".webm", ".mpeg", ".mpg", ".3gp",
    ".m4v", ".ts",
})

# 受保护的目录名（不扫描、不删除其中内容）
# .git 为版本控制元数据，删除会破坏仓库
# SP 为用户指定保留目录，其下所有文件与子目录均不删除
PROTECTED_DIRS: frozenset[str] = frozenset({".git", "SP"})


def setup_logger() -> logging.Logger:
    """配置并返回日志记录器。

    返回值:
        配置好的 Logger 实例，输出到标准错误流。
    """
    logger = logging.getLogger("organize_media")
    if logger.handlers:
        return logger
    logger.setLevel(logging.INFO)
    handler = logging.StreamHandler(sys.stderr)
    handler.setFormatter(logging.Formatter("%(message)s"))
    logger.addHandler(handler)
    return logger


def is_media_file(path: Path) -> bool:
    """判断给定路径是否为应保留的媒体文件（图片或视频）。

    参数:
        path: 待判断的文件路径。

    返回值:
        True 表示文件扩展名属于保留的图片或视频格式，否则 False。
    """
    suffix = path.suffix.lower()
    return suffix in IMAGE_EXTENSIONS or suffix in VIDEO_EXTENSIONS


def scan_deletable_files(
    root: Path, script_path: Path, logger: logging.Logger
) -> list[Path]:
    """扫描根目录下所有待删除的文件。

    遍历 root 及其所有子目录，跳过受保护目录，收集扩展名不在
    保留清单内且非脚本自身的文件路径。

    参数:
        root: 扫描的根目录路径。
        script_path: 脚本自身路径，需保护不被删除。
        logger: 日志记录器。

    返回值:
        待删除文件路径列表，按路径排序。

    抛出异常:
        OSError: 当目录遍历遇到无法访问的路径时记录并跳过。
    """
    to_delete: list[Path] = []
    script_resolved = script_path.resolve()

    for path in root.rglob("*"):
        # 跳过受保护目录内的任何条目
        if any(part in PROTECTED_DIRS for part in path.parts):
            continue

        if not path.is_file():
            continue

        # 保护脚本自身
        if path.resolve() == script_resolved:
            continue

        if not is_media_file(path):
            to_delete.append(path)

    to_delete.sort()
    return to_delete


def scan_empty_dirs(
    root: Path, logger: logging.Logger
) -> list[Path]:
    """扫描根目录下所有空文件夹（按深度从深到浅排序）。

    遍历所有子目录，筛选出不含任何文件的目录。按路径深度降序
    排列，以便从最深层开始逐级删除，使上级目录也能被清理。

    参数:
        root: 扫描的根目录路径。
        logger: 日志记录器。

    返回值:
        空文件夹路径列表，按深度从深到浅排序。

    抛出异常:
        OSError: 当目录遍历遇到无法访问的路径时记录并跳过。
    """
    empty_dirs: list[Path] = []

    for path in root.rglob("*"):
        if any(part in PROTECTED_DIRS for part in path.parts):
            continue

        if not path.is_dir():
            continue

        # 目录为空（无任何子条目）
        try:
            next(path.iterdir())
        except StopIteration:
            empty_dirs.append(path)
        except OSError as exc:
            logger.error("无法读取目录 %s: %s", path, exc)

    # 按路径深度降序：深层优先删除，删除后上层可能也变空
    empty_dirs.sort(key=lambda p: len(p.parts), reverse=True)
    return empty_dirs


def preview(
    files_to_delete: list[Path],
    dirs_to_delete: list[Path],
    logger: logging.Logger,
) -> None:
    """打印待删除文件和空文件夹的预览清单。

    参数:
        files_to_delete: 待删除文件路径列表。
        dirs_to_delete: 待删除空文件夹路径列表。
        logger: 日志记录器。
    """
    logger.info("=" * 60)
    logger.info("预览：将删除 %d 个文件", len(files_to_delete))
    logger.info("-" * 60)
    for path in files_to_delete:
        logger.info("  [文件] %s", path)

    logger.info("-" * 60)
    logger.info("预览：将清理 %d 个空文件夹", len(dirs_to_delete))
    logger.info("-" * 60)
    for path in dirs_to_delete:
        logger.info("  [空夹] %s", path)

    logger.info("=" * 60)
    logger.info(
        "保留格式: 图片%s", ", ".join(sorted(IMAGE_EXTENSIONS))
    )
    logger.info(
        "保留格式: 视频%s", ", ".join(sorted(VIDEO_EXTENSIONS))
    )
    logger.info("受保护目录: %s", ", ".join(sorted(PROTECTED_DIRS)))
    logger.info("=" * 60)


def execute_deletion(
    files_to_delete: list[Path],
    dirs_to_delete: list[Path],
    logger: logging.Logger,
) -> tuple[int, int]:
    """执行文件和空文件夹的删除操作。

    逐个删除文件，再从最深层空文件夹开始逐级删除。遇到错误时
    记录日志并继续处理后续条目，不中断整体流程。

    参数:
        files_to_delete: 待删除文件路径列表。
        dirs_to_delete: 待删除空文件夹路径列表（已按深度降序）。
        logger: 日志记录器。

    返回值:
        元组 (成功删除文件数, 成功删除文件夹数)。
    """
    deleted_files = 0
    deleted_dirs = 0

    for path in files_to_delete:
        try:
            path.unlink()
            deleted_files += 1
            logger.info("已删除文件: %s", path)
        except OSError as exc:
            logger.error("删除文件失败 %s: %s", path, exc)

    for path in dirs_to_delete:
        # 删除前再次确认仍为空（可能因前序删除产生新空目录）
        try:
            next(path.iterdir())
            continue
        except StopIteration:
            pass
        try:
            path.rmdir()
            deleted_dirs += 1
            logger.info("已清理空夹: %s", path)
        except OSError as exc:
            logger.error("删除空文件夹失败 %s: %s", path, exc)

    return deleted_files, deleted_dirs


def parse_args(argv: list[str]) -> argparse.Namespace:
    """解析命令行参数。

    参数:
        argv: 命令行参数列表。

    返回值:
        解析后的参数命名空间。
    """
    parser = argparse.ArgumentParser(
        description="删除非视频/图片文件并清理空文件夹，整理当前目录资料。"
    )
    parser.add_argument(
        "--yes",
        action="store_true",
        help="跳过交互确认，直接执行删除（危险）。",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="仅预览将要删除的内容，不实际删除任何文件或文件夹。",
    )
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    """脚本主入口。

    执行扫描、预览、确认、删除的完整流程。返回退出码供调用方判断。

    参数:
        argv: 可选的命令行参数列表，默认从 sys.argv 读取。

    返回值:
        0 表示成功完成，1 表示用户取消或执行出错。
    """
    args = parse_args(argv if argv is not None else sys.argv[1:])
    logger = setup_logger()

    script_path = Path(__file__).resolve()
    root = script_path.parent

    logger.info("扫描目录: %s", root)
    logger.info("脚本路径: %s", script_path)
    logger.info("")

    files_to_delete = scan_deletable_files(root, script_path, logger)
    dirs_to_delete = scan_empty_dirs(root, logger)

    if not files_to_delete and not dirs_to_delete:
        logger.info("无需整理：没有待删除的文件或空文件夹。")
        return 0

    preview(files_to_delete, dirs_to_delete, logger)

    if args.dry_run:
        logger.info("已启用 --dry-run，不执行任何删除。")
        return 0

    if not args.yes:
        logger.info("")
        try:
            answer = input("确认执行删除？此操作不可逆 [y/N]: ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            logger.info("已取消。")
            return 1
        if answer not in ("y", "yes"):
            logger.info("已取消，未做任何修改。")
            return 1

    logger.info("")
    logger.info("开始执行删除...")
    deleted_files, deleted_dirs = execute_deletion(
        files_to_delete, dirs_to_delete, logger
    )

    # 删除文件后可能产生新的空文件夹，二次扫描清理
    logger.info("")
    logger.info("二次扫描新增空文件夹...")
    new_empty = scan_empty_dirs(root, logger)
    extra_dirs = 0
    if new_empty:
        for path in new_empty:
            try:
                path.rmdir()
                extra_dirs += 1
                logger.info("已清理空夹: %s", path)
            except OSError as exc:
                logger.error("删除空文件夹失败 %s: %s", path, exc)

    logger.info("")
    logger.info(
        "完成：删除文件 %d 个，清理空文件夹 %d 个（含二次扫描 %d 个）。",
        deleted_files,
        deleted_dirs + extra_dirs,
        extra_dirs,
    )
    return 0


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