import json
import sys

import anyio
import click
import mcp.types as types
from dap_types import LaunchRequestArguments, SourceBreakpoint
from mcp.server.lowlevel import Server
from pathlib import Path
from pydantic import TypeAdapter
from typing import Optional, TextIO

from dap_mcp.config import DebuggerSpecificConfig
from dap_mcp.debugger import Debugger, FunctionCallError
from dap_mcp.factory import DAPClientSingletonFactory
from dap_mcp.render import render_xml, try_render

import logging

logger = logging.getLogger(__name__)


@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
    "--transport",
    type=click.Choice(["stdio", "sse"]),
    default="stdio",
    help="Transport type",
)
@click.option(
    "--verbose", "-v", is_flag=True, help="Enable verbose logging", default=False
)
@click.option(
    "--config",
    "-c",
    "config_file",
    help="Path to the configuration file",
    required=True,
    type=click.File("r"),
)
def main(
    port: int,
    transport: str,
    verbose: bool,
    config_file: TextIO,
) -> int:
    if verbose:
        logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
    debug_config_type_adapter = TypeAdapter(DebuggerSpecificConfig)
    try:
        json_config = json.load(config_file)
    except json.JSONDecodeError as e:
        logger.error(f"Error parsing JSON configuration: {e}")
        return 1
    config = debug_config_type_adapter.validate_python(json_config)
    app: Server[object] = Server("dap_mcp")
    dap_factory = DAPClientSingletonFactory(config.debuggerPath, config.debuggerArgs)
    launch_arguments = LaunchRequestArguments(
        noDebug=False,
        **{
            k: v
            for k, v in config.model_dump(exclude_none=True).items()
            if k not in ["type", "debuggerPath", "debuggerArgs", "sourceDirs"]
        },
    )
    debugger = Debugger(dap_factory, launch_arguments)

    def ensure_file_path(str_path: str) -> Path | None:
        path = Path(str_path)
        if not path.is_file():
            if path.is_absolute():
                return None
            for potential_dir in config.sourceDirs:
                potential_path = Path(potential_dir) / path
                if potential_path.is_file():
                    return potential_path
            return None
        return path

    async def launch():
        return (await debugger.launch()).render()

    async def set_breakpoint(path: str, line: int, condition: Optional[str] = None):
        file_path = ensure_file_path(path)
        if file_path is None:
            return FunctionCallError(message=f"File ({path}) not found").render()
        response = await debugger.set_breakpoint(file_path, line, condition)
        return try_render(response)

    async def remove_breakpoint(path: str, line: int):
        file_path = ensure_file_path(path)
        if file_path is None:
            return FunctionCallError(message=f"File ({path}) not found").render()
        response = await debugger.remove_breakpoint(file_path, line)
        return try_render(response)

    async def view_file_at_line(line: int, path: Optional[str] = None):
        if path:
            file_path = ensure_file_path(path)
            if file_path is None:
                return FunctionCallError(message=f"File ({path}) not found").render()
        else:
            file_path = None
        response = await debugger.view_file_at_line(file_path, line)
        return try_render(response)

    async def list_all_breakpoints():
        response = await debugger.list_all_breakpoints()
        if isinstance(response, FunctionCallError):
            return response.render()

        def render_file(file: str, breakpoints: list[SourceBreakpoint]) -> str:
            return render_xml(
                "file",
                [
                    render_xml("breakpoint", None, **sb.model_dump())
                    for sb in breakpoints
                ],
                path=file,
            )

        return render_xml(
            "breakpoints",
            [
                render_file(str(file), breakpoints)
                for file, breakpoints in response.items()
            ],
        )

    async def continue_execution():
        response = await debugger.continue_execution()
        return try_render(response)

    async def step_in():
        response = await debugger.step_in()
        return try_render(response)

    async def step_out():
        response = await debugger.step_out()
        return try_render(response)

    async def next():
        response = await debugger.next()
        return try_render(response)

    async def evaluate(expression: str):
        response = await debugger.evaluate(expression)
        return try_render(response)

    async def change_frame(frameId: int):
        response = await debugger.change_frame(frameId)
        return response.render()

    async def terminate():
        response = await debugger.terminate()
        if isinstance(response, FunctionCallError):
            return response.render()
        return response

    @app.call_tool()
    async def call_tool(
        name: str, arguments: dict
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        if name == "get_launch_config":
            config_schema = json.dumps(type(config).model_json_schema())
            config_json = json.dumps(config.model_dump(exclude_none=True))
            return [
                types.TextContent(
                    type="text",
                    text=render_xml(
                        "config",
                        [
                            render_xml("schema", config_schema),
                            render_xml("data", config_json),
                        ],
                    ),
                )
            ]
        if name == "launch":
            return [types.TextContent(type="text", text=await launch())]
        if name == "set_breakpoint":
            return [
                types.TextContent(
                    type="text",
                    text=await set_breakpoint(
                        arguments["path"], arguments["line"], arguments.get("condition")
                    ),
                )
            ]
        if name == "remove_breakpoint":
            return [
                types.TextContent(
                    type="text",
                    text=await remove_breakpoint(arguments["path"], arguments["line"]),
                )
            ]
        if name == "view_file_at_line":
            return [
                types.TextContent(
                    type="text",
                    text=await view_file_at_line(
                        arguments["line"], arguments.get("path")
                    ),
                )
            ]
        if name == "list_all_breakpoints":
            return [types.TextContent(type="text", text=await list_all_breakpoints())]
        if name == "continue_execution":
            return [types.TextContent(type="text", text=await continue_execution())]
        if name == "step_in":
            return [types.TextContent(type="text", text=await step_in())]
        if name == "step_out":
            return [types.TextContent(type="text", text=await step_out())]
        if name == "next":
            return [types.TextContent(type="text", text=await next())]
        if name == "evaluate":
            return [
                types.TextContent(
                    type="text", text=await evaluate(arguments["expression"])
                )
            ]
        if name == "change_frame":
            return [
                types.TextContent(
                    type="text", text=await change_frame(arguments["frameId"])
                )
            ]
        if name == "terminate":
            return [types.TextContent(type="text", text=await terminate())]
        raise ValueError(f"Unknown tool: {name}")

    @app.list_tools()
    async def list_tools() -> list[types.Tool]:
        return [
            types.Tool(
                name="get_launch_config",
                description="Returns the user provided launch configuration along with its detailed schema for a DAP-compatible debugger. The schema includes descriptions for each field.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="launch",
                description="Launch the debuggee program. Set breakpoints before launching if necessary.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="set_breakpoint",
                description="Set a breakpoint at the specified file and line with an optional condition.",
                inputSchema={
                    "type": "object",
                    "required": ["path", "line"],
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "The file path where the breakpoint should be set.",
                        },
                        "line": {
                            "type": "integer",
                            "description": "The line number at which to set the breakpoint.",
                        },
                        "condition": {
                            "type": "string",
                            "description": "Optional condition to trigger the breakpoint.",
                        },
                    },
                },
            ),
            types.Tool(
                name="remove_breakpoint",
                description="Remove a breakpoint from the specified file and line.",
                inputSchema={
                    "type": "object",
                    "required": ["path", "line"],
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "The file path from which to remove the breakpoint.",
                        },
                        "line": {
                            "type": "integer",
                            "description": "The line number of the breakpoint to remove.",
                        },
                    },
                },
            ),
            types.Tool(
                name="list_all_breakpoints",
                description="List all breakpoints currently set in the debugger.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="continue_execution",
                description="Continue execution in the debugger after hitting a breakpoint.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="step_in",
                description="Step into the function call in the debugger.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="step_out",
                description="Step out of the current function in the debugger.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="next",
                description="Step over to the next line of code in the debugger.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="evaluate",
                description="Evaluate an expression in the current debugging context.",
                inputSchema={
                    "type": "object",
                    "required": ["expression"],
                    "properties": {
                        "expression": {
                            "type": "string",
                            "description": "The expression to evaluate.",
                        }
                    },
                },
            ),
            types.Tool(
                name="change_frame",
                description="Change the current debugging frame to the specified frame ID.",
                inputSchema={
                    "type": "object",
                    "required": ["frameId"],
                    "properties": {
                        "frameId": {
                            "type": "integer",
                            "description": "The ID of the frame to switch to.",
                        }
                    },
                },
            ),
            types.Tool(
                name="terminate",
                description="Terminate the current debugging session.",
                inputSchema={"type": "object", "properties": {}},
            ),
            types.Tool(
                name="view_file_at_line",
                description="Returns the lines of source code and the source code around the specified line. You should ALWAYS prefer this tool if you are reading code. Because it will show the line number, which is crucial for debugging. If 'path' is provided, it opens that file; otherwise, it uses the last specified file. You must provide a file to read the source before launch as no prefilled paths exist.",
                inputSchema={
                    "type": "object",
                    "required": ["line"],
                    "properties": {
                        "line": {
                            "type": "integer",
                            "description": "The line number around which the source code will be displayed.",
                        },
                        "path": {
                            "type": "string",
                            "description": "Optional file path. Provide this to open a specific file; otherwise, the last specified file is used.",
                        },
                    },
                },
            ),
        ]

    if transport == "sse":
        from mcp.server.sse import SseServerTransport
        from starlette.applications import Starlette
        from starlette.routing import Mount, Route

        sse = SseServerTransport("/messages/")
        debugger_inited = False

        async def handle_sse(request):
            nonlocal debugger_inited
            if not debugger_inited:
                await debugger.initialize()
                debugger_inited = True
            async with sse.connect_sse(
                request.scope, request.receive, request._send
            ) as streams:
                await app.run(
                    streams[0], streams[1], app.create_initialization_options()
                )

        starlette_app = Starlette(
            debug=True,
            routes=[
                Route("/sse", endpoint=handle_sse),
                Mount("/messages/", app=sse.handle_post_message),
            ],
        )

        import uvicorn

        uvicorn.run(starlette_app, host="0.0.0.0", port=port)
    else:
        from mcp.server.stdio import stdio_server

        async def arun():
            await debugger.initialize()
            async with stdio_server() as streams:
                await app.run(
                    streams[0], streams[1], app.create_initialization_options()
                )

        anyio.run(arun)

    return 0


if __name__ == "__main__":
    main()
