"""
Context Loader Module

Handles repository analysis and file content loading:
- Recursive file discovery with .gitignore support
- Binary file detection and exclusion
- File tree generation
- Content aggregation for LLM context
"""

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from gitignore_parser import parse_gitignore
from rich.console import Console
from rich.tree import Tree

console = Console()


@dataclass
class ContextConfig:
    """Context loading configuration."""

    ignore_patterns: list[str] = field(default_factory=list)
    include_extensions: list[str] = field(default_factory=list)
    max_files: int = 100
    max_file_size: int = 100_000  # 100KB per file for context

    def __post_init__(self):
        if not self.ignore_patterns:
            self.ignore_patterns = [
                "node_modules",
                "__pycache__",
                ".git",
                "*.pyc",
                "*.pyo",
                ".env",
                "venv",
                ".venv",
                "dist",
                "build",
                "*.egg-info",
                ".idea",
                ".vscode",
                "*.lock",
                "package-lock.json",
                "*.min.js",
                "*.min.css",
            ]


@dataclass
class FileInfo:
    """Information about a single file."""

    path: Path
    relative_path: str
    size: int
    extension: str
    is_binary: bool = False
    content: Optional[str] = None


@dataclass
class ProjectContext:
    """Complete project context information."""

    root: Path
    files: list[FileInfo] = field(default_factory=list)
    file_tree: str = ""
    total_files: int = 0
    total_size: int = 0
    languages: dict[str, int] = field(default_factory=dict)
    key_files: list[str] = field(default_factory=list)

    def get_summary(self) -> str:
        """Get a summary of the project context."""
        lang_summary = ", ".join(
            f"{lang}: {count}" for lang, count in sorted(
                self.languages.items(), key=lambda x: -x[1]
            )[:5]
        )

        return f"""Project: {self.root.name}
Total Files: {self.total_files}
Total Size: {self.total_size:,} bytes
Languages: {lang_summary}
Key Files: {', '.join(self.key_files[:10])}

File Tree:
{self.file_tree}"""


class ContextLoader:
    """
    Loads and analyzes repository context for the AI agent.
    
    Features:
    - Respects .gitignore patterns
    - Detects and excludes binary files
    - Identifies key project files (package.json, requirements.txt, etc.)
    - Generates file tree visualization
    - Aggregates file contents within size limits
    """

    # Binary file signatures (magic bytes)
    BINARY_SIGNATURES = [
        b'\x89PNG',      # PNG
        b'\xff\xd8\xff', # JPEG
        b'GIF8',         # GIF
        b'PK\x03\x04',   # ZIP/DOCX/etc
        b'\x7fELF',      # ELF executable
        b'MZ',           # Windows executable
        b'\x00\x00\x01\x00',  # ICO
        b'%PDF',         # PDF
    ]

    # Key project files to always include
    KEY_FILES = [
        "package.json",
        "requirements.txt",
        "pyproject.toml",
        "setup.py",
        "Cargo.toml",
        "go.mod",
        "pom.xml",
        "build.gradle",
        "Makefile",
        "Dockerfile",
        "docker-compose.yml",
        "docker-compose.yaml",
        ".env.example",
        "README.md",
        "readme.md",
        "README",
    ]

    # Extension to language mapping
    EXTENSION_LANGUAGES = {
        ".py": "Python",
        ".js": "JavaScript",
        ".ts": "TypeScript",
        ".jsx": "JavaScript",
        ".tsx": "TypeScript",
        ".java": "Java",
        ".go": "Go",
        ".rs": "Rust",
        ".rb": "Ruby",
        ".php": "PHP",
        ".c": "C",
        ".cpp": "C++",
        ".h": "C/C++",
        ".cs": "C#",
        ".swift": "Swift",
        ".kt": "Kotlin",
        ".scala": "Scala",
        ".vue": "Vue",
        ".svelte": "Svelte",
        ".html": "HTML",
        ".css": "CSS",
        ".scss": "SCSS",
        ".json": "JSON",
        ".yaml": "YAML",
        ".yml": "YAML",
        ".toml": "TOML",
        ".md": "Markdown",
        ".sql": "SQL",
        ".sh": "Shell",
        ".bash": "Shell",
        ".ps1": "PowerShell",
    }

    def __init__(self, config: Optional[ContextConfig] = None):
        self.config = config or ContextConfig()
        self._gitignore_matcher = None

    def load(self, root: Path, include_content: bool = True) -> ProjectContext:
        """
        Load the complete project context from a directory.
        
        Args:
            root: Root directory path
            include_content: Whether to include file contents
            
        Returns:
            ProjectContext with all analyzed information
        """
        root = Path(root).resolve()

        if not root.exists():
            raise ValueError(f"Directory does not exist: {root}")

        if not root.is_dir():
            raise ValueError(f"Path is not a directory: {root}")

        # Load .gitignore if present
        gitignore_path = root / ".gitignore"
        if gitignore_path.exists():
            self._gitignore_matcher = parse_gitignore(gitignore_path)
        else:
            self._gitignore_matcher = None

        context = ProjectContext(root=root)

        # Discover all files
        all_files = self._discover_files(root)
        context.total_files = len(all_files)

        # Sort and limit files
        all_files.sort(key=lambda f: f.relative_path)
        context.files = all_files[: self.config.max_files]

        # Load content if requested
        if include_content:
            for file_info in context.files:
                if not file_info.is_binary and file_info.size <= self.config.max_file_size:
                    try:
                        content = file_info.path.read_text(encoding="utf-8", errors="ignore")
                        file_info.content = content
                    except Exception:
                        pass

        # Calculate totals and languages
        for file_info in context.files:
            context.total_size += file_info.size
            if file_info.extension in self.EXTENSION_LANGUAGES:
                lang = self.EXTENSION_LANGUAGES[file_info.extension]
                context.languages[lang] = context.languages.get(lang, 0) + 1

        # Identify key files
        for file_info in context.files:
            if file_info.path.name in self.KEY_FILES:
                context.key_files.append(file_info.relative_path)

        # Generate file tree
        context.file_tree = self._generate_tree(root, context.files)

        return context

    def _discover_files(self, root: Path, current: Optional[Path] = None) -> list[FileInfo]:
        """Recursively discover all files in the directory."""
        if current is None:
            current = root

        files = []

        try:
            entries = list(current.iterdir())
        except PermissionError:
            return files

        for entry in entries:
            relative = entry.relative_to(root)
            relative_str = str(relative).replace("\\", "/")

            # Check if ignored
            if self._should_ignore(entry, relative_str):
                continue

            if entry.is_dir():
                # Recurse into directory
                files.extend(self._discover_files(root, entry))
            elif entry.is_file():
                # Check if binary
                is_binary = self._is_binary(entry)

                try:
                    size = entry.stat().st_size
                except Exception:
                    size = 0

                files.append(FileInfo(
                    path=entry,
                    relative_path=relative_str,
                    size=size,
                    extension=entry.suffix.lower(),
                    is_binary=is_binary,
                ))

        return files

    def _should_ignore(self, path: Path, relative: str) -> bool:
        """Check if a path should be ignored."""
        name = path.name

        # Check built-in ignore patterns
        for pattern in self.config.ignore_patterns:
            if pattern.startswith("*."):
                # Extension pattern
                if name.endswith(pattern[1:]):
                    return True
            elif pattern in relative or name == pattern:
                return True

        # Check .gitignore
        if self._gitignore_matcher and self._gitignore_matcher(str(path)):
            return True

        # Check extension filter if specified
        if self.config.include_extensions and path.is_file():
            ext = path.suffix.lower().lstrip(".")
            if ext not in self.config.include_extensions:
                return True

        return False

    def _is_binary(self, path: Path) -> bool:
        """Check if a file is binary."""
        try:
            with open(path, "rb") as f:
                header = f.read(16)

            # Check magic bytes
            for sig in self.BINARY_SIGNATURES:
                if header.startswith(sig):
                    return True

            # Check for null bytes (common in binary files)
            if b'\x00' in header:
                return True

            return False
        except Exception:
            return True

    def _generate_tree(self, root: Path, files: list[FileInfo]) -> str:
        """Generate a text-based file tree."""
        lines = [f"📁 {root.name}/"]

        # Build directory structure
        dirs: dict[str, list[str]] = {"": []}

        for file_info in files:
            parts = file_info.relative_path.split("/")
            if len(parts) == 1:
                dirs[""].append(parts[0])
            else:
                dir_path = "/".join(parts[:-1])
                if dir_path not in dirs:
                    dirs[dir_path] = []
                dirs[dir_path].append(parts[-1])

        # Sort and format
        def add_level(prefix: str, path: str, indent: str):
            items = sorted(dirs.get(path, []))
            # Find subdirectories at this level
            subdirs = []
            for p in dirs.keys():
                if p == path or not p:
                    continue
                if path:
                    if p.startswith(path + "/"):
                        remaining = p[len(path) + 1:]
                        if "/" not in remaining:
                            subdirs.append(remaining)
                else:
                    if "/" not in p:
                        subdirs.append(p)
            subdirs = sorted(set(subdirs))

            all_items = []
            # Add subdirectories
            for subdir in subdirs:
                full_path = f"{path}/{subdir}" if path else subdir
                if full_path in dirs or any(p.startswith(full_path + "/") for p in dirs):
                    all_items.append((subdir, True, full_path))

            # Add files
            for item in items:
                all_items.append((item, False, None))

            for i, (name, is_dir, full_path) in enumerate(all_items):
                is_last = i == len(all_items) - 1
                connector = "└── " if is_last else "├── "
                if is_dir:
                    lines.append(f"{indent}{connector}📁 {name}/")
                    new_indent = indent + ("    " if is_last else "│   ")
                    add_level(prefix, full_path, new_indent)
                else:
                    icon = self._get_file_icon(name)
                    lines.append(f"{indent}{connector}{icon} {name}")

        add_level("", "", "")

        return "\n".join(lines[:50])  # Limit tree size

    def _get_file_icon(self, filename: str) -> str:
        """Get an emoji icon for a file based on its extension."""
        ext = Path(filename).suffix.lower()
        icons = {
            ".py": "🐍",
            ".js": "📜",
            ".ts": "📘",
            ".jsx": "⚛️",
            ".tsx": "⚛️",
            ".json": "📋",
            ".yaml": "📋",
            ".yml": "📋",
            ".md": "📝",
            ".html": "🌐",
            ".css": "🎨",
            ".sql": "🗃️",
            ".sh": "⚡",
            ".dockerfile": "🐳",
        }

        if filename.lower() == "dockerfile":
            return "🐳"

        return icons.get(ext, "📄")


def load_project_context(path: str | Path, config: Optional[ContextConfig] = None) -> ProjectContext:
    """
    Convenience function to load project context.
    
    Args:
        path: Path to the project root
        config: Optional context configuration
        
    Returns:
        ProjectContext with analyzed information
    """
    loader = ContextLoader(config)
    return loader.load(Path(path))
