"""
SMF Inspector CLI - Interactive tool for testing SMF servers.

Inspired by MCP Inspector: https://modelcontextprotocol.io/docs/tools/inspector
"""

import argparse
import asyncio
import json
import sys
from pathlib import Path
from typing import Any, Dict, Optional

from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport


class SMFInspector:
    """Interactive inspector for SMF servers."""

    def __init__(self, server_path: str):
        """
        Initialize inspector.

        Args:
            server_path: Path to server file (e.g., "server.py:mcp")
        """
        self.server_path = server_path
        self.client: Optional[Client[FastMCPTransport]] = None
        self.server: Optional[Any] = None

    def _load_server(self) -> None:
        """Load server module."""
        if self.server is not None:
            return  # Already loaded

        # Import server module
        if ":" in self.server_path:
            file_path, server_var = self.server_path.split(":", 1)
        else:
            file_path = self.server_path
            server_var = "mcp"

        file_path = Path(file_path)
        if not file_path.exists():
            raise FileNotFoundError(f"Server file not found: {file_path}")

        import importlib.util

        spec = importlib.util.spec_from_file_location("server", file_path)
        if spec is None or spec.loader is None:
            raise ImportError(f"Cannot import server from {file_path}")

        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)

        if not hasattr(module, server_var):
            raise AttributeError(f"Server module must define '{server_var}' variable")

        self.server = getattr(module, server_var)

    async def connect(self) -> None:
        """Connect to the server."""
        self._load_server()
        
        # Create client - FastMCP transport uses the server directly
        self.client = Client(transport=self.server)
        await self.client.__aenter__()

    async def disconnect(self) -> None:
        """Disconnect from the server."""
        if self.client:
            try:
                await self.client.__aexit__(None, None, None)
            except Exception:
                pass  # Ignore errors on disconnect
            finally:
                self.client = None

    async def list_tools(self) -> list:
        """List all available tools."""
        if not self.client:
            raise RuntimeError("Client not connected. Call connect() first.")
        return await self.client.list_tools()

    async def list_resources(self) -> list:
        """List all available resources."""
        if not self.client:
            raise RuntimeError("Client not connected. Call connect() first.")
        return await self.client.list_resources()

    async def list_prompts(self) -> list:
        """List all available prompts."""
        if not self.client:
            raise RuntimeError("Client not connected. Call connect() first.")
        return await self.client.list_prompts()

    async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
        """Call a tool."""
        if not self.client:
            raise RuntimeError("Client not connected. Call connect() first.")
        result = await self.client.call_tool(name=name, arguments=arguments)
        # Extract data from result object (CallToolResult)
        # FastMCP Client returns CallToolResult with .data attribute
        response: Dict[str, Any] = {}
        if hasattr(result, "data"):
            response["data"] = result.data
        if hasattr(result, "isError"):
            response["isError"] = result.isError
        if hasattr(result, "content"):
            response["content"] = result.content
        if hasattr(result, "text"):
            response["text"] = result.text
        # If no standard attributes, try to serialize the object
        if not response:
            try:
                if hasattr(result, "__dict__"):
                    response = result.__dict__
                else:
                    response = {"result": str(result)}
            except Exception:
                response = {"result": str(result)}
        return response

    async def read_resource(self, uri: str) -> Dict[str, Any]:
        """Read a resource."""
        if not self.client:
            raise RuntimeError("Client not connected. Call connect() first.")
        result = await self.client.read_resource(uri=uri)
        # Extract data from result object (ReadResourceResult)
        response: Dict[str, Any] = {}
        if hasattr(result, "contents"):
            # contents is typically a list
            contents = result.contents
            if isinstance(contents, list):
                response["contents"] = [
                    {
                        "uri": getattr(c, "uri", None),
                        "mimeType": getattr(c, "mimeType", None),
                        "text": getattr(c, "text", None),
                        "blob": getattr(c, "blob", None),
                    }
                    for c in contents
                ]
            else:
                response["contents"] = contents
        if hasattr(result, "data"):
            response["data"] = result.data
        if not response:
            try:
                if hasattr(result, "__dict__"):
                    response = result.__dict__
                else:
                    response = {"result": str(result)}
            except Exception:
                response = {"result": str(result)}
        return response

    async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Get a prompt."""
        if not self.client:
            raise RuntimeError("Client not connected. Call connect() first.")
        result = await self.client.get_prompt(name=name, arguments=arguments or {})
        # Extract data from result object (GetPromptResult)
        response: Dict[str, Any] = {}
        if hasattr(result, "messages"):
            # messages is typically a list of message objects
            messages = result.messages
            if isinstance(messages, list):
                response["messages"] = [
                    {
                        "role": getattr(m, "role", None),
                        "content": getattr(m, "content", None),
                        "text": getattr(m, "text", None),
                    }
                    for m in messages
                ]
            else:
                response["messages"] = messages
        if hasattr(result, "content"):
            response["content"] = result.content
        if hasattr(result, "text"):
            response["text"] = result.text
        if not response:
            try:
                if hasattr(result, "__dict__"):
                    response = result.__dict__
                else:
                    response = {"result": str(result)}
            except Exception:
                response = {"result": str(result)}
        return response


def print_section(title: str) -> None:
    """Print a section header."""
    print(f"\n{'=' * 60}")
    print(f" {title}")
    print(f"{'=' * 60}\n")


def print_tools(tools: list) -> None:
    """Print tools list."""
    print_section("TOOLS")
    if not tools:
        print("No tools available.")
        return

    for tool in tools:
        print(f"  • {tool.name}")
        if tool.description:
            print(f"    {tool.description}")
        if hasattr(tool, "inputSchema") and tool.inputSchema:
            print(f"    Schema: {json.dumps(tool.inputSchema, indent=4)}")
        print()


def print_resources(resources: list) -> None:
    """Print resources list."""
    print_section("RESOURCES")
    if not resources:
        print("No resources available.")
        return

    for resource in resources:
        print(f"  • {resource.uri}")
        if resource.name:
            print(f"    Name: {resource.name}")
        if resource.description:
            print(f"    Description: {resource.description}")
        if resource.mimeType:
            print(f"    MIME Type: {resource.mimeType}")
        print()


def print_prompts(prompts: list) -> None:
    """Print prompts list."""
    print_section("PROMPTS")
    if not prompts:
        print("No prompts available.")
        return

    for prompt in prompts:
        print(f"  • {prompt.name}")
        if prompt.description:
            print(f"    Description: {prompt.description}")
        if hasattr(prompt, "arguments") and prompt.arguments:
            print(f"    Arguments: {json.dumps(prompt.arguments, indent=4)}")
        print()


async def interactive_mode(inspector: SMFInspector) -> None:
    """Run interactive inspection mode."""
    await inspector.connect()

    try:
        while True:
            print("\n" + "=" * 60)
            print(" SMF Inspector - Interactive Mode")
            print("=" * 60)
            print("\nCommands:")
            print("  1. list-tools      - List all tools")
            print("  2. list-resources  - List all resources")
            print("  3. list-prompts    - List all prompts")
            print("  4. call-tool       - Call a tool")
            print("  5. read-resource   - Read a resource")
            print("  6. get-prompt      - Get a prompt")
            print("  7. info            - Show server info")
            print("  8. quit            - Exit inspector")
            print()

            command = input("Enter command: ").strip().lower()

            if command == "quit" or command == "q":
                break
            elif command == "list-tools" or command == "1":
                tools = await inspector.list_tools()
                print_tools(tools)
            elif command == "list-resources" or command == "2":
                resources = await inspector.list_resources()
                print_resources(resources)
            elif command == "list-prompts" or command == "3":
                prompts = await inspector.list_prompts()
                print_prompts(prompts)
            elif command == "call-tool" or command == "4":
                tool_name = input("Tool name: ").strip()
                
                # Get tool info to show schema
                try:
                    tools = await inspector.list_tools()
                    tool_info = next((t for t in tools if t.name == tool_name), None)
                    if tool_info:
                        print(f"\nTool: {tool_name}")
                        if tool_info.description:
                            print(f"Description: {tool_info.description}")
                        if hasattr(tool_info, "inputSchema") and tool_info.inputSchema:
                            print(f"Schema: {json.dumps(tool_info.inputSchema, indent=2)}")
                            # Try to extract parameter names
                            schema = tool_info.inputSchema
                            if isinstance(schema, dict) and "properties" in schema:
                                params = list(schema["properties"].keys())
                                if len(params) == 1:
                                    print(f"\nTip: This tool takes one parameter: '{params[0]}'")
                                    print(f"     You can enter just the value, or JSON: {{\"{params[0]}\": \"value\"}}")
                        print()
                except Exception:
                    pass  # Continue even if we can't get tool info
                
                args_str = input("Arguments (JSON, or single value if tool takes one parameter): ").strip()
                try:
                    if args_str:
                        # Try to parse as JSON first
                        try:
                            args = json.loads(args_str)
                        except json.JSONDecodeError:
                            # If JSON parsing fails, check if it's a single value
                            # and if the tool takes one parameter
                            if tool_info and hasattr(tool_info, "inputSchema"):
                                schema = tool_info.inputSchema
                                if isinstance(schema, dict) and "properties" in schema:
                                    params = list(schema["properties"].keys())
                                    if len(params) == 1:
                                        # Single parameter - use the value directly
                                        param_name = params[0]
                                        args = {param_name: args_str}
                                        print(f"Using '{param_name}' = {args_str!r}")
                                    else:
                                        raise ValueError(
                                            f"Tool requires {len(params)} parameters. "
                                            f"Please provide JSON: {json.dumps({p: 'value' for p in params}, indent=2)}"
                                        )
                                else:
                                    raise ValueError("Please provide valid JSON arguments")
                            else:
                                raise ValueError(
                                    "Invalid JSON. Please provide JSON format: "
                                    '{"param": "value"} or use --list to see tool schemas'
                                )
                    else:
                        args = {}
                    
                    result = await inspector.call_tool(tool_name, args)
                    print(f"\nResult:")
                    print(json.dumps(result, indent=2, default=str))
                except json.JSONDecodeError as e:
                    print(f"\nError parsing JSON: {e}")
                    print("Example: {\"name\": \"Alice\", \"age\": 30}")
                except ValueError as e:
                    print(f"\nError: {e}")
                except Exception as e:
                    print(f"\nError calling tool: {e}")
                    import traceback
                    traceback.print_exc()
            elif command == "read-resource" or command == "5":
                uri = input("Resource URI: ").strip()
                try:
                    result = await inspector.read_resource(uri)
                    print(f"\nResult:")
                    print(json.dumps(result, indent=2, default=str))
                except Exception as e:
                    print(f"Error: {e}")
                    import traceback
                    traceback.print_exc()
            elif command == "get-prompt" or command == "6":
                prompt_name = input("Prompt name: ").strip()
                args_str = input("Arguments (JSON, optional): ").strip()
                try:
                    args = json.loads(args_str) if args_str else {}
                    result = await inspector.get_prompt(prompt_name, args)
                    print(f"\nResult:")
                    print(json.dumps(result, indent=2, default=str))
                except Exception as e:
                    print(f"Error: {e}")
                    import traceback
                    traceback.print_exc()
            elif command == "info" or command == "7":
                tools = await inspector.list_tools()
                resources = await inspector.list_resources()
                prompts = await inspector.list_prompts()
                print_section("SERVER INFO")
                print(f"Tools: {len(tools)}")
                print(f"Resources: {len(resources)}")
                print(f"Prompts: {len(prompts)}")
            else:
                print(f"Unknown command: {command}")

    finally:
        await inspector.disconnect()


async def list_mode(inspector: SMFInspector) -> int:
    """List all components."""
    await inspector.connect()

    try:
        tools = await inspector.list_tools()
        resources = await inspector.list_resources()
        prompts = await inspector.list_prompts()

        print_tools(tools)
        print_resources(resources)
        print_prompts(prompts)

        print_section("SUMMARY")
        print(f"Total Tools: {len(tools)}")
        print(f"Total Resources: {len(resources)}")
        print(f"Total Prompts: {len(prompts)}")

    finally:
        await inspector.disconnect()
    
    return 0


def run_official_inspector(server_file: str, args: list) -> int:
    """
    Run the official MCP Inspector using npx.
    
    Args:
        server_file: Path to server file
        args: Additional arguments
        
    Returns:
        Exit code
    """
    import shutil
    import subprocess
    
    # Check for npx
    npx_path = shutil.which("npx")
    if not npx_path:
        print("Error: 'npx' is required to run the official inspector.", file=sys.stderr)
        print("Please install Node.js and npm, or use --text for the text-based inspector.", file=sys.stderr)
        return 1
        
    # Construct command
    # npx -y @modelcontextprotocol/inspector uv run <server_file>
    # Use the resolved npx path
    # Also resolve server_file to absolute path to avoid issues with relative paths (like .\server.py)
    # when passed through npx/inspector on Windows.
    # IMPORTANT: Use .as_posix() to ensure forward slashes are used.
    # Backslashes can be stripped or misinterpreted as escape characters when passed through
    # the inspector's argument parsing or shell execution, leading to "H:Desktopsmfadminserver.py".
    server_path = Path(server_file).resolve().as_posix()
    cmd = [npx_path, "-y", "@modelcontextprotocol/inspector", "uv", "run", server_path]
    
    # Add any additional args passed to the server
    # Note: The inspector might not pass these through easily depending on how it invokes the command
    # For now, we assume server_file is the script path
    
    print(f"Launching official MCP Inspector for {server_file}...")
    print(f"Command: {' '.join(cmd)}")
    
    try:
        # Run interactively
        # On Windows, shell=True is often needed for .cmd files even with full path
        use_shell = sys.platform == "win32"
        return subprocess.call(cmd, shell=use_shell)
    except KeyboardInterrupt:
        return 0
    except Exception as e:
        print(f"Error running inspector: {e}", file=sys.stderr)
        return 1


def main() -> int:
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        description="SMF Inspector - Interactive tool for testing SMF servers",
        epilog="""
Examples:
  # Launch Official Web Inspector (recommended)
  smf-inspector server.py
  
  # Launch Text-based Inspector
  smf-inspector server.py --text
  
  # List all components (Text mode)
  smf-inspector server.py --list
  
  # Call a tool (Text mode)
  smf-inspector server.py --tool greet --args '{"name": "Alice"}'
        """,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "server",
        help="Server file path (e.g., server.py)",
        nargs="?",
        default="server.py",
    )
    parser.add_argument(
        "--text", "--cli",
        action="store_true",
        help="Use text-based inspector instead of official web UI",
    )
    parser.add_argument(
        "--list",
        action="store_true",
        help="List all components and exit (implies --text)",
    )
    parser.add_argument(
        "--tool",
        help="Call a tool (implies --text)",
    )
    parser.add_argument(
        "--args",
        help="Tool arguments as JSON",
    )
    parser.add_argument(
        "--resource",
        help="Read a resource (implies --text)",
    )
    parser.add_argument(
        "--prompt",
        help="Get a prompt (implies --text)",
    )

    args = parser.parse_args()

    # Determine mode
    use_text_mode = args.text or args.list or args.tool or args.resource or args.prompt
    
    if not use_text_mode:
        return run_official_inspector(args.server, [])

    # Text mode
    # For text mode, we might need to handle "server.py:mcp" format vs "server.py"
    # The official inspector expects a file to run with `uv run`.
    # Our text inspector loads the module.
    
    server_path = args.server
    # If it doesn't have a colon, append default :mcp for text inspector
    if ":" not in server_path and not server_path.endswith(".py"):
         # It might be just a module name? 
         pass
    elif ":" not in server_path:
        server_path = f"{server_path}:mcp"

    inspector = SMFInspector(server_path)

    async def run_tool_command():
        """Run tool command with proper async context."""
        await inspector.connect()
        try:
            if args.args:
                # Try to parse JSON, with helpful error messages
                try:
                    tool_args = json.loads(args.args)
                except json.JSONDecodeError as e:
                    print(f"Error parsing JSON arguments: {e}", file=sys.stderr)
                    print(f"\nReceived: {args.args!r}", file=sys.stderr)
                    print("\nTips:", file=sys.stderr)
                    print("  PowerShell: smf-inspector server.py --tool greet --args '{\"name\": \"Alice\"}'", file=sys.stderr)
                    print("  Bash/CMD:   smf-inspector server.py --tool greet --args '{\"name\": \"Alice\"}'", file=sys.stderr)
                    return 1
            else:
                tool_args = {}
            result = await inspector.call_tool(args.tool, tool_args)
            print(json.dumps(result, indent=2, default=str))
        finally:
            await inspector.disconnect()
        return 0

    async def run_resource_command():
        """Run resource command with proper async context."""
        await inspector.connect()
        try:
            result = await inspector.read_resource(args.resource)
            print(json.dumps(result, indent=2, default=str))
        finally:
            await inspector.disconnect()
        return 0

    async def run_prompt_command():
        """Run prompt command with proper async context."""
        await inspector.connect()
        try:
            result = await inspector.get_prompt(args.prompt)
            print(json.dumps(result, indent=2, default=str))
        finally:
            await inspector.disconnect()
        return 0

    try:
        if args.list:
            exit_code = asyncio.run(list_mode(inspector))
            return exit_code
        elif args.tool:
            exit_code = asyncio.run(run_tool_command())
            return exit_code
        elif args.resource:
            exit_code = asyncio.run(run_resource_command())
            return exit_code
        elif args.prompt:
            exit_code = asyncio.run(run_prompt_command())
            return exit_code
        else:
            # Interactive mode
            asyncio.run(interactive_mode(inspector))
    except KeyboardInterrupt:
        print("\n\nInspector stopped.")
        return 0
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())

