"""
Fuzzy File Search - Find files by fuzzy matching with @filename syntax

Provides fuzzy file search functionality for the CLI:
- Detect `@` prefix in input text
- Fuzzy-match filename against project files
- Show top matches for user selection
- Insert file path or contents

Examples:
    # Find files matching "app"
    searcher = FuzzyFileSearch(workspace_root="/project")
    matches = searcher.search("app", limit=5)
    for match in matches:
        print(f"{match.path} (score: {match.score})")

    # Detect and expand @references
    text = "Look at @app.py and @test"
    expanded = expand_at_references(text, workspace_root="/project")
"""

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Optional

from rapidfuzz import fuzz, process
from rich.console import Console
from rich.prompt import Prompt
from rich.table import Table


@dataclass
class FuzzyMatch:
    """A fuzzy match result"""

    path: str
    filename: str
    score: float
    is_exact: bool = False

    def __post_init__(self):
        if self.score < 0 or self.score > 100:
            raise ValueError(f"Score must be between 0 and 100, got {self.score}")


@dataclass
class AtReference:
    """An @filename reference in text"""

    original: str
    query: str
    start: int
    end: int
    matches: list[FuzzyMatch] = field(default_factory=list)
    selected: Optional[FuzzyMatch] = None


@dataclass
class ExpansionResult:
    """Result of expanding @references"""

    text: str
    references: list[AtReference]
    success: bool = True
    warnings: list[str] = field(default_factory=list)


class FileFinder:
    """
    Finds files in a workspace directory

    Handles file discovery with .gitignore-like exclusions.

    Example:
        finder = FileFinder("/project")
        files = finder.find_all()
        # ['src/app.py', 'tests/test_app.py', ...]
    """

    DEFAULT_IGNORE = {
        ".git",
        "__pycache__",
        "node_modules",
        ".venv",
        "venv",
        ".env",
        ".pytest_cache",
        ".mypy_cache",
        ".ruff_cache",
        "dist",
        "build",
        ".eggs",
        ".egg-info",
        ".tox",
        ".coverage",
        "htmlcov",
        ".hypothesis",
    }

    def __init__(
        self,
        workspace_root: Optional[Path] = None,
        ignore_patterns: Optional[set[str]] = None,
        max_files: int = 10000,
    ):
        """
        Initialize file finder

        Args:
            workspace_root: Root directory to search (default: cwd)
            ignore_patterns: Patterns to ignore (default: DEFAULT_IGNORE)
            max_files: Maximum files to index
        """
        self.workspace_root = Path(workspace_root) if workspace_root else Path.cwd()
        self.ignore_patterns = ignore_patterns or self.DEFAULT_IGNORE.copy()
        self.max_files = max_files
        self._cache: Optional[list[str]] = None

    def find_all(self, use_cache: bool = True) -> list[str]:
        """
        Find all files in workspace

        Args:
            use_cache: Use cached file list if available

        Returns:
            List of relative file paths
        """
        if use_cache and self._cache is not None:
            return self._cache

        files = []
        try:
            for file_path in self.workspace_root.rglob("*"):
                if len(files) >= self.max_files:
                    break

                if file_path.is_file() and not self._should_ignore(file_path):
                    try:
                        rel_path = file_path.relative_to(self.workspace_root)
                        files.append(str(rel_path))
                    except ValueError:
                        files.append(str(file_path))
        except PermissionError:
            pass

        files.sort()
        self._cache = files
        return files

    def clear_cache(self) -> None:
        """Clear the file cache"""
        self._cache = None

    def _should_ignore(self, file_path: Path) -> bool:
        """Check if file should be ignored"""
        path_str = str(file_path)
        for pattern in self.ignore_patterns:
            if pattern in path_str:
                return True
        return False


class FuzzyFileSearch:
    """
    Fuzzy file search with @filename syntax support

    Provides fuzzy matching against project files with configurable
    scoring and interactive selection.

    Example:
        searcher = FuzzyFileSearch("/project")
        matches = searcher.search("app", limit=5)

        # With interactive selection
        selected = searcher.interactive_select("app")
    """

    # Pattern to detect @references: @filename or @"filename with spaces"
    AT_PATTERN = re.compile(r'@(?:"([^"]+)"|([^\s@]+))')

    def __init__(
        self,
        workspace_root: Optional[Path] = None,
        file_finder: Optional[FileFinder] = None,
        min_score: float = 40.0,
        console: Optional[Console] = None,
    ):
        """
        Initialize fuzzy file search

        Args:
            workspace_root: Root directory to search
            file_finder: Custom file finder (default: creates new)
            min_score: Minimum score threshold (0-100)
            console: Rich console for output
        """
        self.workspace_root = Path(workspace_root) if workspace_root else Path.cwd()
        self.file_finder = file_finder or FileFinder(self.workspace_root)
        self.min_score = min_score
        self.console = console or Console()

    def search(self, query: str, limit: int = 5) -> list[FuzzyMatch]:
        """
        Search for files matching query

        Args:
            query: Search query (filename or partial)
            limit: Maximum results to return

        Returns:
            List of FuzzyMatch objects sorted by score
        """
        if not query:
            return []

        files = self.file_finder.find_all()
        if not files:
            return []

        # Build choices: map filename to full path for matching
        choices = {}
        for file_path in files:
            filename = Path(file_path).name
            # Store path with filename as key for quick lookup
            if filename not in choices:
                choices[filename] = []
            choices[filename].append(file_path)

        # First check for exact filename match
        matches = []
        query_lower = query.lower()

        for filename, paths in choices.items():
            if filename.lower() == query_lower:
                for path in paths:
                    matches.append(
                        FuzzyMatch(
                            path=path,
                            filename=filename,
                            score=100.0,
                            is_exact=True,
                        )
                    )

        # If we have exact matches, return those
        if matches:
            return matches[:limit]

        # Fuzzy match against all file paths
        # Use the full path for better matching context
        results = process.extract(
            query,
            files,
            scorer=fuzz.WRatio,
            limit=limit * 2,  # Get extra results to filter
        )

        for path, score, _ in results:
            if score >= self.min_score:
                filename = Path(path).name
                matches.append(
                    FuzzyMatch(
                        path=path,
                        filename=filename,
                        score=float(score),
                        is_exact=False,
                    )
                )

        # Sort by score descending
        matches.sort(key=lambda m: m.score, reverse=True)
        return matches[:limit]

    def interactive_select(
        self,
        query: str,
        limit: int = 5,
        prompt_text: str = "Select file",
    ) -> Optional[FuzzyMatch]:
        """
        Show interactive file selection menu

        Args:
            query: Search query
            limit: Maximum results to show
            prompt_text: Prompt text for selection

        Returns:
            Selected FuzzyMatch or None if cancelled
        """
        matches = self.search(query, limit=limit)

        if not matches:
            self.console.print(f"[yellow]No files found matching '{query}'[/yellow]")
            return None

        if len(matches) == 1 and matches[0].is_exact:
            # Single exact match - auto-select
            return matches[0]

        # Build selection table
        table = Table(title=f"Files matching '{query}'", show_header=True)
        table.add_column("#", style="cyan", width=3)
        table.add_column("File", style="green")
        table.add_column("Score", style="yellow", width=6)

        for i, match in enumerate(matches, 1):
            score_str = f"{match.score:.0f}%"
            if match.is_exact:
                score_str = "exact"
            table.add_row(str(i), match.path, score_str)

        self.console.print(table)

        # Get user selection
        choices = [str(i) for i in range(1, len(matches) + 1)]
        choices.append("0")  # Cancel option

        self.console.print("[dim]Enter 0 to cancel[/dim]")
        selection = Prompt.ask(prompt_text, choices=choices, default="1")

        if selection == "0":
            return None

        index = int(selection) - 1
        return matches[index]

    def detect_references(self, text: str) -> list[AtReference]:
        """
        Detect @filename references in text

        Args:
            text: Input text to scan

        Returns:
            List of AtReference objects
        """
        references = []

        for match in self.AT_PATTERN.finditer(text):
            # Group 1 is quoted, group 2 is unquoted
            query = match.group(1) or match.group(2)
            references.append(
                AtReference(
                    original=match.group(0),
                    query=query,
                    start=match.start(),
                    end=match.end(),
                )
            )

        return references

    def expand_references(
        self,
        text: str,
        interactive: bool = False,
        insert_contents: bool = False,
        read_file: Optional[Callable[[str], str]] = None,
    ) -> ExpansionResult:
        """
        Expand @references in text to file paths or contents

        Args:
            text: Input text with @references
            interactive: Show selection menu for ambiguous matches
            insert_contents: Insert file contents instead of paths
            read_file: Custom file reader function

        Returns:
            ExpansionResult with expanded text
        """
        references = self.detect_references(text)
        if not references:
            return ExpansionResult(text=text, references=[])

        warnings = []
        result_text = text

        # Process in reverse order to maintain correct positions
        for ref in reversed(references):
            matches = self.search(ref.query, limit=5)
            ref.matches = matches

            if not matches:
                warnings.append(f"No files found for @{ref.query}")
                continue

            # Select match
            if len(matches) == 1 or matches[0].is_exact:
                ref.selected = matches[0]
            elif interactive:
                ref.selected = self.interactive_select(ref.query)
            else:
                # Auto-select best match
                ref.selected = matches[0]
                if not matches[0].is_exact:
                    warnings.append(
                        f"Fuzzy match: @{ref.query} -> {matches[0].path} "
                        f"(score: {matches[0].score:.0f}%)"
                    )

            if ref.selected:
                # Get replacement value
                if insert_contents:
                    if read_file:
                        try:
                            replacement = read_file(ref.selected.path)
                        except Exception as e:
                            warnings.append(f"Could not read {ref.selected.path}: {e}")
                            replacement = ref.selected.path
                    else:
                        # Default file reader
                        try:
                            file_path = self.workspace_root / ref.selected.path
                            replacement = file_path.read_text(encoding="utf-8")
                        except Exception as e:
                            warnings.append(f"Could not read {ref.selected.path}: {e}")
                            replacement = ref.selected.path
                else:
                    replacement = ref.selected.path

                # Replace in text
                result_text = result_text[: ref.start] + replacement + result_text[ref.end :]

        return ExpansionResult(
            text=result_text,
            references=references,
            success=len(warnings) == 0,
            warnings=warnings,
        )


# Convenience functions


def fuzzy_search_files(
    query: str,
    workspace_root: Optional[Path] = None,
    limit: int = 5,
) -> list[FuzzyMatch]:
    """
    Search for files matching query (convenience function)

    Args:
        query: Search query
        workspace_root: Root directory
        limit: Maximum results

    Returns:
        List of FuzzyMatch objects
    """
    searcher = FuzzyFileSearch(workspace_root=workspace_root)
    return searcher.search(query, limit=limit)


def detect_at_references(text: str) -> list[AtReference]:
    """
    Detect @filename references in text (convenience function)

    Args:
        text: Input text

    Returns:
        List of AtReference objects
    """
    searcher = FuzzyFileSearch()
    return searcher.detect_references(text)


def expand_at_references(
    text: str,
    workspace_root: Optional[Path] = None,
    interactive: bool = False,
    insert_contents: bool = False,
) -> ExpansionResult:
    """
    Expand @references in text (convenience function)

    Args:
        text: Input text with @references
        workspace_root: Root directory
        interactive: Show selection menu
        insert_contents: Insert file contents

    Returns:
        ExpansionResult with expanded text
    """
    searcher = FuzzyFileSearch(workspace_root=workspace_root)
    return searcher.expand_references(
        text,
        interactive=interactive,
        insert_contents=insert_contents,
    )
