"""
File collector for Gar
재귀적 파일 수집기. 보안 검사, 인터페이스 검사, 일반 검사에서 공통으로 사용
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Iterable

logger = logging.getLogger(__name__)


@dataclass
class FileCollectorConfig:
    """파일 수집 설정"""

    # 포함할 확장자
    extensions: frozenset[str] = frozenset({".py"})

    # 제외할 디렉터리 패턴
    exclude_dirs: frozenset[str] = frozenset({
        "__pycache__",
        ".venv",
        "venv",
        "dist",
        "build",
        ".git",
        ".pytest_cache",
        "htmlcov",
        "node_modules",
        ".tox",
        ".mypy_cache",
        ".ruff_cache",
        "egg-info",
        ".eggs",
    })

    # 제외할 파일 패턴
    exclude_files: frozenset[str] = frozenset({
        ".pyc",
        ".pyo",
        ".pyd",
    })

    # 빈 파일 포함 여부
    include_empty: bool = True

    # 최대 파일 크기 (바이트, 0이면 무제한)
    max_file_size: int = 0


@dataclass
class CollectedFile:
    """수집된 파일 정보"""

    path: Path
    size: int
    extension: str
    is_readable: bool = True
    error: str | None = None


@dataclass
class CollectionResult:
    """파일 수집 결과"""

    files: list[CollectedFile] = field(default_factory=list)
    skipped: list[tuple[Path, str]] = field(default_factory=list)  # (경로, 사유)
    errors: list[tuple[Path, str]] = field(default_factory=list)  # (경로, 에러)

    @property
    def valid_files(self) -> list[Path]:
        """유효한 파일 경로 목록"""
        return [f.path for f in self.files if f.is_readable]

    @property
    def all_files(self) -> list[Path]:
        """모든 파일 경로 목록 (읽기 불가 포함)"""
        return [f.path for f in self.files]

    @property
    def total_count(self) -> int:
        return len(self.files)

    @property
    def skipped_count(self) -> int:
        return len(self.skipped)

    @property
    def error_count(self) -> int:
        return len(self.errors)


class FileCollector:
    """재귀적 파일 수집기
    향후 보안 검사, 일반 검사에서도 동일하게 재사용 가능
    """

    def __init__(self, config: FileCollectorConfig | None = None) -> None:
        self.config = config or FileCollectorConfig()

    def collect(
        self,
        paths: Iterable[str | Path],
        *,
        filter_fn: Callable[[Path], bool] | None = None,
    ) -> CollectionResult:
        """주어진 경로들에서 파일을 재귀적으로 수집.

        Args:
            paths: 검사할 파일/디렉터리 경로들
            filter_fn: 추가 필터 함수 (True면 포함)

        Returns:
            수집 결과
        """
        result = CollectionResult()
        seen: set[Path] = set()

        for raw_path in paths:
            path = Path(raw_path).resolve()
            self._collect_recursive(path, result, seen, filter_fn)

        return result

    def collect_from_root(
        self,
        root: Path | None = None,
        *,
        filter_fn: Callable[[Path], bool] | None = None,
    ) -> CollectionResult:
        """프로젝트 루트에서 모든 파일 수집"""
        if root is None:
            root = self._find_project_root()
        return self.collect([root], filter_fn=filter_fn)

    def _collect_recursive(
        self,
        path: Path,
        result: CollectionResult,
        seen: set[Path],
        filter_fn: Callable[[Path], bool] | None,
    ) -> None:
        """재귀적으로 파일 수집"""
        if path in seen:
            return
        seen.add(path)

        if not path.exists():
            result.errors.append((path, "경로가 존재하지 않음"))
            return

        if path.is_file():
            self._process_file(path, result, filter_fn)
        elif path.is_dir():
            self._process_directory(path, result, seen, filter_fn)

    def _process_file(
        self,
        path: Path,
        result: CollectionResult,
        filter_fn: Callable[[Path], bool] | None,
    ) -> None:
        """단일 파일 처리"""
        # 확장자 체크
        if path.suffix not in self.config.extensions:
            result.skipped.append((path, f"지원하지 않는 확장자: {path.suffix}"))
            return

        # 제외 파일 패턴 체크
        if any(path.name.endswith(pat) for pat in self.config.exclude_files):
            result.skipped.append((path, "제외 파일 패턴"))
            return

        # 커스텀 필터
        if filter_fn and not filter_fn(path):
            result.skipped.append((path, "커스텀 필터에 의해 제외"))
            return

        try:
            stat = path.stat()
            size = stat.st_size

            # 빈 파일 체크
            if size == 0 and not self.config.include_empty:
                result.skipped.append((path, "빈 파일"))
                return

            # 최대 크기 체크
            if self.config.max_file_size > 0 and size > self.config.max_file_size:
                result.skipped.append((path, f"파일 크기 초과: {size} bytes"))
                return

            # 읽기 가능 여부 테스트
            try:
                path.read_text(encoding="utf-8")
                is_readable = True
                error = None
            except UnicodeDecodeError as e:
                is_readable = False
                error = f"인코딩 오류: {e}"
            except Exception as e:
                is_readable = False
                error = f"읽기 오류: {e}"

            result.files.append(
                CollectedFile(
                    path=path,
                    size=size,
                    extension=path.suffix,
                    is_readable=is_readable,
                    error=error,
                )
            )

        except Exception as e:
            result.errors.append((path, str(e)))

    def _process_directory(
        self,
        path: Path,
        result: CollectionResult,
        seen: set[Path],
        filter_fn: Callable[[Path], bool] | None,
    ) -> None:
        """디렉터리를 재귀적으로 처리한다."""
        # 제외 디렉터리 체크
        if path.name in self.config.exclude_dirs:
            result.skipped.append((path, "제외 디렉터리"))
            return

        try:
            for child in sorted(path.iterdir()):
                self._collect_recursive(child, result, seen, filter_fn)
        except PermissionError:
            result.errors.append((path, "권한 없음"))
        except Exception as e:
            result.errors.append((path, str(e)))

    def _find_project_root(self, start_path: Path | None = None) -> Path:
        """프로젝트 루트 찾기"""
        if start_path is None:
            start_path = Path(".").resolve()

        current = start_path
        for _ in range(10):
            if (current / "pyproject.toml").exists():
                return current
            if (current / ".git").is_dir():
                return current
            if (current / "src").is_dir():
                return current
            parent = current.parent
            if parent == current:
                break
            current = parent

        return start_path
