"""Pattern matching utilities for file discovery with security and limits."""

import fnmatch
import glob
import os
from pathlib import Path
from typing import List, Set

from anyio import Path as AsyncPath

from mcp_guide.config_constants import COMMANDS_DIR, MAX_DOCUMENTS_PER_GLOB, MAX_GLOB_DEPTH
from mcp_guide.core.mcp_log import get_logger
from mcp_guide.lazy_path import LazyPath

logger = get_logger(__name__)


def is_valid_file(path: Path) -> bool:
    """Check if file should be included in results.

    Args:
        path: File path to check

    Returns:
        True if file is valid, False if should be excluded
    """
    # Check filename - exclude __pycache__ style names, hidden files, and backup files
    name = path.name
    if name.startswith("__") or name.startswith(".") or name.endswith(".orig"):
        return False

    # Check parent directories for __pycache__ style directories and reject . and .. segments
    for part in path.parts[:-1]:  # Exclude filename from check
        if part.startswith("__") or part in (".", ".."):
            return False

    return True


def is_valid_command(path: Path) -> bool:
    """Check if file should be considered a valid command.

    Args:
        path: File path to check

    Returns:
        True if file is a valid command, False if should be excluded
    """
    # First apply general file validity rules
    if not is_valid_file(path):
        return False

    # Command-specific rules: exclude underscore-prefixed files
    name = path.name
    if name.startswith("_"):
        return False

    # Check path components for underscore prefixes, but allow _commands directory
    for i, part in enumerate(path.parts[:-1]):  # Exclude filename from check
        if part.startswith("_"):
            # Allow _commands as the first underscore directory (official commands dir)
            if part == COMMANDS_DIR:
                continue
            return False

    return True


def is_valid_partial(path: Path) -> bool:
    """Check if file should be considered a valid partial.

    Args:
        path: File path to check

    Returns:
        True if file is a valid partial, False if should be excluded
    """
    # Apply general file validity rules
    return is_valid_file(path)


async def _process_match(
    match_path: Path,
    search_dir: Path,
    seen_files: Set[Path],
    matched_files: List[Path],
) -> bool:
    """Process a single glob match and add to results if valid.

    Returns:
        True if file was added, False otherwise
    """
    if not await AsyncPath(match_path).is_file():
        return False

    if not is_valid_file(match_path):
        return False

    # Resolve symlinks and deduplicate
    try:
        resolved_path = match_path.resolve()
    except OSError as e:
        logger.warning(f"Failed to resolve symlink {match_path}: {e}")
        return False

    if resolved_path in seen_files:
        return False

    # Check depth limit
    try:
        relative_path = resolved_path.relative_to(search_dir.resolve())
        depth = len(relative_path.parts) - 1  # Subtract 1 for file itself
        if depth > MAX_GLOB_DEPTH:
            return False
    except ValueError:
        # Path is outside search_dir, skip
        logger.debug(f"Skipping file outside search directory: {resolved_path}")
        return False

    matched_files.append(resolved_path)
    seen_files.add(resolved_path)
    return True


async def _walk_with_depth_limit(search_dir: Path, pattern: str) -> List[Path]:
    """Walk directory tree with depth limit, matching pattern.

    Uses os.walk() with manual depth tracking to prevent DOS from deep traversal.
    Only traverses up to MAX_GLOB_DEPTH levels.
    """
    matched_paths: List[Path] = []
    search_dir_resolved = search_dir  # Already resolved by caller

    # Check if pattern contains ** (recursive)
    if "**" not in pattern:
        # Non-recursive: use glob directly (safe, only checks one level)
        for match_str in glob.iglob(pattern, root_dir=search_dir_resolved, recursive=False):
            matched_paths.append(search_dir_resolved / match_str)
        return matched_paths

    # Recursive pattern: use os.walk with depth limit
    # Parse pattern to extract directory prefix and file pattern
    # Examples:
    #   "**/*.md" -> prefix="", file_pattern="*.md"
    #   "docs/**/*.py" -> prefix="docs", file_pattern="*.py"
    #   "**/*" -> prefix="", file_pattern="*"

    pattern_parts = pattern.split("**/")
    if len(pattern_parts) == 2:
        prefix = pattern_parts[0].rstrip("/")
        file_pattern = pattern_parts[1] if pattern_parts[1] else "*"
    elif pattern.startswith("**/"):
        prefix = ""
        file_pattern = pattern[3:] if len(pattern) > 3 else "*"
    else:
        # Pattern like "**" alone
        prefix = ""
        file_pattern = "*"

    start_dir = search_dir_resolved / prefix if prefix else search_dir_resolved

    if not await AsyncPath(start_dir).exists():
        return matched_paths

    for root, dirs, files in os.walk(start_dir):
        root_path = Path(root).resolve()  # Resolve to handle symlinks

        # Calculate depth relative to search_dir
        try:
            relative = root_path.relative_to(search_dir_resolved)
            depth = len(relative.parts)
        except ValueError:
            # Outside search_dir, skip
            continue

        # Stop traversing deeper if we've hit the limit
        if depth >= MAX_GLOB_DEPTH:
            dirs.clear()  # Don't descend into subdirectories

        # Match files in this directory
        for filename in files:
            if fnmatch.fnmatch(filename, file_pattern):
                matched_paths.append(root_path / filename)

    return matched_paths


async def safe_glob_search(search_dir: Path, patterns: List[str]) -> List[Path]:
    """Safely search for files using glob patterns with safety limits.

    Args:
        search_dir: Directory to search within
        patterns: List of glob patterns (e.g., ["*.md", "**/*.py"])

    Returns:
        List of Path objects matching patterns, limited to MAX_DOCUMENTS_PER_GLOB
    """
    # Resolve search_dir once to handle ~ and ${VAR} expansion
    search_dir_resolved = LazyPath(search_dir).resolve()

    matched_files: List[Path] = []
    seen_files: Set[Path] = set()

    for pattern in patterns:
        if len(matched_files) >= MAX_DOCUMENTS_PER_GLOB:
            logger.warning(f"Reached maximum document limit ({MAX_DOCUMENTS_PER_GLOB}) for glob search")
            break

        matches_found = False

        # Use depth-limited walk for safety
        try:
            candidate_paths = await _walk_with_depth_limit(search_dir_resolved, pattern)
        except Exception as e:
            logger.warning(f"Pattern '{pattern}' failed: {e}")
            continue

        for match_path in candidate_paths:
            if len(matched_files) >= MAX_DOCUMENTS_PER_GLOB:
                logger.warning(f"Reached maximum document limit ({MAX_DOCUMENTS_PER_GLOB}) for glob search")
                break

            if await _process_match(match_path, search_dir_resolved, seen_files, matched_files):
                matches_found = True

        # If no matches and pattern has no extension, try with .* wildcard
        if not matches_found and "." not in Path(pattern).name:
            wildcard_pattern = f"{pattern}.*"

            try:
                candidate_paths = await _walk_with_depth_limit(search_dir_resolved, wildcard_pattern)
            except Exception as e:
                logger.warning(f"Pattern '{wildcard_pattern}' failed: {e}")
                continue

            for match_path in candidate_paths:
                if len(matched_files) >= MAX_DOCUMENTS_PER_GLOB:
                    logger.warning(
                        f"Reached maximum document limit ({MAX_DOCUMENTS_PER_GLOB}) for glob search (.* fallback)"
                    )
                    break

                await _process_match(match_path, search_dir_resolved, seen_files, matched_files)

    return matched_files
