import anyio
import click
import httpx
import mcp.types as types
from bs4 import BeautifulSoup
from mcp.server.lowlevel import Server
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Mount, Route

# Use absolute import
from mcp_hitchcode.templates.template_loader import render_prompt_template


def serialize_content(content_list):
    return [{"type": content.type, "text": content.text} for content in content_list]


async def fetch_website(
    url: str,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
    headers = {
        "User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"
    }
    try:
        timeout = httpx.Timeout(10.0, connect=5.0)
        async with httpx.AsyncClient(
            follow_redirects=True, headers=headers, timeout=timeout
        ) as client:
            response = await client.get(url)
            response.raise_for_status()
            return [types.TextContent(type="text", text=response.text)]
    except httpx.TimeoutException:
        return [
            types.TextContent(
                type="text",
                text="Error: Request timed out while trying to fetch the website.",
            )
        ]
    except httpx.HTTPStatusError as e:
        return [
            types.TextContent(
                type="text",
                text=(
                    f"Error: HTTP {e.response.status_code} "
                    "error while fetching the website."
                ),
            )
        ]
    except Exception as e:
        return [
            types.TextContent(
                type="text", text=f"Error: Failed to fetch website: {str(e)}"
            )
        ]


async def check_mood(
    question: str,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
    """Check server's mood - always responds cheerfully with a heart."""
    msg: str = "I'm feeling great and happy to help you! ❤️"
    return [types.TextContent(type="text", text=msg)]


async def fetch_railway_docs(
    url: str = "https://docs.railway.app/guides/cli",
) -> list[types.TextContent]:
    """
    Fetch the most recent Railway CLI documentation.
    """
    headers = {
        "User-Agent": "MCP Railway Docs Fetcher (github.com/modelcontextprotocol/python-sdk)"
    }

    try:
        timeout = httpx.Timeout(10.0, connect=5.0)
        async with httpx.AsyncClient(
            follow_redirects=True, headers=headers, timeout=timeout
        ) as client:
            response = await client.get(url)
            response.raise_for_status()

            # Optionally, parse specific sections of the docs here
            return [types.TextContent(type="text", text=response.text)]
    except httpx.TimeoutException:
        return [
            types.TextContent(
                type="text",
                text="Error: Request timed out while trying to fetch the Railway CLI docs.",
            )
        ]
    except httpx.HTTPStatusError as e:
        return [
            types.TextContent(
                type="text",
                text=f"Error: HTTP {e.response.status_code} error while fetching the Railway CLI docs.",
            )
        ]
    except Exception as e:
        return [
            types.TextContent(
                type="text", text=f"Error: Failed to fetch Railway CLI docs: {str(e)}"
            )
        ]


async def fetch_railway_docs_optimized(
    url: str = "https://docs.railway.app/guides/cli",
) -> list[types.TextContent]:
    headers = {
        "User-Agent": "MCP Railway Docs Fetcher (github.com/modelcontextprotocol/python-sdk)"
    }

    try:
        timeout = httpx.Timeout(10.0, connect=5.0)
        async with httpx.AsyncClient(
            follow_redirects=True, headers=headers, timeout=timeout
        ) as client:
            response = await client.get(url)
            response.raise_for_status()

            # Parse HTML content
            soup = BeautifulSoup(response.text, "html.parser")
            content = []

            # Try to find any command sections
            for heading in soup.find_all(["h1", "h2", "h3", "h4"]):
                heading_text = heading.get_text(strip=True).lower()
                if any(
                    keyword in heading_text for keyword in ["command", "cli", "usage"]
                ):
                    # Look for the next element that might contain commands
                    next_elem = heading.find_next_sibling()
                    while next_elem and next_elem.name not in [
                        "ul",
                        "ol",
                        "h1",
                        "h2",
                        "h3",
                        "h4",
                    ]:
                        next_elem = next_elem.find_next_sibling()

                    if next_elem and next_elem.name in ["ul", "ol"]:
                        commands = []
                        for li in next_elem.find_all("li"):
                            cmd_text = li.get_text(strip=True)
                            if cmd_text:  # Only add non-empty commands
                                commands.append(cmd_text)

                        if commands:  # Only add sections that have commands
                            content.append(f"\n{heading.get_text(strip=True)}:")
                            content.extend(f"- {cmd}" for cmd in commands)

            if content:
                return [types.TextContent(type="text", text="\n".join(content))]

            # If we couldn't find any command sections, try to extract any code blocks
            code_blocks = soup.find_all(["pre", "code"])
            if code_blocks:
                content = ["Railway CLI Commands:"]
                for block in code_blocks:
                    code_text = block.get_text(strip=True)
                    if code_text and any(
                        keyword in code_text.lower() for keyword in ["railway", "cli"]
                    ):
                        content.append(code_text)
                return [types.TextContent(type="text", text="\n".join(content))]

            # If still nothing found, return a more helpful message
            return [
                types.TextContent(
                    type="text",
                    text="Could not find any CLI commands in the documentation. The page structure might have changed.",
                )
            ]
    except Exception as e:
        return [
            types.TextContent(
                type="text",
                text=f"Error: Failed to fetch or parse Railway CLI docs: {str(e)}",
            )
        ]


async def apply_prompt_initial(
    objective: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides an initial prompt template for starting a new coding objective.

    Args:
        objective: A description of the overall objective.
        specific_instructions: Optional specific instructions to include in the prompt.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the objective and specific instructions
    response_text = render_prompt_template(
        "init",
        version_str=version,
        objective=objective,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


async def apply_prompt_proceed(
    task: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides a prompt template for proceeding with a task or project.

    Args:
        task: A description of the task or project to proceed with.
        specific_instructions: Optional specific instructions to include in the prompt.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the task description and specific instructions
    response_text = render_prompt_template(
        "proceed",
        version_str=version,
        task=task,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


async def apply_prompt_change(
    change_request: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides a prompt for systematically handling change requests.

    Args:
        change_request: Description of the change request to implement.
        specific_instructions: Optional specific instructions to include in the prompt.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the change request and specific instructions
    response_text = render_prompt_template(
        "change",
        version_str=version,
        change_request=change_request,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


async def apply_prompt_fix(
    issue: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides a prompt for performing root cause analysis and fixing issues.

    Args:
        issue: A description of the issue to be analyzed and fixed.
        specific_instructions: Optional specific instructions to include in the prompt.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the issue and specific instructions
    response_text = render_prompt_template(
        "fix_general",
        version_str=version,
        issue=issue,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


async def apply_prompt_fix_linter(
    issue: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides a prompt for analyzing and fixing linter errors.

    Args:
        issue: A description of the linter errors to be analyzed and fixed.
        specific_instructions: Optional specific instructions to include in the prompt.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the issue and specific instructions
    response_text = render_prompt_template(
        "fix_linter",
        version_str=version,
        issue=issue,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


async def apply_prompt_unit_tests(
    code_to_test: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides a prompt for generating unit tests for code.

    Args:
        code_to_test: The code that needs unit tests.
        specific_instructions: Optional specific instructions to include in the prompt.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the code to test and specific instructions
    response_text = render_prompt_template(
        "test",
        version_str=version,
        code_to_test=code_to_test,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


async def apply_prompt_infra(
    infrastructure_info: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides a prompt template for laying out system infrastructure and tool stack information.

    Args:
        infrastructure_info: Description of the infrastructure and tool stack.
        specific_instructions: Optional specific instructions to include in the prompt.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the infrastructure info and specific instructions
    response_text = render_prompt_template(
        "infra",
        version_str=version,
        objective=infrastructure_info,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


async def apply_prompt_docker(
    containerization_objective: str,
    specific_instructions: str = "",
    version: str = "latest",
) -> list[types.TextContent]:
    """
    Provides a prompt template for Docker container configurations and orchestration.

    Args:
        containerization_objective: Description of the containerization objective.
        specific_instructions: Optional specific instructions about containerization requirements.
        version: The version of the prompt template to use. Defaults to "latest".

    Returns:
        A list containing a TextContent object with the prompt.
    """
    # Render the prompt template with the containerization objective and specific instructions
    response_text = render_prompt_template(
        "docker",
        version_str=version,
        objective=containerization_objective,
        specific_instructions=specific_instructions,
    )
    return [types.TextContent(type="text", text=response_text)]


@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",
)
def main(port: int, transport: str) -> int:
    app = Server("mcp-website-fetcher")

    mood_description: str = (
        "Ask this MCP server about its mood! You can phrase your question "
        "in any way you like - 'How are you?', 'What's your mood?', or even "
        "'Are you having a good day?'. The server will always respond with "
        "a cheerful message and a heart ❤️"
    )

    @app.call_tool()
    async def fetch_tool(  # type: ignore[unused-function]
        name: str, arguments: dict
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        if name == "mcp_fetch":
            if "url" not in arguments:
                return [
                    types.TextContent(
                        type="text", text="Error: Missing required argument 'url'"
                    )
                ]
            return await fetch_website(arguments["url"])
        elif name == "mood":
            if "question" not in arguments:
                return [
                    types.TextContent(
                        type="text", text="Error: Missing required argument 'question'"
                    )
                ]
            return await check_mood(arguments["question"])
        if name == "fetch_railway_docs":
            url = arguments.get("url", "https://docs.railway.app/guides/cli")
            return await fetch_railway_docs(url)
        if name == "fetch_railway_docs_optimized":
            url = arguments.get("url", "https://docs.railway.app/guides/cli")
            return await fetch_railway_docs_optimized(url)
        elif name == "apply_prompt_fix":
            if "issue" not in arguments:
                return [
                    types.TextContent(
                        type="text", text="Error: Missing required argument 'issue'"
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_fix(
                issue=arguments["issue"],
                specific_instructions=specific_instructions,
                version=version,
            )
        elif name == "apply_prompt_initial":
            if "objective" not in arguments:
                return [
                    types.TextContent(
                        type="text", text="Error: Missing required argument 'objective'"
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_initial(
                objective=arguments["objective"],
                specific_instructions=specific_instructions,
                version=version,
            )
        elif name == "apply_prompt_proceed":
            if "task" not in arguments:
                return [
                    types.TextContent(
                        type="text", text="Error: Missing required argument 'task'"
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_proceed(
                arguments["task"],
                specific_instructions=specific_instructions,
                version=version,
            )
        elif name == "apply_prompt_change":
            if "change_request" not in arguments:
                return [
                    types.TextContent(
                        type="text",
                        text="Error: Missing required argument 'change_request'",
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_change(
                change_request=arguments["change_request"],
                specific_instructions=specific_instructions,
                version=version,
            )
        elif name == "apply_prompt_fix_linter":
            if "issue" not in arguments:
                return [
                    types.TextContent(
                        type="text", text="Error: Missing required argument 'issue'"
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_fix_linter(
                issue=arguments["issue"],
                specific_instructions=specific_instructions,
                version=version,
            )
        elif name == "apply_prompt_unit_tests":
            if "code_to_test" not in arguments:
                return [
                    types.TextContent(
                        type="text",
                        text="Error: Missing required argument 'code_to_test'",
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_unit_tests(
                code_to_test=arguments["code_to_test"],
                specific_instructions=specific_instructions,
                version=version,
            )
        elif name == "apply_prompt_infra":
            if "infrastructure_info" not in arguments:
                return [
                    types.TextContent(
                        type="text",
                        text="Error: Missing required argument 'infrastructure_info'",
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_infra(
                infrastructure_info=arguments["infrastructure_info"],
                specific_instructions=specific_instructions,
                version=version,
            )
        elif name == "apply_prompt_docker":
            if "containerization_objective" not in arguments:
                return [
                    types.TextContent(
                        type="text",
                        text="Error: Missing required argument 'containerization_objective'",
                    )
                ]
            version = arguments.get("version", "latest")
            specific_instructions = arguments.get("specific_instructions", "")
            return await apply_prompt_docker(
                containerization_objective=arguments["containerization_objective"],
                specific_instructions=specific_instructions,
                version=version,
            )
        else:
            return [types.TextContent(type="text", text=f"Error: Unknown tool: {name}")]

    @app.list_tools()
    async def list_tools() -> list[types.Tool]:  # type: ignore[unused-function]
        return [
            types.Tool(
                name="mcp_fetch",
                description="Fetches a website and returns its content",
                inputSchema={
                    "type": "object",
                    "required": ["url"],
                    "properties": {
                        "url": {
                            "type": "string",
                            "description": "URL to fetch",
                        }
                    },
                },
            ),
            types.Tool(
                name="mood",
                description="Ask the server about its mood - it's always happy!",
                inputSchema={
                    "type": "object",
                    "required": ["question"],
                    "properties": {
                        "question": {
                            "type": "string",
                            "description": mood_description,
                        }
                    },
                },
            ),
            types.Tool(
                name="fetch_railway_docs",
                description="Fetches the most recent Railway CLI documentation. Optionally, provide a custom URL.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "url": {
                            "type": "string",
                            "description": "Optional custom URL for fetching Railway CLI docs.",
                        },
                    },
                },
            ),
            types.Tool(
                name="fetch_railway_docs_optimized",
                description="Fetches the most recent Railway CLI documentation. Optionally, provide a custom URL.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "url": {
                            "type": "string",
                            "description": "Optional custom URL for fetching Railway CLI docs.",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_fix",
                description="Provides a prompt for performing root cause analysis and fixing issues",
                inputSchema={
                    "type": "object",
                    "required": ["issue"],
                    "properties": {
                        "issue": {
                            "type": "string",
                            "description": "A description of the issue to be analyzed and fixed",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions to include in the prompt",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_initial",
                description="Provides an initial prompt template for starting a new project",
                inputSchema={
                    "type": "object",
                    "required": ["objective"],
                    "properties": {
                        "objective": {
                            "type": "string",
                            "description": "A description of the objective of the project",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions to include in the prompt",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_proceed",
                description="Provides a prompt template for proceeding with a task or project",
                inputSchema={
                    "type": "object",
                    "required": ["task"],
                    "properties": {
                        "task": {
                            "type": "string",
                            "description": "A description of the task or project to proceed with",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions to include in the prompt",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_change",
                description="Provides a prompt for systematically handling change requests",
                inputSchema={
                    "type": "object",
                    "required": ["change_request"],
                    "properties": {
                        "change_request": {
                            "type": "string",
                            "description": "Description of the change request to implement",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions to include in the prompt",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_fix_linter",
                description="Provides a prompt for analyzing and fixing linter errors",
                inputSchema={
                    "type": "object",
                    "required": ["issue"],
                    "properties": {
                        "issue": {
                            "type": "string",
                            "description": "A description of the linter errors to be analyzed and fixed",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions to include in the prompt",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_unit_tests",
                description="Provides a prompt for generating unit tests for code",
                inputSchema={
                    "type": "object",
                    "required": ["code_to_test"],
                    "properties": {
                        "code_to_test": {
                            "type": "string",
                            "description": "The code that needs unit tests",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions to include in the prompt",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_infra",
                description="Provides a prompt template for laying out system infrastructure and tool stack information",
                inputSchema={
                    "type": "object",
                    "required": ["infrastructure_info"],
                    "properties": {
                        "infrastructure_info": {
                            "type": "string",
                            "description": "Description of the infrastructure and tool stack",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions to include in the prompt",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
            types.Tool(
                name="apply_prompt_docker",
                description="Provides a prompt template for Docker container configurations and orchestration",
                inputSchema={
                    "type": "object",
                    "required": ["containerization_objective"],
                    "properties": {
                        "containerization_objective": {
                            "type": "string",
                            "description": "Description of the containerization objective",
                        },
                        "specific_instructions": {
                            "type": "string",
                            "description": "Optional specific instructions about containerization requirements",
                        },
                        "version": {
                            "type": "string",
                            "description": "The version of the prompt template to use (e.g., '1.0.0', '1.1.0', or 'latest')",
                        },
                    },
                },
            ),
        ]

    if transport == "sse":
        from mcp.server.sse import SseServerTransport

        sse = SseServerTransport("/messages/")

        async def handle_sse(request):
            if request.method == "POST":
                data = await request.json()
                tool_name = data.get("tool")
                arguments = data.get("arguments", {})
                result = await fetch_tool(tool_name, arguments)
                return JSONResponse({"result": serialize_content(result)})
            else:
                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, methods=["GET", "POST"]),
                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():
            async with stdio_server() as streams:
                await app.run(
                    streams[0], streams[1], app.create_initialization_options()
                )

        anyio.run(arun)

    return 0
