"""File discovery for static analysis.

Walks a git repository to discover source files, compute hashes,
and classify by language using registered adapters.
"""

from __future__ import annotations

import hashlib
import logging
import subprocess
from typing import TYPE_CHECKING

from unfamiliar.analysis.models import FileInfo

if TYPE_CHECKING:
    from pathlib import Path

    from unfamiliar.analysis.adapters.protocol import AdapterRegistry
    from unfamiliar.analysis.filters.patterns import FileFilter

logger = logging.getLogger(__name__)


class AnalysisError(Exception):
    """Raised when the analysis pipeline encounters an error."""


def discover_files(
    repo_path: Path,
    registry: AdapterRegistry,
    file_filter: FileFilter | None = None,
) -> tuple[list[FileInfo], int]:
    """Discover all source files in a git repository.

    Uses git ls-files to get tracked files (respects .gitignore).
    For each file: reads content, computes SHA-256 hash, determines language.

    Args:
        repo_path: Path to the git repository root.
        registry: Adapter registry for language classification.
        file_filter: Optional filter to exclude non-code files.

    Returns:
        Tuple of (filtered files, excluded count).
        Files are sorted by path for deterministic output.
        Excluded count is 0 if no filter provided.

    Raises:
        AnalysisError: If git ls-files fails or repo_path is not a git repo.
    """
    tracked_files = _run_git_ls_files(repo_path)
    file_infos: list[FileInfo] = []
    excluded_count = 0

    for relative_path in tracked_files:
        file_path = repo_path / relative_path

        # Skip directories (git ls-files can return submodule paths)
        if not file_path.is_file():
            continue

        # Apply filter if provided
        if file_filter is not None and file_filter.should_exclude(relative_path):
            excluded_count += 1
            continue

        try:
            content = file_path.read_bytes()
        except OSError as e:
            logger.warning("Could not read file %s: %s", relative_path, e)
            continue

        # Skip binary files (files that fail UTF-8 decode)
        try:
            content.decode("utf-8")
        except UnicodeDecodeError:
            logger.debug("Skipping binary file: %s", relative_path)
            continue

        content_hash = hashlib.sha256(content).hexdigest()
        size_bytes = len(content)
        language = _classify_language(relative_path, registry)

        file_infos.append(
            FileInfo(
                path=relative_path,
                language=language,
                size_bytes=size_bytes,
                content_hash=content_hash,
            )
        )

    # Sort by path for deterministic output
    file_infos.sort(key=lambda f: f.path)
    return file_infos, excluded_count


def _run_git_ls_files(repo_path: Path) -> list[str]:
    """Run git ls-files to get tracked files in a repository.

    Args:
        repo_path: Path to the git repository root.

    Returns:
        List of relative file paths.

    Raises:
        AnalysisError: If git command fails.
    """
    try:
        result = subprocess.run(
            ["git", "ls-files"],
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=True,
        )
    except subprocess.CalledProcessError as e:
        raise AnalysisError(f"git ls-files failed in {repo_path}: {e.stderr}") from e
    except FileNotFoundError as e:
        raise AnalysisError("git executable not found") from e

    # Split on newlines, filter empty strings
    return [line for line in result.stdout.strip().split("\n") if line]


def _classify_language(path: str, registry: AdapterRegistry) -> str:
    """Determine language from file extension using adapter registry.

    Args:
        path: File path (relative).
        registry: Adapter registry for language detection.

    Returns:
        Language name from matching adapter, or "unknown" if no match.
    """
    adapter = registry.get_adapter(path)
    if adapter is not None:
        return adapter.language_name()
    return "unknown"
