import asyncio
import logging
import os
import platform
import re
import shutil
import tempfile
from contextlib import asynccontextmanager

from mcp.server.fastmcp import Context, FastMCP

from .constants import (
    ALL_ENTITY_TYPES,
    DEFAULT_MAX_OUTPUT_CHARS,
    DEFAULT_MAX_SEARCH_RESULTS,
    MAX_ERROR_OUTPUT_CHARS,
)
from .il_parser import extract_csharp_method, extract_il_method
from .ilspy_wrapper import ILSpyWrapper
from .models import EntityType, LanguageVersion
from .utils import find_ilspycmd_path

# Setup logging
log_level = os.getenv("LOGLEVEL", "INFO").upper()
logging.basicConfig(
    level=getattr(logging, log_level, logging.INFO),
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)


# Module-level cached wrapper instance (lazily initialized)
# This avoids complex lifespan context access and provides simple caching
_cached_wrapper: ILSpyWrapper | None = None


@asynccontextmanager
async def app_lifespan(server: FastMCP):
    """Initialize application state on startup, cleanup on shutdown."""
    global _cached_wrapper
    logger.info("mcilspy server starting up")
    try:
        yield {}
    finally:
        logger.info("mcilspy server shutting down")
        _cached_wrapper = None  # Clear cache on shutdown


# Create FastMCP server with lifespan
mcp = FastMCP("mcilspy", lifespan=app_lifespan)


def get_wrapper(ctx: Context | None = None) -> ILSpyWrapper:
    """Get ILSpy wrapper instance, creating one if needed.

    The wrapper is cached at module level for efficiency. It's lazily
    initialized on first use to avoid failing startup if ilspycmd
    isn't installed (dnfile-based tools still work without it).

    Args:
        ctx: Optional FastMCP context (unused, kept for API compatibility)

    Returns:
        ILSpyWrapper instance

    Raises:
        RuntimeError: If ilspycmd is not installed
    """
    global _cached_wrapper
    if _cached_wrapper is None:
        _cached_wrapper = ILSpyWrapper()
    return _cached_wrapper


def _format_error(error: Exception, context: str = "") -> str:
    """Format an error message consistently across all tools.

    Args:
        error: The exception that occurred
        context: Optional context string (e.g., "listing types")

    Returns:
        Formatted error string for MCP response
    """
    error_msg = str(error)
    if context:
        return f"**Error** ({context}): {error_msg}"
    return f"**Error**: {error_msg}"


class AssemblyPathError(ValueError):
    """Raised when an assembly path fails validation."""

    pass


def _validate_assembly_path(assembly_path: str) -> str:
    """Validate and normalize an assembly path for security.

    Performs the following checks:
    1. Path is not empty
    2. Resolves to an absolute path (prevents path traversal)
    3. File exists and is a regular file (not a directory or symlink to directory)
    4. Has a valid .NET assembly extension (.dll or .exe)

    Args:
        assembly_path: User-provided path to a .NET assembly

    Returns:
        Absolute, validated path to the assembly

    Raises:
        AssemblyPathError: If the path fails any validation check
    """
    if not assembly_path or not assembly_path.strip():
        raise AssemblyPathError("Assembly path cannot be empty")

    # Resolve to absolute path (handles .., symlinks, etc.)
    try:
        resolved_path = os.path.realpath(os.path.expanduser(assembly_path.strip()))
    except (OSError, ValueError) as e:
        raise AssemblyPathError(f"Invalid path: {e}") from e

    # Check if path exists
    if not os.path.exists(resolved_path):
        raise AssemblyPathError(f"Assembly file not found: {resolved_path}")

    # Check if it's a regular file (not a directory)
    if not os.path.isfile(resolved_path):
        raise AssemblyPathError(f"Path is not a file: {resolved_path}")

    # Validate extension
    _, ext = os.path.splitext(resolved_path)
    if ext.lower() not in (".dll", ".exe"):
        raise AssemblyPathError(
            f"Invalid assembly extension '{ext}'. Expected .dll or .exe"
        )

    return resolved_path


def _compile_search_pattern(
    pattern: str, case_sensitive: bool, use_regex: bool
) -> tuple[re.Pattern | None, str | None]:
    """Compile a search pattern into a regex, handling errors.

    Args:
        pattern: The search pattern string
        case_sensitive: Whether matching should be case-sensitive
        use_regex: Whether to treat pattern as a regular expression

    Returns:
        Tuple of (compiled_pattern, error_message). If use_regex is False,
        returns (None, None). If regex compilation fails, returns
        (None, error_message). Otherwise returns (pattern, None).
    """
    if not use_regex:
        return None, None

    try:
        flags = 0 if case_sensitive else re.IGNORECASE
        return re.compile(pattern, flags), None
    except re.error as e:
        return None, f"Invalid regex pattern: {e}"


async def _check_dotnet_tools() -> dict:
    """Check status of dotnet CLI and ilspycmd tool."""
    result = {
        "dotnet_available": False,
        "dotnet_version": None,
        "ilspycmd_available": False,
        "ilspycmd_version": None,
        "ilspycmd_path": None,
    }

    # Check if dotnet CLI is available
    dotnet_path = shutil.which("dotnet")
    if dotnet_path:
        result["dotnet_available"] = True
        try:
            proc = await asyncio.create_subprocess_exec(
                "dotnet",
                "--version",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, _ = await proc.communicate()
            if proc.returncode == 0:
                result["dotnet_version"] = stdout.decode().strip()
        except (OSError, FileNotFoundError) as e:
            logger.debug(f"Could not check dotnet version: {e}")

    # Check if ilspycmd is available (check PATH and common locations)
    ilspy_path = find_ilspycmd_path()
    if ilspy_path:
        result["ilspycmd_available"] = True
        result["ilspycmd_path"] = ilspy_path
        try:
            proc = await asyncio.create_subprocess_exec(
                ilspy_path,
                "--version",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, _ = await proc.communicate()
            if proc.returncode == 0:
                result["ilspycmd_version"] = stdout.decode().strip()
        except (OSError, FileNotFoundError) as e:
            logger.debug(f"Could not check ilspycmd version: {e}")

    return result


def _detect_platform() -> dict:
    """Detect the platform and recommend the appropriate .NET SDK install command.

    Returns a dict with:
    - system: OS name (linux, darwin, windows)
    - distro: Distribution name if detected
    - package_manager: Package manager name
    - install_command: Human-readable command string (for display)
    - install_commands: List of command arg lists for safe execution (no shell)
    - needs_sudo: Whether installation requires elevated privileges
    """
    system = platform.system().lower()
    result = {
        "system": system,
        "distro": None,
        "package_manager": None,
        "install_command": None,
        "install_commands": None,  # List of [arg, list, ...] for subprocess_exec
        "needs_sudo": True,
    }

    if system == "linux":
        # Try to detect the Linux distribution
        try:
            with open("/etc/os-release") as f:
                os_release = f.read().lower()
                if "arch" in os_release or "manjaro" in os_release or "endeavour" in os_release:
                    result["distro"] = "arch"
                    result["package_manager"] = "pacman"
                    result["install_command"] = "sudo pacman -S --noconfirm dotnet-sdk"
                    result["install_commands"] = [
                        ["sudo", "pacman", "-S", "--noconfirm", "dotnet-sdk"]
                    ]
                elif "ubuntu" in os_release or "debian" in os_release or "mint" in os_release:
                    result["distro"] = "debian"
                    result["package_manager"] = "apt"
                    result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0"
                    result["install_commands"] = [
                        ["sudo", "apt", "update"],
                        ["sudo", "apt", "install", "-y", "dotnet-sdk-8.0"],
                    ]
                elif "fedora" in os_release or "rhel" in os_release or "centos" in os_release:
                    result["distro"] = "fedora"
                    result["package_manager"] = "dnf"
                    result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0"
                    result["install_commands"] = [
                        ["sudo", "dnf", "install", "-y", "dotnet-sdk-8.0"]
                    ]
                elif "opensuse" in os_release or "suse" in os_release:
                    result["distro"] = "suse"
                    result["package_manager"] = "zypper"
                    result["install_command"] = "sudo zypper install -y dotnet-sdk-8.0"
                    result["install_commands"] = [
                        ["sudo", "zypper", "install", "-y", "dotnet-sdk-8.0"]
                    ]
        except FileNotFoundError:
            logger.debug("Could not find /etc/os-release - using fallback detection")
        except PermissionError:
            logger.warning("Permission denied reading /etc/os-release - using fallback detection")
        except OSError as e:
            logger.warning(f"Error reading /etc/os-release: {e} - using fallback detection")

        # Fallback: check for common package managers
        if result["install_commands"] is None:
            if shutil.which("pacman"):
                result["package_manager"] = "pacman"
                result["install_command"] = "sudo pacman -S --noconfirm dotnet-sdk"
                result["install_commands"] = [
                    ["sudo", "pacman", "-S", "--noconfirm", "dotnet-sdk"]
                ]
            elif shutil.which("apt"):
                result["package_manager"] = "apt"
                result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0"
                result["install_commands"] = [
                    ["sudo", "apt", "update"],
                    ["sudo", "apt", "install", "-y", "dotnet-sdk-8.0"],
                ]
            elif shutil.which("dnf"):
                result["package_manager"] = "dnf"
                result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0"
                result["install_commands"] = [
                    ["sudo", "dnf", "install", "-y", "dotnet-sdk-8.0"]
                ]

    elif system == "darwin":
        result["distro"] = "macos"
        if shutil.which("brew"):
            result["package_manager"] = "homebrew"
            result["install_command"] = "brew install dotnet-sdk"
            result["install_commands"] = [["brew", "install", "dotnet-sdk"]]
            result["needs_sudo"] = False
        else:
            result["install_command"] = (
                "Download from https://dotnet.microsoft.com/download or install Homebrew first"
            )

    elif system == "windows":
        result["distro"] = "windows"
        if shutil.which("winget"):
            result["package_manager"] = "winget"
            result["install_command"] = "winget install Microsoft.DotNet.SDK.8"
            result["install_commands"] = [
                ["winget", "install", "Microsoft.DotNet.SDK.8", "--accept-source-agreements"]
            ]
            result["needs_sudo"] = False
        elif shutil.which("choco"):
            result["package_manager"] = "chocolatey"
            result["install_command"] = "choco install dotnet-sdk -y"
            result["install_commands"] = [["choco", "install", "dotnet-sdk", "-y"]]
        else:
            result["install_command"] = "Download from https://dotnet.microsoft.com/download"
            result["needs_sudo"] = False

    # Final fallback
    if result["install_command"] is None:
        result["install_command"] = "Download from https://dotnet.microsoft.com/download"
        result["needs_sudo"] = False

    return result


async def _try_install_dotnet_sdk(ctx: Context | None = None) -> tuple[bool, str]:
    """Attempt to install the .NET SDK using the detected package manager.

    Returns (success, message) tuple.

    Security: Uses create_subprocess_exec with explicit argument lists
    to avoid shell injection vulnerabilities.
    """
    platform_info = _detect_platform()
    commands = platform_info.get("install_commands")
    display_cmd = platform_info.get("install_command", "")

    if not commands:
        return False, (
            f"Cannot auto-install .NET SDK on {platform_info['system']}.\n\n"
            f"Please install manually: {display_cmd}"
        )

    if ctx:
        await ctx.info(f"Installing .NET SDK via {platform_info['package_manager']}...")

    all_output = []
    try:
        # Execute each command in sequence (e.g., apt update, then apt install)
        for cmd_args in commands:
            if ctx:
                await ctx.info(f"Running: {' '.join(cmd_args)}")

            proc = await asyncio.create_subprocess_exec(
                *cmd_args,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await proc.communicate()
            output = stdout.decode() + stderr.decode()
            all_output.append(f"$ {' '.join(cmd_args)}\n{output}")

            if proc.returncode != 0:
                return False, (
                    f"❌ Installation failed (exit code {proc.returncode}).\n\n"
                    f"Command: `{' '.join(cmd_args)}`\n\n"
                    f"Output:\n```\n{output[-MAX_ERROR_OUTPUT_CHARS:]}\n```\n\n"
                    "Try running the command manually with sudo if needed."
                )

        return True, f"✅ .NET SDK installed successfully via {platform_info['package_manager']}!"

    except Exception as e:
        return False, f"❌ Failed to run install command: {e}\n\nTry manually: `{display_cmd}`"


@mcp.tool()
async def check_ilspy_installation(
    ctx: Context | None = None,
) -> str:
    """Check if ilspycmd is installed and show installation status.

    Returns information about:
    - Whether dotnet CLI is available
    - Whether ilspycmd is installed
    - Version information for both tools
    - Instructions for installing if missing

    Use this tool to diagnose issues with decompilation tools.
    """
    if ctx:
        await ctx.info("Checking ILSpy installation status...")

    status = await _check_dotnet_tools()

    content = "# ILSpy Installation Status\n\n"

    # dotnet status
    if status["dotnet_available"]:
        content += f"✅ **dotnet CLI**: Installed (v{status['dotnet_version']})\n"
    else:
        content += "❌ **dotnet CLI**: Not found\n"
        content += "   - Install from: https://dotnet.microsoft.com/download\n\n"
        return content

    # ilspycmd status
    if status["ilspycmd_available"]:
        content += "✅ **ilspycmd**: Installed"
        if status["ilspycmd_version"]:
            content += f" (v{status['ilspycmd_version']})"
        content += f"\n   - Path: `{status['ilspycmd_path']}`\n"
        content += "\n🎉 All ILSpy-based tools are ready to use!"
    else:
        content += "❌ **ilspycmd**: Not installed\n\n"
        content += "To install, use the `install_ilspy` tool or run manually:\n"
        content += "```bash\n"
        content += "dotnet tool install --global ilspycmd\n"
        content += "```\n"
        content += (
            "\n**Note**: The direct metadata tools (`search_methods`, `search_fields`, etc.) "
        )
        content += "work without ilspycmd."

    return content


@mcp.tool()
async def install_ilspy(
    update: bool = False,
    install_dotnet_sdk: bool = False,
    ctx: Context | None = None,
) -> str:
    """Install or update ilspycmd, the ILSpy command-line decompiler.

    This tool handles the complete installation process:
    - Detects your platform and package manager
    - Optionally installs the .NET SDK if missing (`install_dotnet_sdk=True`)
    - Installs ilspycmd globally
    - Updates to the latest version if `update=True`

    After installation, all ILSpy-based decompilation tools will be available.

    Args:
        update: If True, update ilspycmd to the latest version even if already installed
        install_dotnet_sdk: If True, attempt to install .NET SDK when missing (may require sudo)
    """
    if ctx:
        await ctx.info("Checking prerequisites...")

    status = await _check_dotnet_tools()

    if not status["dotnet_available"]:
        platform_info = _detect_platform()

        if install_dotnet_sdk:
            # Attempt to install the .NET SDK
            if ctx:
                await ctx.info("dotnet CLI not found. Attempting to install .NET SDK...")

            success, message = await _try_install_dotnet_sdk(ctx)

            if success:
                # Re-check status after SDK installation
                status = await _check_dotnet_tools()
                if not status["dotnet_available"]:
                    return (
                        f"{message}\n\n"
                        "However, dotnet is still not found in PATH.\n"
                        "You may need to restart your terminal or add dotnet to PATH."
                    )
            else:
                return message
        else:
            # Provide platform-specific instructions
            cmd = platform_info.get("install_command", "Download from https://dotnet.microsoft.com/download")

            return (
                "❌ **Error**: dotnet CLI is not installed.\n\n"
                f"**Detected platform**: {platform_info['system']}"
                + (f" ({platform_info['distro']})" if platform_info.get("distro") else "")
                + "\n\n"
                f"**Recommended installation**:\n```bash\n{cmd}\n```\n\n"
                "**Or** call this tool with `install_dotnet_sdk=True` to attempt automatic installation:\n"
                "```json\n"
                '{"install_dotnet_sdk": true}\n'
                "```\n\n"
                "**Alternative**: Download manually from https://dotnet.microsoft.com/download"
            )

    # Check if already installed and not updating
    if status["ilspycmd_available"] and not update:
        version_info = f" (v{status['ilspycmd_version']})" if status["ilspycmd_version"] else ""
        return (
            f"✅ **ilspycmd** is already installed{version_info}.\n\n"
            f"Path: `{status['ilspycmd_path']}`\n\n"
            "To update to the latest version, call this tool with `update=True`."
        )

    # Determine command
    if status["ilspycmd_available"] and update:
        cmd = ["dotnet", "tool", "update", "--global", "ilspycmd"]
        action = "Updating"
    else:
        cmd = ["dotnet", "tool", "install", "--global", "ilspycmd"]
        action = "Installing"

    if ctx:
        await ctx.info(f"{action} ilspycmd...")

    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()

        output = stdout.decode() + stderr.decode()

        if proc.returncode == 0:
            # Verify installation
            new_status = await _check_dotnet_tools()
            if new_status["ilspycmd_available"]:
                version_info = (
                    f" v{new_status['ilspycmd_version']}" if new_status["ilspycmd_version"] else ""
                )
                return (
                    f"✅ **Success!** ilspycmd{version_info} has been {'updated' if update else 'installed'}.\n\n"
                    f"Path: `{new_status['ilspycmd_path']}`\n\n"
                    "All ILSpy-based decompilation tools are now available:\n"
                    "- `decompile_assembly` (with PDB generation support)\n"
                    "- `list_types`\n"
                    "- `search_types`\n"
                    "- `search_strings`\n"
                    "- `generate_diagrammer`\n"
                    "- `dump_package`\n"
                    "- `get_assembly_info`"
                )
            else:
                return (
                    f"⚠️ Installation appeared to succeed but ilspycmd is not found in PATH.\n\n"
                    f"Output:\n```\n{output}\n```\n\n"
                    "You may need to:\n"
                    "1. Add `~/.dotnet/tools` to your PATH\n"
                    "2. Restart your terminal/shell\n"
                    '3. For zsh: `export PATH="$PATH:$HOME/.dotnet/tools"`\n'
                    '4. For bash: `export PATH="$PATH:$HOME/.dotnet/tools"`'
                )
        else:
            return (
                f"❌ **Installation failed** (exit code {proc.returncode})\n\n"
                f"Output:\n```\n{output}\n```\n\n"
                "Try running manually:\n"
                "```bash\n"
                f"{' '.join(cmd)}\n"
                "```"
            )

    except Exception as e:
        logger.error(f"Error installing ilspycmd: {e}")
        return _format_error(e, "installation")


@mcp.tool()
async def decompile_assembly(
    assembly_path: str,
    output_dir: str | None = None,
    type_name: str | None = None,
    language_version: str = "Latest",
    create_project: bool = False,
    show_il_code: bool = False,
    remove_dead_code: bool = False,
    remove_dead_stores: bool = False,
    show_il_sequence_points: bool = False,
    nested_directories: bool = False,
    generate_pdb: bool = False,
    use_pdb_variable_names: bool = False,
    max_output_chars: int = DEFAULT_MAX_OUTPUT_CHARS,
    ctx: Context | None = None,
) -> str:
    """Decompile a .NET assembly to readable C# source code.

    This is the primary tool for reverse-engineering .NET binaries. Use it to:
    - Decompile entire assemblies to understand their structure
    - Extract specific types by fully qualified name
    - Generate compilable project structures for analysis
    - View IL (Intermediate Language) code for low-level analysis
    - Generate PDB files for debugging decompiled code

    WORKFLOW TIP: Start with `list_types` to discover available types, then use
    this tool with `type_name` to decompile specific classes of interest.
    If a type is very large, use `decompile_method` to extract individual methods.

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        output_dir: Directory to save decompiled files. If omitted, returns source code directly
        type_name: Fully qualified type name (e.g., "MyNamespace.MyClass") to decompile only that type
        language_version: C# version for output syntax. Options: CSharp1-CSharp12_0, Preview, Latest (default)
        create_project: Generate a full .csproj project structure with multiple files (useful for large assemblies)
        show_il_code: Output IL bytecode instead of C# (useful for understanding low-level behavior)
        remove_dead_code: Strip unreachable code paths from output
        remove_dead_stores: Strip unused variable assignments from output
        show_il_sequence_points: Include debugging sequence points in IL output (implies show_il_code)
        nested_directories: Organize output files in namespace-based directory hierarchy
        generate_pdb: Generate a portable PDB file (requires output_dir). Enables debugging of decompiled code
        use_pdb_variable_names: Use original variable names from existing PDB if available. Improves readability
        max_output_chars: Maximum characters to return inline (default: 100000). Output exceeding this limit is saved to a temp file and a truncated preview is returned. Set to 0 to disable truncation.
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Starting decompilation of assembly: {validated_path}")

    try:
        wrapper = get_wrapper(ctx)

        # Use simplified request object (no complex pydantic validation needed)
        from .models import DecompileRequest

        # Validate language version with helpful error message
        try:
            lang_ver = LanguageVersion(language_version)
        except ValueError:
            valid_versions = [v.value for v in LanguageVersion]
            return (
                f"Invalid language version: '{language_version}'\n\n"
                f"Valid options: {', '.join(valid_versions)}"
            )

        request = DecompileRequest(
            assembly_path=validated_path,
            output_dir=output_dir,
            type_name=type_name,
            language_version=lang_ver,
            create_project=create_project,
            show_il_code=show_il_code or show_il_sequence_points,
            remove_dead_code=remove_dead_code,
            remove_dead_stores=remove_dead_stores,
            show_il_sequence_points=show_il_sequence_points,
            nested_directories=nested_directories,
            generate_pdb=generate_pdb,
            use_pdb_variable_names=use_pdb_variable_names,
        )

        response = await wrapper.decompile(request)

        if response.success:
            if response.source_code:
                code_fence = "il" if (show_il_code or show_il_sequence_points) else "csharp"
                source = response.source_code
                source_len = len(source)

                # Check if truncation is needed
                if max_output_chars > 0 and source_len > max_output_chars:
                    # Save full output to a temp file so the client can access it
                    suffix = ".il" if code_fence == "il" else ".cs"
                    tmp = tempfile.NamedTemporaryFile(  # noqa: SIM115
                        delete=False,
                        prefix="mcilspy_full_output_",
                        suffix=suffix,
                        mode="w",
                        encoding="utf-8",
                    )
                    tmp.write(source)
                    tmp.close()
                    saved_path = tmp.name

                    logger.info(
                        f"Output truncated: {source_len} chars -> {max_output_chars} chars. "
                        f"Full output saved to {saved_path}"
                    )

                    # Build truncated response with recovery info
                    preview_chars = max_output_chars - 1000  # Reserve space for header
                    if preview_chars < 0:
                        preview_chars = max_output_chars

                    content = f"# Decompilation result: {response.assembly_name}"
                    if response.type_name:
                        content += f" - {response.type_name}"
                    content += (
                        f"\n\n**Output truncated** ({source_len:,} chars exceeded "
                        f"{max_output_chars:,} char limit)\n\n"
                        f"Full output saved to: `{saved_path}`\n\n"
                        "**Recovery options:**\n"
                        "- Read the full file directly from the path above\n"
                        "- Use `decompile_method` to extract specific methods\n"
                        "- Call again with `max_output_chars=0` to disable truncation\n"
                        f"- Call again with a larger `max_output_chars` value\n\n"
                        f"**Preview** (first {preview_chars:,} chars):\n\n"
                        f"```{code_fence}\n{source[:preview_chars]}\n```"
                    )
                    return content

                # Normal case: output fits within limit
                content = f"# Decompilation result: {response.assembly_name}"
                if response.type_name:
                    content += f" - {response.type_name}"
                content += f"\n\n```{code_fence}\n{source}\n```"
                return content
            else:
                return f"Decompilation successful! Files saved to: {response.output_path}"
        else:
            return f"Decompilation failed: {response.error_message}"

    except Exception as e:
        logger.error(f"Decompilation error: {e}")
        return _format_error(e)


@mcp.tool()
async def decompile_method(
    assembly_path: str,
    type_name: str,
    method_name: str,
    show_il_code: bool = True,
    language_version: str = "Latest",
    use_pdb_variable_names: bool = False,
    ctx: Context | None = None,
) -> str:
    """Decompile a specific method from a .NET type.

    When a type is too large to return in full, use this tool to extract just
    the method you need. It decompiles the containing type, then extracts the
    named method from the output.

    IL mode (default) is most reliable because IL output has standard ECMA-335
    delimiters. C# mode uses signature matching with brace-depth counting.

    If multiple overloads exist, all matching methods are returned.

    WORKFLOW TIP: Use `search_methods` to find method names, then this tool
    to get the full implementation. If decompile_assembly hit the output limit,
    this tool extracts just what you need.

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        type_name: Fully qualified type name (e.g., "MyNamespace.MyClass"). Required to scope the decompilation
        method_name: Name of the method to extract (e.g., "DoElaborate"). All overloads with this name are returned
        show_il_code: Output IL bytecode instead of C# (default: True, more reliable for extraction)
        language_version: C# version for output syntax. Options: CSharp1-CSharp12_0, Preview, Latest (default)
        use_pdb_variable_names: Use original variable names from existing PDB if available
    """
    # Validate assembly path
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if not type_name or not type_name.strip():
        return _format_error(ValueError("type_name is required"), "validation")

    if not method_name or not method_name.strip():
        return _format_error(ValueError("method_name is required"), "validation")

    if ctx:
        await ctx.info(
            f"Extracting method '{method_name}' from type '{type_name}' "
            f"in: {validated_path}"
        )

    try:
        wrapper = get_wrapper(ctx)

        # Validate language version
        try:
            lang_ver = LanguageVersion(language_version)
        except ValueError:
            valid_versions = [v.value for v in LanguageVersion]
            return (
                f"Invalid language version: '{language_version}'\n\n"
                f"Valid options: {', '.join(valid_versions)}"
            )

        from .models import DecompileRequest

        request = DecompileRequest(
            assembly_path=validated_path,
            type_name=type_name.strip(),
            language_version=lang_ver,
            show_il_code=show_il_code,
            use_pdb_variable_names=use_pdb_variable_names,
        )

        response = await wrapper.decompile(request)

        if not response.success:
            return f"Decompilation failed: {response.error_message}"

        if not response.source_code:
            return f"No source code returned for type '{type_name}'"

        # Extract the method from the full type output
        clean_method_name = method_name.strip()
        if show_il_code:
            methods = extract_il_method(response.source_code, clean_method_name)
            code_fence = "il"
        else:
            methods = extract_csharp_method(response.source_code, clean_method_name)
            code_fence = "csharp"

        if not methods:
            return (
                f"Method '{clean_method_name}' not found in type '{type_name}'.\n\n"
                "Possible causes:\n"
                "- Method name is misspelled (matching is exact)\n"
                "- Method is inherited from a base type (try decompiling the base type)\n"
                "- Method is a compiler-generated accessor (try the property name)\n\n"
                "**TIP**: Use `search_methods` with `type_filter` to find available methods."
            )

        # Format output
        overload_note = ""
        if len(methods) > 1:
            overload_note = f" ({len(methods)} overloads)"

        content = (
            f"# Method: {clean_method_name}{overload_note}\n"
            f"**Type**: `{type_name}` | **Assembly**: `{response.assembly_name}`\n\n"
        )

        for i, method_block in enumerate(methods):
            if len(methods) > 1:
                content += f"### Overload {i + 1}\n\n"
            content += f"```{code_fence}\n{method_block}\n```\n\n"

        return content.rstrip()

    except Exception as e:
        logger.error(f"Method extraction error: {e}")
        return _format_error(e)


@mcp.tool()
async def list_types(
    assembly_path: str,
    entity_types: list[str] | None = None,
    max_results: int = 1000,
    offset: int = 0,
    ctx: Context | None = None,
) -> str:
    """List all types (classes, interfaces, structs, etc.) in a .NET assembly.

    This is typically the FIRST tool to use when analyzing an unknown assembly.
    It reveals the assembly's structure and helps identify types worth decompiling.

    Results are organized by namespace for easy navigation. Use this to discover:
    - Main application classes and their namespaces
    - Interface contracts and service definitions
    - Data structures (structs) and enumerations
    - Event delegates and callbacks

    WORKFLOW TIP: After listing types, use `decompile_assembly` with `type_name`
    to examine specific classes that look interesting.

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        entity_types: Types to include in listing. Accepts full names or single letters:
            - "class" or "c" (default if omitted)
            - "interface" or "i"
            - "struct" or "s"
            - "delegate" or "d"
            - "enum" or "e"
            Example: ["class", "interface"] or ["c", "i"]
        max_results: Maximum number of results to return (default: 1000)
        offset: Number of results to skip for pagination (default: 0)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Listing types in assembly: {validated_path}")

    try:
        wrapper = get_wrapper(ctx)

        # Default to list only classes
        if entity_types is None:
            entity_types = ["class"]

        # Convert to EntityType enums using the flexible from_string method
        entity_type_enums = []
        for et in entity_types:
            try:
                entity_type_enums.append(EntityType.from_string(et))
            except ValueError as e:
                logger.warning(f"Skipping invalid entity type: {e}")
                continue

        from .models import ListTypesRequest

        request = ListTypesRequest(assembly_path=validated_path, entity_types=entity_type_enums)

        response = await wrapper.list_types(request)

        if response.success and response.types:
            all_types = response.types
            total_count = len(all_types)

            # Apply pagination
            paginated_types = all_types[offset : offset + max_results]
            has_more = (offset + max_results) < total_count

            content = f"# Types in {validated_path}\n\n"
            content += f"Showing {len(paginated_types)} of {total_count} types"
            if offset > 0:
                content += f" (offset: {offset})"
            if has_more:
                content += " - more results available"
            content += ":\n\n"

            # Group by namespace
            by_namespace: dict[str, list] = {}
            for type_info in paginated_types:
                ns = type_info.namespace or "(Global)"
                if ns not in by_namespace:
                    by_namespace[ns] = []
                by_namespace[ns].append(type_info)

            for namespace, types in sorted(by_namespace.items()):
                content += f"## {namespace}\n\n"
                for type_info in sorted(types, key=lambda t: t.name):
                    content += f"- **{type_info.name}** ({type_info.kind})\n"
                    content += f"  - Full name: `{type_info.full_name}`\n"
                content += "\n"

            if has_more:
                next_offset = offset + max_results
                content += f"\n**More results available.** Use `offset={next_offset}` to see the next page.\n"

            return content
        else:
            return response.error_message or "No types found in assembly"

    except Exception as e:
        logger.error(f"Error listing types: {e}")
        return _format_error(e)


@mcp.tool()
async def generate_diagrammer(
    assembly_path: str,
    output_dir: str | None = None,
    include_pattern: str | None = None,
    exclude_pattern: str | None = None,
    ctx: Context | None = None,
) -> str:
    """Generate an interactive HTML diagram showing assembly type relationships.

    Creates a visual class diagram that can be opened in any web browser.
    Useful for understanding complex assemblies with many interdependent types.

    The diagram shows:
    - Type inheritance hierarchies
    - Interface implementations
    - Type relationships and dependencies
    - Namespace organization

    Use regex patterns to focus on specific namespaces or exclude generated code.

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        output_dir: Where to save the HTML file. Defaults to "diagrammer" folder next to assembly
        include_pattern: Regex to whitelist types (e.g., "MyApp\\\\.Services\\\\..+" for Services namespace)
        exclude_pattern: Regex to blacklist types (e.g., ".*Generated.*" to hide generated code)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Generating assembly diagram: {validated_path}")

    try:
        wrapper = get_wrapper(ctx)

        from .models import GenerateDiagrammerRequest

        request = GenerateDiagrammerRequest(
            assembly_path=validated_path,
            output_dir=output_dir,
            include_pattern=include_pattern,
            exclude_pattern=exclude_pattern,
        )

        response = await wrapper.generate_diagrammer(request)

        if response["success"]:
            return f"HTML diagram generated successfully!\nOutput directory: {response['output_directory']}\nOpen the HTML file in a web browser to view the interactive diagram."
        else:
            return f"Failed to generate diagram: {response['error_message']}"

    except Exception as e:
        logger.error(f"Error generating diagram: {e}")
        return _format_error(e)


@mcp.tool()
async def dump_package(
    assembly_path: str,
    output_dir: str,
    ctx: Context | None = None,
) -> str:
    """Dump package assemblies into a folder.

    Extracts all assemblies from a NuGet package folder structure into a flat
    directory for easier analysis. This is useful for:
    - Bulk decompilation of NuGet packages
    - Analyzing package dependencies
    - Extracting DLLs from complex package structures

    The output directory will contain all .dll and .exe files found in the
    package structure, making them easier to analyze with other tools.

    Args:
        assembly_path: Path to the assembly or NuGet package folder to extract from
        output_dir: Directory to dump assemblies into (will be created if needed)
    """
    # Basic path validation - dump_package can work with directories too
    if not assembly_path or not assembly_path.strip():
        return _format_error(ValueError("Assembly path cannot be empty"), "path validation")

    resolved_path = os.path.realpath(os.path.expanduser(assembly_path.strip()))
    if not os.path.exists(resolved_path):
        return _format_error(FileNotFoundError(f"Path not found: {resolved_path}"), "path validation")

    if not output_dir or not output_dir.strip():
        return _format_error(ValueError("Output directory is required"), "validation")

    if ctx:
        await ctx.info(f"Dumping package assemblies from: {resolved_path}")

    try:
        wrapper = get_wrapper(ctx)

        from .models import DumpPackageRequest

        request = DumpPackageRequest(
            assembly_path=resolved_path,
            output_dir=output_dir.strip(),
        )

        response = await wrapper.dump_package(request)

        if response.success:
            content = f"# Package Dump Complete\n\n"
            content += f"**Output directory**: `{response.output_path}`\n\n"

            if response.assemblies_dumped:
                content += f"**Assemblies extracted** ({len(response.assemblies_dumped)}):\n\n"
                for asm in response.assemblies_dumped:
                    content += f"- `{asm}`\n"
            else:
                content += "No assemblies found in package.\n"

            content += "\n**TIP**: Use `list_types` or `decompile_assembly` on any of these assemblies for analysis."
            return content
        else:
            return f"Failed to dump package: {response.error_message}"

    except Exception as e:
        logger.error(f"Error dumping package: {e}")
        return _format_error(e)


@mcp.tool()
async def get_assembly_info(assembly_path: str, ctx: Context | None = None) -> str:
    """Get metadata and version information about a .NET assembly.

    Returns detailed assembly metadata useful for understanding:
    - Assembly name and version (for compatibility checks)
    - Target framework (.NET Framework, .NET Core, .NET 5+, etc.)
    - Whether the assembly is signed (important for trust verification)
    - Debug information availability (PDB files present)

    This is a quick reconnaissance tool - use it first to understand what
    kind of assembly you're dealing with before deep-diving with other tools.

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Getting assembly info: {validated_path}")

    try:
        wrapper = get_wrapper(ctx)

        from .models import AssemblyInfoRequest

        request = AssemblyInfoRequest(assembly_path=validated_path)

        info = await wrapper.get_assembly_info(request)

        content = "# Assembly Information\n\n"
        content += f"- **Name**: {info.name}\n"
        content += f"- **Full Name**: {info.full_name}\n"
        content += f"- **Location**: {info.location}\n"
        content += f"- **Version**: {info.version}\n"
        if info.target_framework:
            content += f"- **Target Framework**: {info.target_framework}\n"
        if info.runtime_version:
            content += f"- **Runtime Version**: {info.runtime_version}\n"
        content += f"- **Is Signed**: {info.is_signed}\n"
        content += f"- **Has Debug Info**: {info.has_debug_info}\n"

        return content

    except Exception as e:
        logger.error(f"Error getting assembly info: {e}")
        return _format_error(e)


@mcp.tool()
async def search_types(
    assembly_path: str,
    pattern: str,
    namespace_filter: str | None = None,
    entity_types: list[str] | None = None,
    case_sensitive: bool = False,
    use_regex: bool = False,
    max_results: int = 1000,
    offset: int = 0,
    ctx: Context | None = None,
) -> str:
    """Search for types in an assembly by name pattern.

    Essential for finding specific classes, interfaces, or services in large assemblies.
    Much faster than listing all types when you know what you're looking for.

    Common search patterns:
    - "Service" - Find all service classes
    - "Controller" - Find ASP.NET controllers
    - "Handler" - Find command/event handlers
    - "Exception" - Find custom exception types
    - "I*Service" (with use_regex=True) - Find service interfaces

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        pattern: Search pattern to match against type names. Matches if type name contains this string
        namespace_filter: Only return types in namespaces containing this string (e.g., "MyApp.Services")
        entity_types: Types to search. Accepts: "class", "interface", "struct", "delegate", "enum" (default: all)
        case_sensitive: Whether pattern matching is case-sensitive (default: False)
        use_regex: Treat pattern as regular expression (default: False)
        max_results: Maximum number of results to return (default: 1000)
        offset: Number of results to skip for pagination (default: 0)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Searching for types matching '{pattern}' in: {validated_path}")

    try:
        wrapper = get_wrapper(ctx)

        # Default to search all entity types
        if entity_types is None:
            entity_types = ALL_ENTITY_TYPES.copy()

        # Convert to EntityType enums
        entity_type_enums = []
        for et in entity_types:
            try:
                entity_type_enums.append(EntityType.from_string(et))
            except ValueError as e:
                logger.warning(f"Skipping invalid entity type: {e}")
                continue

        from .models import ListTypesRequest

        request = ListTypesRequest(assembly_path=validated_path, entity_types=entity_type_enums)
        response = await wrapper.list_types(request)

        if not response.success:
            return response.error_message or "Failed to list types"

        # Compile regex if needed
        search_pattern, regex_error = _compile_search_pattern(pattern, case_sensitive, use_regex)
        if regex_error:
            return regex_error

        # Filter types by pattern and namespace
        matching_types = []
        for type_info in response.types:
            # Check namespace filter first
            if namespace_filter:
                if type_info.namespace is None:
                    continue
                ns_match = namespace_filter.lower() in type_info.namespace.lower()
                if not ns_match:
                    continue

            # Check name pattern
            name_to_check = type_info.name
            if use_regex and search_pattern:
                if not search_pattern.search(name_to_check):
                    continue
            elif case_sensitive:
                if pattern not in name_to_check:
                    continue
            else:
                if pattern.lower() not in name_to_check.lower():
                    continue

            matching_types.append(type_info)

        if not matching_types:
            return f"No types found matching pattern '{pattern}'"

        # Apply pagination
        total_count = len(matching_types)
        paginated_types = matching_types[offset : offset + max_results]
        has_more = (offset + max_results) < total_count

        # Format results
        content = f"# Search Results for '{pattern}'\n\n"
        content += f"Showing {len(paginated_types)} of {total_count} matching types"
        if offset > 0:
            content += f" (offset: {offset})"
        if has_more:
            content += " - more results available"
        content += ":\n\n"

        # Group by namespace
        by_namespace: dict[str, list] = {}
        for type_info in paginated_types:
            ns = type_info.namespace or "(Global)"
            if ns not in by_namespace:
                by_namespace[ns] = []
            by_namespace[ns].append(type_info)

        for namespace, types in sorted(by_namespace.items()):
            content += f"## {namespace}\n\n"
            for type_info in sorted(types, key=lambda t: t.name):
                content += f"- **{type_info.name}** ({type_info.kind})\n"
                content += f"  - Full name: `{type_info.full_name}`\n"
            content += "\n"

        if has_more:
            next_offset = offset + max_results
            content += f"\n**More results available.** Use `offset={next_offset}` to see the next page.\n"

        content += "\n**TIP**: Use `decompile_assembly` with `type_name` set to the full name to examine any of these types."
        return content

    except Exception as e:
        logger.error(f"Error searching types: {e}")
        return _format_error(e)


@mcp.tool()
async def search_strings(
    assembly_path: str,
    pattern: str,
    case_sensitive: bool = False,
    use_regex: bool = False,
    max_results: int = DEFAULT_MAX_SEARCH_RESULTS,
    ctx: Context | None = None,
) -> str:
    """Search for string literals in assembly code.

    Crucial for reverse engineering - finds hardcoded strings like:
    - URLs, API endpoints, and connection strings
    - Error messages and logging text
    - Configuration keys and magic values
    - Hardcoded credentials (security analysis)
    - Registry keys and file paths

    This uses the fast #US (User Strings) heap search which reads directly
    from metadata without decompilation. This is typically 10-100x faster
    than the previous approach of decompiling to IL.

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        pattern: String pattern to search for in the assembly's strings
        case_sensitive: Whether search is case-sensitive (default: False)
        use_regex: Treat pattern as regular expression (default: False)
        max_results: Maximum number of matches to return (default: 100)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Searching for strings matching '{pattern}' in: {validated_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(validated_path) as reader:
            matches = reader.search_user_strings(
                pattern=pattern,
                case_sensitive=case_sensitive,
                use_regex=use_regex,
                max_results=max_results,
            )

        if not matches:
            return f"No strings found matching pattern '{pattern}'"

        # Format results
        content = f"# String Search Results for '{pattern}'\n\n"
        content += f"Found {len(matches)} matches"
        if len(matches) >= max_results:
            content += f" (limited to {max_results}, use `max_results` to increase)"
        content += ":\n\n"

        for match in matches:
            # Truncate long strings for display
            display_value = match.value
            if len(display_value) > 200:
                display_value = display_value[:197] + "..."

            # Escape backticks in the string
            display_value = display_value.replace("`", "\\`")

            content += f"- `{display_value}`\n"

        content += "\n**TIP**: Use `decompile_assembly` with `-il` option to find which methods use these strings."
        return content

    except FileNotFoundError as e:
        return _format_error(e)
    except ValueError as e:
        # Invalid regex pattern
        return _format_error(e)
    except Exception as e:
        logger.exception(f"Error searching strings: {e}")
        return _format_error(e)


# ============================================================================
# Direct Metadata Tools (using dnfile - no ilspycmd required)
# ============================================================================


@mcp.tool()
async def search_methods(
    assembly_path: str,
    pattern: str,
    type_filter: str | None = None,
    namespace_filter: str | None = None,
    public_only: bool = False,
    case_sensitive: bool = False,
    use_regex: bool = False,
    max_results: int = 1000,
    offset: int = 0,
    ctx: Context | None = None,
) -> str:
    """Search for methods in an assembly by name pattern.

    Uses direct metadata parsing (no ilspycmd required) to find methods.
    This searches the MethodDef metadata table directly.

    Useful for finding:
    - Entry points and main methods
    - Event handlers (OnClick, Handle*, etc.)
    - Lifecycle methods (Initialize, Dispose, etc.)
    - API endpoints and controllers

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        pattern: Search pattern to match against method names
        type_filter: Only search methods in types containing this string
        namespace_filter: Only search in namespaces containing this string
        public_only: Only return public methods (default: False)
        case_sensitive: Whether pattern matching is case-sensitive (default: False)
        use_regex: Treat pattern as regular expression (default: False)
        max_results: Maximum number of results to return (default: 1000)
        offset: Number of results to skip for pagination (default: 0)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Searching for methods matching '{pattern}' in: {validated_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(validated_path) as reader:
            methods = reader.list_methods(
                type_filter=type_filter,
                namespace_filter=namespace_filter,
                public_only=public_only,
            )

        if not methods:
            return "No methods found in assembly (or assembly has no metadata)"

        # Compile regex if needed
        search_pattern, regex_error = _compile_search_pattern(pattern, case_sensitive, use_regex)
        if regex_error:
            return regex_error

        # Filter by pattern
        matching_methods = []
        for method in methods:
            if use_regex and search_pattern:
                if not search_pattern.search(method.name):
                    continue
            elif case_sensitive:
                if pattern not in method.name:
                    continue
            else:
                if pattern.lower() not in method.name.lower():
                    continue
            matching_methods.append(method)

        if not matching_methods:
            return f"No methods found matching pattern '{pattern}'"

        # Apply pagination
        total_count = len(matching_methods)
        paginated_methods = matching_methods[offset : offset + max_results]
        has_more = (offset + max_results) < total_count

        # Format results
        content = f"# Method Search Results for '{pattern}'\n\n"
        content += f"Showing {len(paginated_methods)} of {total_count} matching methods"
        if offset > 0:
            content += f" (offset: {offset})"
        if has_more:
            content += " - more results available"
        content += ":\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for method in paginated_methods:
            key = (
                f"{method.namespace}.{method.declaring_type}"
                if method.namespace
                else method.declaring_type
            )
            if key not in by_type:
                by_type[key] = []
            by_type[key].append(method)

        for type_name, type_methods in sorted(by_type.items()):
            content += f"## {type_name}\n\n"
            for method in sorted(type_methods, key=lambda m: m.name):
                modifiers = []
                if method.is_public:
                    modifiers.append("public")
                if method.is_static:
                    modifiers.append("static")
                if method.is_virtual:
                    modifiers.append("virtual")
                if method.is_abstract:
                    modifiers.append("abstract")
                mod_str = " ".join(modifiers) + " " if modifiers else ""
                content += f"- `{mod_str}{method.name}()`\n"
            content += "\n"

        if has_more:
            next_offset = offset + max_results
            content += f"\n**More results available.** Use `offset={next_offset}` to see the next page.\n"

        content += (
            "\n**TIP**: Use `decompile_assembly` with `type_name` to see the full implementation."
        )
        return content

    except FileNotFoundError as e:
        return _format_error(e)
    except Exception as e:
        logger.error(f"Error searching methods: {e}")
        return _format_error(e)


@mcp.tool()
async def search_fields(
    assembly_path: str,
    pattern: str,
    type_filter: str | None = None,
    namespace_filter: str | None = None,
    public_only: bool = False,
    constants_only: bool = False,
    case_sensitive: bool = False,
    use_regex: bool = False,
    max_results: int = 1000,
    offset: int = 0,
    ctx: Context | None = None,
) -> str:
    """Search for fields in an assembly by name pattern.

    Uses direct metadata parsing to find fields and constants.
    Useful for finding:
    - Configuration values and magic numbers
    - Constant strings (error messages, URLs)
    - Static fields (singletons, caches)

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        pattern: Search pattern to match against field names
        type_filter: Only search fields in types containing this string
        namespace_filter: Only search in namespaces containing this string
        public_only: Only return public fields (default: False)
        constants_only: Only return constant (literal) fields (default: False)
        case_sensitive: Whether pattern matching is case-sensitive (default: False)
        use_regex: Treat pattern as regular expression (default: False)
        max_results: Maximum number of results to return (default: 1000)
        offset: Number of results to skip for pagination (default: 0)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Searching for fields matching '{pattern}' in: {validated_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(validated_path) as reader:
            fields = reader.list_fields(
                type_filter=type_filter,
                namespace_filter=namespace_filter,
                public_only=public_only,
                constants_only=constants_only,
            )

        if not fields:
            return "No fields found in assembly"

        # Compile regex if needed
        search_pattern, regex_error = _compile_search_pattern(pattern, case_sensitive, use_regex)
        if regex_error:
            return regex_error

        # Filter by pattern
        matching_fields = []
        for field in fields:
            if use_regex and search_pattern:
                if not search_pattern.search(field.name):
                    continue
            elif case_sensitive:
                if pattern not in field.name:
                    continue
            else:
                if pattern.lower() not in field.name.lower():
                    continue
            matching_fields.append(field)

        if not matching_fields:
            return f"No fields found matching pattern '{pattern}'"

        # Apply pagination
        total_count = len(matching_fields)
        paginated_fields = matching_fields[offset : offset + max_results]
        has_more = (offset + max_results) < total_count

        # Format results
        content = f"# Field Search Results for '{pattern}'\n\n"
        content += f"Showing {len(paginated_fields)} of {total_count} matching fields"
        if offset > 0:
            content += f" (offset: {offset})"
        if has_more:
            content += " - more results available"
        content += ":\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for field in paginated_fields:
            key = (
                f"{field.namespace}.{field.declaring_type}"
                if field.namespace
                else field.declaring_type
            )
            if key not in by_type:
                by_type[key] = []
            by_type[key].append(field)

        for type_name, type_fields in sorted(by_type.items()):
            content += f"## {type_name}\n\n"
            for field in sorted(type_fields, key=lambda f: f.name):
                modifiers = []
                if field.is_public:
                    modifiers.append("public")
                if field.is_static:
                    modifiers.append("static")
                if field.is_literal:
                    modifiers.append("const")
                mod_str = " ".join(modifiers) + " " if modifiers else ""
                content += f"- `{mod_str}{field.name}`\n"
            content += "\n"

        if has_more:
            next_offset = offset + max_results
            content += f"\n**More results available.** Use `offset={next_offset}` to see the next page.\n"

        return content

    except FileNotFoundError as e:
        return _format_error(e)
    except Exception as e:
        logger.error(f"Error searching fields: {e}")
        return _format_error(e)


@mcp.tool()
async def search_properties(
    assembly_path: str,
    pattern: str,
    type_filter: str | None = None,
    namespace_filter: str | None = None,
    case_sensitive: bool = False,
    use_regex: bool = False,
    max_results: int = 1000,
    offset: int = 0,
    ctx: Context | None = None,
) -> str:
    """Search for properties in an assembly by name pattern.

    Uses direct metadata parsing to find properties.
    Useful for finding:
    - Configuration properties
    - Data model fields
    - API response/request properties

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        pattern: Search pattern to match against property names
        type_filter: Only search properties in types containing this string
        namespace_filter: Only search in namespaces containing this string
        case_sensitive: Whether pattern matching is case-sensitive (default: False)
        use_regex: Treat pattern as regular expression (default: False)
        max_results: Maximum number of results to return (default: 1000)
        offset: Number of results to skip for pagination (default: 0)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Searching for properties matching '{pattern}' in: {validated_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(validated_path) as reader:
            properties = reader.list_properties(
                type_filter=type_filter,
                namespace_filter=namespace_filter,
            )

        if not properties:
            return "No properties found in assembly"

        # Compile regex if needed
        search_pattern, regex_error = _compile_search_pattern(pattern, case_sensitive, use_regex)
        if regex_error:
            return regex_error

        # Filter by pattern
        matching_props = []
        for prop in properties:
            if use_regex and search_pattern:
                if not search_pattern.search(prop.name):
                    continue
            elif case_sensitive:
                if pattern not in prop.name:
                    continue
            else:
                if pattern.lower() not in prop.name.lower():
                    continue
            matching_props.append(prop)

        if not matching_props:
            return f"No properties found matching pattern '{pattern}'"

        # Apply pagination
        total_count = len(matching_props)
        paginated_props = matching_props[offset : offset + max_results]
        has_more = (offset + max_results) < total_count

        # Format results
        content = f"# Property Search Results for '{pattern}'\n\n"
        content += f"Showing {len(paginated_props)} of {total_count} matching properties"
        if offset > 0:
            content += f" (offset: {offset})"
        if has_more:
            content += " - more results available"
        content += ":\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for prop in paginated_props:
            key = (
                f"{prop.namespace}.{prop.declaring_type}" if prop.namespace else prop.declaring_type
            )
            if key not in by_type:
                by_type[key] = []
            by_type[key].append(prop)

        for type_name, type_props in sorted(by_type.items()):
            content += f"## {type_name}\n\n"
            for prop in sorted(type_props, key=lambda p: p.name):
                content += f"- `{prop.name}`\n"
            content += "\n"

        if has_more:
            next_offset = offset + max_results
            content += f"\n**More results available.** Use `offset={next_offset}` to see the next page.\n"

        return content

    except FileNotFoundError as e:
        return _format_error(e)
    except Exception as e:
        logger.error(f"Error searching properties: {e}")
        return _format_error(e)


@mcp.tool()
async def list_events(
    assembly_path: str,
    type_filter: str | None = None,
    namespace_filter: str | None = None,
    max_results: int = 1000,
    offset: int = 0,
    ctx: Context | None = None,
) -> str:
    """List all events defined in an assembly.

    Uses direct metadata parsing to enumerate events.
    Useful for understanding:
    - Event-driven architecture
    - Observer patterns
    - UI event handlers

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        type_filter: Only list events in types containing this string
        namespace_filter: Only list events in namespaces containing this string
        max_results: Maximum number of results to return (default: 1000)
        offset: Number of results to skip for pagination (default: 0)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Listing events in: {validated_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(validated_path) as reader:
            events = reader.list_events(
                type_filter=type_filter,
                namespace_filter=namespace_filter,
            )

        if not events:
            return "No events found in assembly"

        # Apply pagination
        total_count = len(events)
        paginated_events = events[offset : offset + max_results]
        has_more = (offset + max_results) < total_count

        content = "# Events in Assembly\n\n"
        content += f"Showing {len(paginated_events)} of {total_count} events"
        if offset > 0:
            content += f" (offset: {offset})"
        if has_more:
            content += " - more results available"
        content += ":\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for evt in paginated_events:
            key = f"{evt.namespace}.{evt.declaring_type}" if evt.namespace else evt.declaring_type
            if key not in by_type:
                by_type[key] = []
            by_type[key].append(evt)

        for type_name, type_events in sorted(by_type.items()):
            content += f"## {type_name}\n\n"
            for evt in sorted(type_events, key=lambda e: e.name):
                content += f"- `event {evt.name}`\n"
            content += "\n"

        if has_more:
            next_offset = offset + max_results
            content += f"\n**More results available.** Use `offset={next_offset}` to see the next page.\n"

        return content

    except FileNotFoundError as e:
        return _format_error(e)
    except Exception as e:
        logger.error(f"Error listing events: {e}")
        return _format_error(e)


@mcp.tool()
async def list_resources(
    assembly_path: str,
    max_results: int = 1000,
    offset: int = 0,
    ctx: Context | None = None,
) -> str:
    """List all embedded resources in an assembly.

    Uses direct metadata parsing to enumerate ManifestResource entries.
    Useful for finding:
    - Embedded files (images, configs, data)
    - Localization resources
    - Embedded assemblies

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        max_results: Maximum number of results to return (default: 1000)
        offset: Number of results to skip for pagination (default: 0)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Listing resources in: {validated_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(validated_path) as reader:
            resources = reader.list_resources()

        if not resources:
            return "No embedded resources found in assembly"

        # Apply pagination
        total_count = len(resources)
        sorted_resources = sorted(resources, key=lambda r: r.name)
        paginated_resources = sorted_resources[offset : offset + max_results]
        has_more = (offset + max_results) < total_count

        content = "# Embedded Resources\n\n"
        content += f"Showing {len(paginated_resources)} of {total_count} resources"
        if offset > 0:
            content += f" (offset: {offset})"
        if has_more:
            content += " - more results available"
        content += ":\n\n"

        for res in paginated_resources:
            visibility = "public" if res.is_public else "private"
            content += f"- `{res.name}` ({visibility})\n"

        if has_more:
            next_offset = offset + max_results
            content += f"\n**More results available.** Use `offset={next_offset}` to see the next page.\n"

        return content

    except FileNotFoundError as e:
        return _format_error(e)
    except Exception as e:
        logger.error(f"Error listing resources: {e}")
        return _format_error(e)


@mcp.tool()
async def get_metadata_summary(
    assembly_path: str,
    ctx: Context | None = None,
) -> str:
    """Get a comprehensive metadata summary of an assembly.

    Uses direct metadata parsing (dnfile) for accurate counts and details.
    This is more accurate than get_assembly_info for metadata statistics.

    Returns:
    - Assembly name, version, culture
    - Type/method/field/property/event counts
    - Referenced assemblies
    - Target framework (if available)

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
    """
    # Validate assembly path before any processing
    try:
        validated_path = _validate_assembly_path(assembly_path)
    except AssemblyPathError as e:
        return _format_error(e, "path validation")

    if ctx:
        await ctx.info(f"Getting metadata summary: {validated_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(validated_path) as reader:
            meta = reader.get_assembly_metadata()

        content = "# Assembly Metadata Summary\n\n"
        content += "## Identity\n\n"
        content += f"- **Name**: {meta.name}\n"
        content += f"- **Version**: {meta.version}\n"
        if meta.culture:
            content += f"- **Culture**: {meta.culture}\n"
        if meta.public_key_token:
            content += f"- **Public Key Token**: {meta.public_key_token}\n"
        if meta.target_framework:
            content += f"- **Target Framework**: {meta.target_framework}\n"

        content += "\n## Statistics\n\n"
        content += "| Entity | Count |\n"
        content += "|--------|-------|\n"
        content += f"| Types | {meta.type_count} |\n"
        content += f"| Methods | {meta.method_count} |\n"
        content += f"| Fields | {meta.field_count} |\n"
        content += f"| Properties | {meta.property_count} |\n"
        content += f"| Events | {meta.event_count} |\n"
        content += f"| Resources | {meta.resource_count} |\n"

        if meta.referenced_assemblies:
            content += f"\n## Referenced Assemblies ({len(meta.referenced_assemblies)})\n\n"
            for ref in sorted(meta.referenced_assemblies):
                content += f"- `{ref}`\n"

        return content

    except FileNotFoundError as e:
        return _format_error(e)
    except Exception as e:
        logger.error(f"Error getting metadata summary: {e}")
        return _format_error(e)


# FastMCP automatically handles prompts
@mcp.prompt()
def analyze_assembly_prompt(assembly_path: str, focus_area: str = "types") -> str:
    """Prompt template for analyzing .NET assemblies"""
    return f"""I need to analyze the .NET assembly at "{assembly_path}".

Please help me understand:
1. The overall structure and organization of the assembly
2. Key types and their relationships
3. Main namespaces and their purposes
4. Any notable patterns or architectural decisions

Focus area: {focus_area}

Start by listing the types in the assembly, then provide insights based on what you find."""


@mcp.prompt()
def decompile_and_explain_prompt(assembly_path: str, type_name: str) -> str:
    """Prompt template for decompiling and explaining specific types"""
    return f"""I want to understand the type "{type_name}" from the assembly "{assembly_path}".

Please:
1. Decompile this specific type
2. Explain what this type does and its purpose
3. Highlight any interesting patterns, design decisions, or potential issues
4. Suggest how this type fits into the overall architecture

Type to analyze: {type_name}
Assembly: {assembly_path}"""


def main():
    """Entry point for the MCP server."""
    import sys

    from . import __version__

    # Print banner to stderr (stdout is reserved for MCP protocol)
    print(
        f"🔬 mcilspy v{__version__} - .NET Assembly Decompiler MCP Server",
        file=sys.stderr,
    )
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
