"""
Безопасный рекурсивный поиск файлов по нескольким путям.

Аналог `Get-AllFilesSafe` из dspscanner.ps1: пропускает системные/скрытые
каталоги, временные файлы (~, ~$), дубликаты путей и файлы нулевого размера,
но использует `os.walk` с модификацией `dirnames` "на лету" — это даёт
тот же эффект, что и специальная защита от AppData/$RECYCLE.BIN в
оригинале, без необходимости различать "профиль пользователя" и "корень
диска" отдельными эвристиками.
"""
from __future__ import annotations

import os
from pathlib import Path
from threading import Event
from typing import Callable, Iterable, Iterator, List, Optional, Set

from app.config import IGNORED_FOLDER_NAMES, TEMP_FILE_PREFIXES, FileEntry


def _is_ignored_dir(name: str) -> bool:
    return name.lower() in IGNORED_FOLDER_NAMES or name.startswith(".")


def is_temp_file(filename: str) -> bool:
    return filename.startswith(TEMP_FILE_PREFIXES)


def iter_files_safe(
    paths: Iterable[str],
    extensions: Set[str],
    max_size_bytes: int,
    cancel_event: Optional[Event] = None,
    on_dir_visited: Optional[Callable[[str], None]] = None,
    on_skip: Optional[Callable[[str, str], None]] = None,
) -> Iterator[FileEntry]:
    """Генератор найденных файлов. `on_skip(path, reason)` вызывается для
    файлов, отброшенных по причинам (temp/duplicate/too_big/empty)."""

    seen: Set[str] = set()
    normalized_exts = {e.lower() for e in extensions}

    for raw_path in paths:
        path_str = raw_path.strip()
        if not path_str:
            continue
        root_path = Path(path_str)
        if not root_path.exists():
            continue

        if on_dir_visited:
            on_dir_visited(str(root_path))

        if root_path.is_file():
            entry = _make_entry(root_path, normalized_exts, max_size_bytes, seen, on_skip)
            if entry:
                yield entry
            continue

        for dirpath, dirnames, filenames in os.walk(root_path, topdown=True, onerror=lambda e: None):
            if cancel_event is not None and cancel_event.is_set():
                return

            dirnames[:] = [d for d in dirnames if not _is_ignored_dir(d)]

            for filename in filenames:
                if cancel_event is not None and cancel_event.is_set():
                    return

                if is_temp_file(filename):
                    if on_skip:
                        on_skip(os.path.join(dirpath, filename), "temp")
                    continue

                full_path = Path(dirpath) / filename
                entry = _make_entry(full_path, normalized_exts, max_size_bytes, seen, on_skip)
                if entry:
                    yield entry


def _make_entry(
    path: Path,
    extensions: Set[str],
    max_size_bytes: int,
    seen: Set[str],
    on_skip: Optional[Callable[[str, str], None]],
) -> Optional[FileEntry]:
    ext = path.suffix.lower()
    if ext not in extensions:
        return None

    key = str(path.resolve()).lower()
    if key in seen:
        if on_skip:
            on_skip(str(path), "duplicate")
        return None
    seen.add(key)

    try:
        stat = path.stat()
    except OSError:
        if on_skip:
            on_skip(str(path), "inaccessible")
        return None

    if stat.st_size == 0:
        if on_skip:
            on_skip(str(path), "empty")
        return None

    if stat.st_size > max_size_bytes:
        if on_skip:
            on_skip(str(path), "too_big")
        return None

    return FileEntry(path=path, size=stat.st_size, modified=stat.st_mtime, extension=ext)


def is_file_accessible(path: Path) -> bool:
    try:
        with open(path, "rb"):
            return True
    except (PermissionError, OSError):
        return False
