"""
Codebase Scanner — Scans the project directory structure.

Builds a structural overview of the codebase:
- Directory tree (filtered by relevance)
- Key file patterns
- Code statistics (LOC, file counts by type)
- Entry points (main files, index files, etc.)
"""

import os
import re
from pathlib import Path
from typing import Optional


# Directories to always skip
SKIP_DIRS = {
    ".git", ".svn", ".hg",
    "node_modules", ".pnpm",
    "__pycache__", ".pytest_cache", ".mypy_cache",
    "dist", "build", "out", "target",  # Build outputs
    ".venv", "venv", "env", ".env",  # Virtual envs
    "vendor",  # Go/PHP vendor
    ".tox", ".nox",
    "coverage", ".coverage",
    ".idea", ".vscode",  # IDE files
    "eggs", ".eggs", "*.egg-info",
}

# Extensions to count as code
CODE_EXTENSIONS = {
    ".py", ".js", ".ts", ".jsx", ".tsx",
    ".rs", ".go", ".java", ".kt",
    ".rb", ".php", ".cs", ".cpp", ".c", ".h",
    ".swift", ".dart", ".elm",
    ".html", ".css", ".scss", ".sass",
    ".sql", ".sh", ".bash", ".zsh",
}

# Files that are typically "key" entry points
KEY_FILES = {
    "main.py", "app.py", "server.py", "wsgi.py", "asgi.py",
    "index.js", "index.ts", "main.js", "main.ts", "server.js", "server.ts",
    "main.go", "main.rs",
    "App.tsx", "App.jsx", "App.vue",
    "manage.py",  # Django
    "cli.py", "cli.ts",
    "Makefile", "justfile",
}

# Config files worth mentioning
CONFIG_FILES = {
    "pyproject.toml", "setup.py", "setup.cfg",
    "package.json", "tsconfig.json",
    "Cargo.toml", "go.mod",
    "Dockerfile", "docker-compose.yml",
    ".env.example",
    "CLAUDE.md", ".cursorrules", "AGENTS.md",
    "README.md", "CONTRIBUTING.md",
}


class CodebaseScanner:
    """Scans a project directory and extracts structural information."""

    def __init__(self, project_root: str, max_depth: int = 4):
        self.root = Path(project_root).resolve()
        self.max_depth = max_depth

    def scan(self) -> dict:
        """
        Perform full scan.
        Returns comprehensive project structure data.
        """
        return {
            "project_name": self.root.name,
            "root": str(self.root),
            "tree": self._build_tree(),
            "stats": self._count_files(),
            "key_files": self._find_key_files(),
            "config_files": self._find_config_files(),
            "has_tests": self._has_tests(),
            "has_docs": self._has_docs(),
            "top_level_dirs": self._get_top_level_dirs(),
        }

    def _should_skip(self, name: str) -> bool:
        """Check if a directory/file should be skipped."""
        if name.startswith(".") and name not in {".github", ".cursor", ".env.example"}:
            return True
        return name in SKIP_DIRS or name.endswith(".egg-info")

    def _build_tree(self, max_files_per_dir: int = 10) -> str:
        """Build a directory tree string, limited to relevant depth."""
        lines = [f"{self.root.name}/"]
        self._tree_recursive(self.root, lines, prefix="", depth=0, max_files=max_files_per_dir)
        return "\n".join(lines)

    def _tree_recursive(self, path: Path, lines: list, prefix: str, depth: int, max_files: int):
        """Recursively build the tree."""
        if depth >= self.max_depth:
            return

        try:
            children = sorted(path.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
        except PermissionError:
            return

        # Filter out skipped items
        children = [c for c in children if not self._should_skip(c.name)]

        dirs = [c for c in children if c.is_dir()]
        files = [c for c in children if c.is_file()]

        # Show all dirs, limit files
        shown = dirs + files[:max_files]
        remaining = len(files) - max_files if len(files) > max_files else 0

        for i, child in enumerate(shown):
            is_last = (i == len(shown) - 1) and remaining == 0
            connector = "└── " if is_last else "├── "
            name = child.name + ("/" if child.is_dir() else "")
            lines.append(f"{prefix}{connector}{name}")

            if child.is_dir():
                extension = "    " if is_last else "│   "
                self._tree_recursive(child, lines, prefix + extension, depth + 1, max_files)

        if remaining > 0:
            lines.append(f"{prefix}    ... and {remaining} more files")

    def _count_files(self) -> dict:
        """Count files by extension."""
        counts: dict[str, int] = {}
        total = 0

        for root, dirs, files in os.walk(self.root):
            # Skip certain dirs in-place
            dirs[:] = [d for d in dirs if not self._should_skip(d)]

            for f in files:
                ext = Path(f).suffix.lower()
                if ext in CODE_EXTENSIONS:
                    counts[ext] = counts.get(ext, 0) + 1
                    total += 1

        # Sort by count
        sorted_counts = dict(sorted(counts.items(), key=lambda x: -x[1]))
        return {
            "by_extension": sorted_counts,
            "total_code_files": total,
        }

    def _find_key_files(self) -> list[str]:
        """Find key entry point files."""
        found = []

        for root, dirs, files in os.walk(self.root):
            dirs[:] = [d for d in dirs if not self._should_skip(d)]
            depth = len(Path(root).relative_to(self.root).parts)
            if depth > 2:
                continue  # Only look in top 2 levels for key files

            for f in files:
                if f in KEY_FILES:
                    rel_path = Path(root).relative_to(self.root) / f
                    found.append(str(rel_path))

        return found

    def _find_config_files(self) -> list[str]:
        """Find configuration files."""
        found = []

        for f in CONFIG_FILES:
            if (self.root / f).exists():
                found.append(f)

        # Check .github/
        github = self.root / ".github"
        if github.exists():
            for workflow_file in github.rglob("*.yml"):
                rel = workflow_file.relative_to(self.root)
                found.append(str(rel))
                if len(found) > 20:
                    break

        return found

    def _has_tests(self) -> bool:
        """Check if the project has a test suite."""
        test_indicators = [
            "tests/", "test/", "spec/", "__tests__/",
            "pytest.ini", "jest.config.js", "jest.config.ts",
            ".pytest.ini",
        ]
        for indicator in test_indicators:
            if (self.root / indicator).exists():
                return True
        # Also check for test files
        for f in self.root.glob("test_*.py"):
            return True
        for f in self.root.glob("*_test.go"):
            return True
        return False

    def _has_docs(self) -> bool:
        """Check if the project has documentation."""
        for doc_dir in ["docs/", "doc/", "documentation/", "wiki/"]:
            if (self.root / doc_dir).exists():
                return True
        for doc_file in ["README.md", "README.rst", "CONTRIBUTING.md"]:
            if (self.root / doc_file).exists():
                return True
        return False

    def _get_top_level_dirs(self) -> list[str]:
        """Get top-level directories (excluding skipped ones)."""
        dirs = []
        try:
            for item in sorted(self.root.iterdir()):
                if item.is_dir() and not self._should_skip(item.name):
                    dirs.append(item.name)
        except PermissionError:
            pass
        return dirs

    def to_summary(self) -> str:
        """Return a human-readable project summary."""
        data = self.scan()
        lines = [
            f"Project: {data['project_name']}",
            f"Total code files: {data['stats']['total_code_files']}",
        ]

        if data["stats"]["by_extension"]:
            ext_summary = ", ".join(
                f"{ext}: {cnt}"
                for ext, cnt in list(data["stats"]["by_extension"].items())[:5]
            )
            lines.append(f"File types: {ext_summary}")

        if data["key_files"]:
            lines.append(f"Entry points: {', '.join(data['key_files'][:5])}")

        if data["top_level_dirs"]:
            lines.append(f"Top-level dirs: {', '.join(data['top_level_dirs'][:8])}")

        flags = []
        if data["has_tests"]:
            flags.append("has tests")
        if data["has_docs"]:
            flags.append("has docs")
        if flags:
            lines.append(f"Indicators: {', '.join(flags)}")

        return "\n".join(lines)
