import asyncio
import logging
import os
import platform
import re
import shutil
from contextlib import asynccontextmanager
from dataclasses import dataclass

from mcp.server.fastmcp import Context, FastMCP

from .ilspy_wrapper import ILSpyWrapper
from .models import EntityType, LanguageVersion

# 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__)


@dataclass
class AppState:
    """Application state shared across tools via lifespan context."""

    wrapper: ILSpyWrapper | None = None


@asynccontextmanager
async def app_lifespan(server: FastMCP):
    """Initialize application state on startup, cleanup on shutdown.

    The ILSpyWrapper is lazily initialized on first use to avoid
    failing startup if ilspycmd isn't installed (dnfile-based tools
    still work without it).
    """
    state = AppState()
    logger.info("mcilspy server starting up")
    try:
        yield {"app_state": state}
    finally:
        logger.info("mcilspy server shutting down")


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


def get_wrapper(ctx: Context | None = None) -> ILSpyWrapper:
    """Get ILSpy wrapper instance from context or create new one.

    Args:
        ctx: Optional FastMCP context. If provided, uses lifespan state
             for caching. If None, creates a new instance (for backwards
             compatibility with non-tool callers).

    Returns:
        ILSpyWrapper instance

    Raises:
        RuntimeError: If ilspycmd is not installed
    """
    if ctx is not None:
        # Use lifespan state for caching (access via request_context)
        try:
            lifespan_ctx = ctx.request_context.lifespan_context
            if lifespan_ctx is not None:
                state: AppState = lifespan_ctx.get("app_state")
                if state is not None:
                    if state.wrapper is None:
                        state.wrapper = ILSpyWrapper()
                    return state.wrapper
        except (AttributeError, ValueError):
            # Context not fully initialized, fall through to create new instance
            pass

    # Fallback: create new instance (no caching)
    return ILSpyWrapper()


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}"


def _find_ilspycmd_path() -> str | None:
    """Find ilspycmd in PATH or common install locations."""
    # Check PATH first
    path = shutil.which("ilspycmd")
    if path:
        return path

    # Check common dotnet tools locations (often not in PATH for MCP servers)
    home = os.path.expanduser("~")
    candidates = [
        os.path.join(home, ".dotnet", "tools", "ilspycmd"),
        os.path.join(home, ".dotnet", "tools", "ilspycmd.exe"),
    ]

    # Windows-specific
    if os.name == "nt":
        userprofile = os.environ.get("USERPROFILE", "")
        if userprofile:
            candidates.append(os.path.join(userprofile, ".dotnet", "tools", "ilspycmd.exe"))

    for candidate in candidates:
        if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
            return candidate

    return None


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 Exception:
            pass

    # 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 Exception:
            pass

    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:
            pass

        # 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[-1000:]}\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`\n"
                    "- `list_types`\n"
                    "- `search_types`\n"
                    "- `search_strings`\n"
                    "- `generate_diagrammer`\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,
    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

    WORKFLOW TIP: Start with `list_types` to discover available types, then use
    this tool with `type_name` to decompile specific classes of interest.

    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
    """
    if ctx:
        await ctx.info(f"Starting decompilation of assembly: {assembly_path}")

    try:
        wrapper = get_wrapper(ctx)

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

        request = DecompileRequest(
            assembly_path=assembly_path,
            output_dir=output_dir,
            type_name=type_name,
            language_version=LanguageVersion(language_version),
            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,
        )

        response = await wrapper.decompile(request)

        if response.success:
            if response.source_code:
                content = f"# Decompilation result: {response.assembly_name}"
                if response.type_name:
                    content += f" - {response.type_name}"
                content += f"\n\n```csharp\n{response.source_code}\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 list_types(
    assembly_path: str,
    entity_types: list[str] | None = None,
    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"]
    """
    if ctx:
        await ctx.info(f"Listing types in assembly: {assembly_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=assembly_path, entity_types=entity_type_enums)

        response = await wrapper.list_types(request)

        if response.success and response.types:
            content = f"# Types in {assembly_path}\n\n"
            content += f"Found {response.total_count} types:\n\n"

            # Group by namespace
            by_namespace = {}
            for type_info in response.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"

            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)
    """
    if ctx:
        await ctx.info(f"Generating assembly diagram: {assembly_path}")

    try:
        wrapper = get_wrapper(ctx)

        from .models import GenerateDiagrammerRequest

        request = GenerateDiagrammerRequest(
            assembly_path=assembly_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 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)
    """
    if ctx:
        await ctx.info(f"Getting assembly info: {assembly_path}")

    try:
        wrapper = get_wrapper(ctx)

        from .models import AssemblyInfoRequest

        request = AssemblyInfoRequest(assembly_path=assembly_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,
    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)
    """
    if ctx:
        await ctx.info(f"Searching for types matching '{pattern}' in: {assembly_path}")

    try:
        wrapper = get_wrapper(ctx)

        # Default to search all entity types
        if entity_types is None:
            entity_types = ["class", "interface", "struct", "delegate", "enum"]

        # 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=assembly_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
        if use_regex:
            try:
                flags = 0 if case_sensitive else re.IGNORECASE
                search_pattern = re.compile(pattern, flags)
            except re.error as e:
                return f"Invalid regex pattern: {e}"
        else:
            search_pattern = None

        # 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}'"

        # Format results
        content = f"# Search Results for '{pattern}'\n\n"
        content += f"Found {len(matching_types)} matching types:\n\n"

        # Group by namespace
        by_namespace: dict[str, list] = {}
        for type_info in matching_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"

        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 = 100,
    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

    Returns the types and methods containing matching strings.

    Args:
        assembly_path: Full path to the .NET assembly file (.dll or .exe)
        pattern: String pattern to search for in the decompiled code
        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)
    """
    if ctx:
        await ctx.info(f"Searching for strings matching '{pattern}' in: {assembly_path}")

    try:
        wrapper = get_wrapper(ctx)

        # Decompile to IL to find string literals (ldstr instructions)
        from .models import DecompileRequest

        request = DecompileRequest(
            assembly_path=assembly_path,
            show_il_code=True,  # IL makes string literals explicit
            language_version=LanguageVersion.LATEST,
        )

        response = await wrapper.decompile(request)

        if not response.success:
            return f"Failed to decompile assembly: {response.error_message}"

        source_code = response.source_code or ""

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

        # Search for string literals containing the pattern
        # In IL, strings appear as: ldstr "string value"
        # In C#, they're just regular string literals
        matches = []
        current_type = None
        current_method = None

        lines = source_code.split("\n")
        for i, line in enumerate(lines):
            # Track current type/method context
            type_match = re.match(
                r"^\s*(?:public|private|internal|protected)?\s*(?:class|struct|interface)\s+(\w+)",
                line,
            )
            if type_match:
                current_type = type_match.group(1)

            method_match = re.match(
                r"^\s*(?:public|private|internal|protected)?\s*(?:static\s+)?(?:\w+\s+)+(\w+)\s*\(",
                line,
            )
            if method_match:
                current_method = method_match.group(1)

            # Search for pattern in the line
            found = False
            if use_regex and search_pattern:
                found = bool(search_pattern.search(line))
            elif case_sensitive:
                found = pattern in line
            else:
                found = pattern.lower() in line.lower()

            if found and len(matches) < max_results:
                matches.append(
                    {
                        "line_num": i + 1,
                        "line": line.strip()[:200],  # Truncate long lines
                        "type": current_type or "Unknown",
                        "method": current_method,
                    }
                )

        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})"
        content += ":\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for match in matches:
            type_name = match["type"]
            if type_name not in by_type:
                by_type[type_name] = []
            by_type[type_name].append(match)

        for type_name, type_matches in sorted(by_type.items()):
            content += f"## {type_name}\n\n"
            for match in type_matches[:20]:  # Limit per type
                method_info = f" in `{match['method']}()`" if match["method"] else ""
                content += f"- Line {match['line_num']}{method_info}:\n"
                content += f"  ```\n  {match['line']}\n  ```\n"
            if len(type_matches) > 20:
                content += f"  ... and {len(type_matches) - 20} more matches in this type\n"
            content += "\n"

        content += "\n**TIP**: Use `decompile_assembly` with `type_name` to see the full context of interesting matches."
        return content

    except Exception as e:
        logger.error(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,
    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)
    """
    if ctx:
        await ctx.info(f"Searching for methods matching '{pattern}' in: {assembly_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(assembly_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
        if use_regex:
            try:
                flags = 0 if case_sensitive else re.IGNORECASE
                search_pattern = re.compile(pattern, flags)
            except re.error as e:
                return f"Invalid regex pattern: {e}"
        else:
            search_pattern = None

        # 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}'"

        # Format results
        content = f"# Method Search Results for '{pattern}'\n\n"
        content += f"Found {len(matching_methods)} matching methods:\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for method in matching_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"

        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,
    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)
    """
    if ctx:
        await ctx.info(f"Searching for fields matching '{pattern}' in: {assembly_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(assembly_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
        if use_regex:
            try:
                flags = 0 if case_sensitive else re.IGNORECASE
                search_pattern = re.compile(pattern, flags)
            except re.error as e:
                return f"Invalid regex pattern: {e}"
        else:
            search_pattern = None

        # 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}'"

        # Format results
        content = f"# Field Search Results for '{pattern}'\n\n"
        content += f"Found {len(matching_fields)} matching fields:\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for field in matching_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"

        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,
    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)
    """
    if ctx:
        await ctx.info(f"Searching for properties matching '{pattern}' in: {assembly_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(assembly_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
        if use_regex:
            try:
                flags = 0 if case_sensitive else re.IGNORECASE
                search_pattern = re.compile(pattern, flags)
            except re.error as e:
                return f"Invalid regex pattern: {e}"
        else:
            search_pattern = None

        # 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}'"

        # Format results
        content = f"# Property Search Results for '{pattern}'\n\n"
        content += f"Found {len(matching_props)} matching properties:\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for prop in matching_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"

        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,
    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
    """
    if ctx:
        await ctx.info(f"Listing events in: {assembly_path}")

    try:
        from .metadata_reader import MetadataReader

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

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

        content = "# Events in Assembly\n\n"
        content += f"Found {len(events)} events:\n\n"

        # Group by type
        by_type: dict[str, list] = {}
        for evt in 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"

        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,
    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)
    """
    if ctx:
        await ctx.info(f"Listing resources in: {assembly_path}")

    try:
        from .metadata_reader import MetadataReader

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

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

        content = "# Embedded Resources\n\n"
        content += f"Found {len(resources)} resources:\n\n"

        for res in sorted(resources, key=lambda r: r.name):
            visibility = "public" if res.is_public else "private"
            content += f"- `{res.name}` ({visibility})\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)
    """
    if ctx:
        await ctx.info(f"Getting metadata summary: {assembly_path}")

    try:
        from .metadata_reader import MetadataReader

        with MetadataReader(assembly_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()
