"""
File scanner — discovers and filters code files in a target directory.
"""

from __future__ import annotations

import logging
import os
from dataclasses import dataclass
from pathlib import Path, PurePosixPath

from hld_generator.config import (
    HLDConfig,
    LANGUAGE_MAP,
    SKIP_DIRS,
    SKIP_FILES,
)

logger = logging.getLogger(__name__)


@dataclass
class ScannedFile:
    """A discovered source file."""

    path: Path
    relative_path: str
    language: str
    size: int


class FileScanner:
    """Walk a directory tree and collect parseable source files."""

    def __init__(self, config: HLDConfig) -> None:
        self.config = config

    # ── public ─────────────────────────────────────────────────────────────

    def scan(self) -> list[ScannedFile]:
        target = self.config.target.resolve()

        if target.is_file():
            return self._scan_single_file(target)

        if not target.is_dir():
            logger.error("Target %s is not a file or directory", target)
            return []

        return self._scan_directory(target)

    # ── private ────────────────────────────────────────────────────────────

    def _scan_single_file(self, path: Path) -> list[ScannedFile]:
        # Apply the same guards as directory scan
        lang = LANGUAGE_MAP.get(path.suffix.lower())
        if not lang:
            from hld_generator.plugins import registry
            lang = registry.custom_language_map.get(path.suffix.lower())
        if not lang:
            logger.warning("Unrecognised extension: %s", path.suffix)
            return []

        if path.name in SKIP_FILES:
            logger.info("Skipping known generated file: %s", path.name)
            return []

        if not self.config.include_tests and self._is_test_file(path):
            logger.info("Skipping test file (use --include-tests): %s", path.name)
            return []

        try:
            size = path.stat().st_size
        except OSError:
            return []

        if size > self.config.max_file_size or size == 0:
            logger.info("Skipping file (size=%d, limit=%d): %s", size, self.config.max_file_size, path.name)
            return []

        return [
            ScannedFile(
                path=path,
                relative_path=path.name,
                language=lang,
                size=size,
            )
        ]

    def _scan_directory(self, root: Path) -> list[ScannedFile]:
        results: list[ScannedFile] = []

        # Use os.walk for lazy iteration — avoids materializing the entire
        # directory tree into memory (important for monorepos with 100k+ files).
        for dirpath, dirnames, filenames in os.walk(root):
            # Prune in-place: skip vendor dirs, hidden dirs (except .hld_plugins)
            dirnames[:] = sorted(
                d for d in dirnames
                if d not in SKIP_DIRS
                and (not d.startswith(".") or d == ".hld_plugins")
            )

            for name in sorted(filenames):
                if len(results) >= self.config.max_files:
                    logger.warning(
                        "Reached max file limit (%d). Increase --max-files to scan more.",
                        self.config.max_files,
                    )
                    break

                path = Path(dirpath) / name

                # Skip known lock / generated files
                if name in SKIP_FILES:
                    continue

                # Skip test files unless configured
                if not self.config.include_tests and self._is_test_file(path):
                    continue

                # Check extension (built-in + plugin-registered)
                suffix = path.suffix.lower()
                lang = LANGUAGE_MAP.get(suffix)
                if not lang:
                    from hld_generator.plugins import registry
                    lang = registry.custom_language_map.get(suffix)
                if not lang:
                    continue

                # Check file size
                try:
                    size = path.stat().st_size
                except OSError:
                    continue

                if size > self.config.max_file_size or size == 0:
                    continue

                results.append(
                    ScannedFile(
                        path=path,
                        relative_path=str(PurePosixPath(path.relative_to(root))),
                        language=lang,
                        size=size,
                    )
                )
            else:
                # Inner loop completed without break — continue outer loop
                continue
            # Inner loop was broken (max_files reached) — break outer loop too
            break

        logger.info("Scanned %d files across %d languages", len(results), len({f.language for f in results}))
        return results

    @staticmethod
    def _is_test_file(path: Path) -> bool:
        name = path.name.lower()
        stem = path.stem.lower()
        parts = [p.lower() for p in path.parts]
        dir_markers = ("tests", "test", "__tests__", "spec", "specs")
        # Name-level: test_ prefix, _test suffix, .test. or .spec. before extension
        return (
            name.startswith("test_")
            or stem.endswith("_test")
            or ".test." in name
            or ".spec." in name
            or any(p in dir_markers for p in parts)
        )
