#!/usr/bin/env python
# coding: utf-8
from dotenv import load_dotenv, find_dotenv
from agent_utilities.base_utilities import to_boolean
import os
import sys

__version__ = "1.3.25"

from typing import Optional, Dict, List, Union, Any
from pydantic import Field
from starlette.requests import Request
from starlette.responses import JSONResponse
from fastmcp import FastMCP, Context
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.server.auth import OAuthProxy, RemoteAuthProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier, StaticTokenVerifier
from fastmcp.server.middleware.logging import LoggingMiddleware
from fastmcp.server.middleware.timing import TimingMiddleware
from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
import requests
import subprocess
import logging

from eunomia_mcp.middleware import EunomiaMcpMiddleware
from fastmcp.utilities.logging import get_logger
from agent_utilities.base_utilities import to_integer
from repository_manager.repository_manager import Git
from repository_manager.models import GitResult, ReadmeResult
from agent_utilities.base_utilities import get_library_file_path
from agent_utilities.mcp_utilities import (
    create_mcp_parser,
    config,
)
from agent_utilities.middlewares import (
    UserTokenMiddleware,
    JWTClaimsLoggingMiddleware,
)

logging.basicConfig(
    level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = get_logger("SearXNGMCPServer")


async def execute_bash_command(command: str) -> Dict[str, Any]:
    """
    Core logic for executing a bash command.
    Used by MCP tool and skills.
    """
    logger.debug(f"Executing bash command: {command}")
    try:
        result = subprocess.run(
            command, shell=True, capture_output=True, text=True, check=False
        )
        output = result.stdout
        if result.stderr:
            output += f"\nSTDERR:\n{result.stderr}"
        return {
            "status": 200 if result.returncode == 0 else 500,
            "output": output,
            "return_code": result.returncode,
        }
    except Exception as e:
        return {"status": 500, "error": str(e)}


def register_misc_tools(mcp: FastMCP):
    async def health_check(request: Request) -> JSONResponse:
        return JSONResponse({"status": "OK"})


def register_git_operations_tools(mcp: FastMCP):
    @mcp.tool(
        annotations={
            "title": "Execute Git Command",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": False,
            "openWorldHint": False,
        },
        tags={"git_operations"},
    )
    async def git_action(
        command: str = Field(
            description="The Git command to execute (e.g., 'git pull', 'git clone <repository_url>')"
        ),
        path: Optional[str] = Field(
            description="The path to execute the command in. Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", "/workspace"),
        ),
        threads: Optional[int] = Field(
            description="Number of threads for parallel processing. Defaults to REPOSITORY_MANAGER_THREADS env variable.",
            default=to_integer(os.environ.get("REPOSITORY_MANAGER_THREADS", "6")),
        ),
        set_to_default_branch: Optional[bool] = Field(
            description="Whether to checkout the default branch. Defaults to REPOSITORY_MANAGER_DEFAULT_BRANCH env variable.",
            default=to_boolean(
                os.environ.get("REPOSITORY_MANAGER_DEFAULT_BRANCH", False)
            ),
        ),
    ) -> GitResult:
        """
        Executes an arbitrary Git command in the specified path.
        Use this tool for any git operation not covered by specialized tools.
        """
        logger.debug(f"Executing git_action with command: {command}, path: {path}")
        try:
            git = Git(
                path=path,
                threads=threads,
                set_to_default_branch=set_to_default_branch,
                capture_output=True,
                is_mcp_server=True,
            )
            response = git.git_action(command=command, path=path)
            return response
        except Exception as e:
            logger.error(f"Error in git_action: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "List Git Projects",
            "readOnlyHint": True,
            "destructiveHint": False,
            "idempotentHint": True,
            "openWorldHint": False,
        },
        tags={"git_operations"},
    )
    async def list_projects(
        projects_file: Optional[str] = Field(
            description="Path to a file containing a list of repository URLs. Defaults to PROJECTS_FILE env variable.",
            default=os.environ.get(
                "PROJECTS_FILE", get_library_file_path(file="repositories-list.txt")
            ),
        ),
        path: Optional[str] = Field(
            description="The parent workspace containing the projects. Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", None),
        ),
    ) -> List[str]:
        """
        Lists all Git repositories found in the specified workspace path or defined in the projects file.
        Use this to discover available projects before performing operations.
        """
        logger.debug(f"Listing projects from file: {projects_file} and path: {path}")
        try:
            git = Git(
                path=path,
                projects=None,
                is_mcp_server=True,
            )

            if projects_file and os.path.exists(projects_file):
                git.read_project_list_file(file=projects_file)

            if path and os.path.exists(path):
                try:
                    for item in os.listdir(path):
                        if os.path.isdir(os.path.join(path, item)) and os.path.exists(
                            os.path.join(path, item, ".git")
                        ):
                            git.projects.append(item)
                except Exception:
                    pass

            return list(dict.fromkeys(git.projects))

        except Exception as e:
            logger.error(f"Error in list_projects: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Run Pre-Commit",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
            "openWorldHint": False,
        },
        tags={"git_operations"},
    )
    async def run_pre_commit(
        run: bool = Field(description="Run 'pre-commit run --all-files'", default=True),
        autoupdate: bool = Field(
            description="Run 'pre-commit autoupdate'", default=False
        ),
        path: Optional[str] = Field(
            description="The path to run pre-commit in (e.g., path/to/project). Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", None),
        ),
    ) -> GitResult:
        """
        Runs pre-commit hooks and/or autoupdate on a repository.
        Enhances code quality by running configured hooks.
        """
        logger.debug(
            f"Running pre-commit: run={run}, autoupdate={autoupdate}, path={path}"
        )
        try:
            target_dir = path
            if not target_dir:
                target_dir = os.getcwd()

            git = Git(
                path=target_dir,
                is_mcp_server=True,
            )

            response = git.pre_commit(run=run, autoupdate=autoupdate, path=target_dir)
            return response

        except Exception as e:
            logger.error(f"Error in run_pre_commit: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Clone Single Git Project",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": True,
            "openWorldHint": False,
        },
        tags={"git_operations"},
    )
    async def clone_project(
        url: str = Field(description="The repository URL to clone."),
        path: Optional[str] = Field(
            description="The path to clone the project into. Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", None),
        ),
        threads: Optional[int] = Field(
            description="Number of threads for parallel processing. Defaults to REPOSITORY_MANAGER_THREADS env variable.",
            default=to_integer(os.environ.get("REPOSITORY_MANAGER_THREADS", "6")),
        ),
        set_to_default_branch: Optional[bool] = Field(
            description="Whether to checkout the default branch. Defaults to REPOSITORY_MANAGER_DEFAULT_BRANCH env variable.",
            default=to_boolean(
                os.environ.get("REPOSITORY_MANAGER_DEFAULT_BRANCH", None)
            ),
        ),
    ) -> GitResult:
        """
        Clones a single Git repository from the provided URL to the specified path.
        """
        logger.debug(f"Cloning project: {url}, path: {path}")
        try:
            if not url:
                raise ValueError("url must not be empty")
            git = Git(
                path=path,
                threads=threads,
                set_to_default_branch=set_to_default_branch,
                capture_output=True,
                is_mcp_server=True,
            )
            response = git.clone_project(url=url)
            return response
        except Exception as e:
            logger.error(f"Error in clone_project: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Clone Multiple Git Projects",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": True,
            "openWorldHint": False,
        },
        tags={"git_operations"},
    )
    async def clone_projects(
        projects: Optional[List[str]] = Field(
            description="List of repository URLs to clone.", default=None
        ),
        projects_file: Optional[str] = Field(
            description="Path to a file containing a list of repository URLs. Defaults to PROJECTS_FILE env variable.",
            default=os.environ.get(
                "PROJECTS_FILE", get_library_file_path(file="repositories-list.txt")
            ),
        ),
        path: Optional[str] = Field(
            description="The path to clone projects into. Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", None),
        ),
        threads: Optional[int] = Field(
            description="Number of threads for parallel processing. Defaults to REPOSITORY_MANAGER_THREADS env variable.",
            default=to_integer(os.environ.get("REPOSITORY_MANAGER_THREADS", "6")),
        ),
        set_to_default_branch: Optional[bool] = Field(
            description="Whether to checkout the default branch. Defaults to REPOSITORY_MANAGER_DEFAULT_BRANCH env variable.",
            default=to_boolean(
                os.environ.get("REPOSITORY_MANAGER_DEFAULT_BRANCH", None)
            ),
        ),
    ) -> List[GitResult]:
        """
        Clones multiple Git repository URLs in parallel to the specified workspace path.
        Can use a list of URLs or a file containing URLs.
        """
        logger.debug(f"Cloning projects to path: {path}")
        try:
            if not projects and not projects_file:
                raise ValueError("Either projects or projects_file must be provided")
            if projects_file and not os.path.exists(projects_file):
                raise FileNotFoundError(f"Projects file not found: {projects_file}")
            if path and not os.path.exists(path):
                raise FileNotFoundError(f"Repository path not found: {path}")
            git = Git(
                path=path,
                projects=projects,
                threads=threads,
                set_to_default_branch=set_to_default_branch,
                capture_output=True,
                is_mcp_server=True,
            )
            if projects_file:
                git.read_project_list_file(file=projects_file)
            response = git.clone_projects()
            return response
        except Exception as e:
            logger.error(f"Error in clone_projects: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Pull Single Git Project",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": True,
            "openWorldHint": False,
        },
        tags={"git_operations"},
    )
    async def pull_project(
        path: str = Field(description="The path of the project directory to pull."),
        threads: Optional[int] = Field(
            description="Number of threads for parallel processing. Defaults to REPOSITORY_MANAGER_THREADS env variable.",
            default=to_integer(os.environ.get("REPOSITORY_MANAGER_THREADS", "6")),
        ),
        set_to_default_branch: Optional[bool] = Field(
            description="Whether to checkout the default branch. Defaults to REPOSITORY_MANAGER_DEFAULT_BRANCH env variable.",
            default=to_boolean(
                os.environ.get("REPOSITORY_MANAGER_DEFAULT_BRANCH", None)
            ),
        ),
    ) -> GitResult:
        """
        Performs a 'git pull' on the repository at the specified path.
        Useful for updating a specific project to the latest commit.
        """
        logger.debug(f"Pulling project: path: {path}")
        try:
            if not path:
                raise ValueError("path must not be empty")
            git = Git(
                path=path,  # Use path as base or let Git manage?
                threads=threads,
                set_to_default_branch=set_to_default_branch,
                capture_output=True,
                is_mcp_server=True,
            )
            # We want to pull specific project. Git initialized with path might assume path is the workspace.
            # But pull_project in Git takes 'path' (the project path).
            # If we call git.pull_project(path=path), it uses self._resolve_path(path).
            # So if we init Git(path=os.path.dirname(path)) it might be cleaner, but Git defaults to self.path.
            # If we init Git() (default), then pass absolute path, it should work.
            response = git.pull_project(path=path)
            return response
        except Exception as e:
            logger.error(f"Error in pull_project: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Pull Multiple Git Projects",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": True,
            "openWorldHint": False,
        },
        tags={"git_operations"},
    )
    async def pull_projects(
        path: Optional[str] = Field(
            description="The workspace containing the projects to pull. Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", None),
        ),
        threads: Optional[int] = Field(
            description="Number of threads for parallel processing. Defaults to REPOSITORY_MANAGER_THREADS env variable.",
            default=to_integer(os.environ.get("REPOSITORY_MANAGER_THREADS", "6")),
        ),
        set_to_default_branch: Optional[bool] = Field(
            description="Whether to checkout the default branch. Defaults to REPOSITORY_MANAGER_DEFAULT_BRANCH env variable.",
            default=to_boolean(
                os.environ.get("REPOSITORY_MANAGER_DEFAULT_BRANCH", None)
            ),
        ),
    ) -> List[GitResult]:
        """
        Pulls updates for all Git projects within the specified workspace path in parallel.
        Useful for bulk updating multiple repositories at once.
        """
        logger.debug(f"Pulling projects from path: {path}")
        try:
            if path and not os.path.exists(path):
                raise FileNotFoundError(f"Repository path not found: {path}")
            git = Git(
                path=path,
                threads=threads,
                set_to_default_branch=set_to_default_branch,
                capture_output=True,
                is_mcp_server=True,
            )
            response = git.pull_projects()
            return response
        except Exception as e:
            logger.error(f"Error in pull_projects: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Create Project",
            "description": "Create a new project directory and initialize it as a git repository.",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": False,
        },
        tags={"git_operations"},
    )
    async def create_project(
        path: str = Field(description="The path for the new project directory."),
    ) -> GitResult:
        """
        Create a new project directory at the specified path and initialize it as a git repository.
        Use this to start a new project managed by git.
        """
        try:
            # Init Git with base path or current directory, actual creation uses absolute path logic
            git = Git(
                path=os.getcwd(),
                is_mcp_server=True,
            )

            response = git.create_project(path=path)
            return response
        except Exception as e:
            logger.error(f"Error in create_project: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Bump Version",
            "description": "Bump the version of the project using bump2version.",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
        },
        tags={"git_operations"},
    )
    async def bump_version(
        part: str = Field(
            description="The part of the version to bump (major, minor, patch)."
        ),
        allow_dirty: bool = Field(
            description="Whether to allow dirty working directory.", default=True
        ),
        path: Optional[str] = Field(
            description="The path to the project directory. Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", None),
        ),
    ) -> GitResult:
        """
        Bumps the version of the project using bump2version.
        Automatically commits and tags the new version.
        """
        try:
            target_dir = path
            if not target_dir:
                target_dir = (
                    os.environ.get("REPOSITORY_MANAGER_WORKSPACE") or os.getcwd()
                )

            git = Git(
                path=target_dir,
                is_mcp_server=True,
            )

            response = git.bump_version(
                part=part,
                allow_dirty=allow_dirty,
                path=path,
            )
            return response
        except Exception as e:
            logger.error(f"Error in bump_version: {e}")
            raise


def register_file_operations_tools(mcp: FastMCP):
    @mcp.tool(
        annotations={
            "title": "Get Project README",
            "readOnlyHint": True,
            "destructiveHint": False,
            "idempotentHint": True,
            "openWorldHint": False,
        },
        tags={"file_operations"},
    )
    async def get_project_readme(
        path: Optional[str] = Field(
            description="The path to the project or directory. Defaults to REPOSITORY_MANAGER_WORKSPACE env variable.",
            default=os.environ.get("REPOSITORY_MANAGER_WORKSPACE", None),
        ),
    ) -> ReadmeResult:
        """
        Retrieves the content of the README.md file for a project or directory.
        Use this to quickly get an overview of a project.
        """
        logger.debug(f"Getting README for path: {path}")
        try:
            target_dir = path
            if not target_dir:
                target_dir = (
                    os.environ.get("REPOSITORY_MANAGER_WORKSPACE") or os.getcwd()
                )

            git = Git(
                path=target_dir,
                is_mcp_server=True,
            )
            response = git.get_readme(path=path)
            return response
        except Exception as e:
            logger.error(f"Error in get_project_readme: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Text Editor",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
            "openWorldHint": True,
        },
        tags={"file_operations"},
    )
    async def text_editor(
        command: str = Field(
            description="The command to execute: view, create, str_replace, insert, undo_edit."
        ),
        path: str = Field(
            description="Standardized file path relative to the project."
        ),
        file_text: Optional[str] = Field(
            description="The content to write to the file (for create command).",
            default=None,
        ),
        view_range: Optional[List[int]] = Field(
            description="The range of lines to view (for view command).", default=None
        ),
        old_str: Optional[str] = Field(
            description="The string to replace (for str_replace command).", default=None
        ),
        new_str: Optional[str] = Field(
            description="The new string (for str_replace and insert commands).",
            default=None,
        ),
        insert_line: Optional[int] = Field(
            description="The line number to insert at (for insert command).",
            default=None,
        ),
    ) -> GitResult:
        """
        A versatile file system editor tool.
        Supports viewing, creating, replacing text, and inserting text in files.
        """
        logger.debug(f"Executing text_editor with command: {command}, path: {path}")

        try:
            # We don't necessarily need to init Git with a specific path if path is absolute
            # But the underlying method uses self._resolve_path.
            git = Git(
                path=os.getcwd(),  # Default to CWD, let resolve_path handle the provided path
                is_mcp_server=True,
            )

            response = git.text_editor(
                command=command,
                path=path,
                file_text=file_text,
                view_range=view_range,
                old_str=old_str,
                new_str=new_str,
                insert_line=insert_line,
            )
            return response

        except Exception as e:
            logger.error(f"Error in text_editor: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Create Directory",
            "description": "Create a new directory at the specified path.",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": False,
        },
        tags={"file_operations"},
    )
    async def create_directory(
        path: str = Field(
            description="The path where the directory should be created."
        ),
    ) -> GitResult:
        """
        Create a new directory at the specified path.
        """
        try:
            git = Git(
                path=os.getcwd(),
                is_mcp_server=True,
            )

            response = git.create_directory(path=path)
            return response
        except Exception as e:
            logger.error(f"Error in create_directory: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Delete Directory",
            "description": "Delete a directory at the specified path.",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
        },
        tags={"file_operations"},
    )
    async def delete_directory(
        path: str = Field(description="The path of the directory to delete."),
    ) -> GitResult:
        """
        Recursively delete a directory at the specified path.
        Use with caution as this action is destructive.
        """
        try:
            git = Git(
                path=os.getcwd(),
                is_mcp_server=True,
            )

            response = git.delete_directory(path=path)
            return response
        except Exception as e:
            logger.error(f"Error in delete_directory: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Rename Directory",
            "description": "Rename/Move a directory or file.",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
        },
        tags={"file_operations"},
    )
    async def rename_directory(
        old_path: str = Field(description="The current path."),
        new_path: str = Field(description="The new path."),
    ) -> GitResult:
        """
        Rename or move a directory or file from old_path to new_path.
        """
        try:
            git = Git(
                path=os.getcwd(),
                is_mcp_server=True,
            )

            response = git.rename_directory(
                old_path=old_path,
                new_path=new_path,
            )
            return response
        except Exception as e:
            logger.error(f"Error in rename_directory: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Search Codebase",
            "description": "Search the codebase using ripgrep.",
            "readOnlyHint": True,
            "destructiveHint": False,
            "idempotentHint": True,
        },
        tags={"file_operations"},
    )
    async def search_codebase(
        query: str = Field(description="The regex pattern to search for."),
        path: Optional[str] = Field(
            description="The path to search in (absolute or relative to CWD). Defaults to CWD or workspace.",
            default=None,
        ),
        glob_pattern: Optional[str] = Field(
            description="Glob pattern to filter files (e.g., '*.py').", default=None
        ),
        case_sensitive: bool = Field(
            description="Whether the search should be case sensitive.", default=False
        ),
    ) -> GitResult:
        """
        Search for a regex pattern in the codebase using ripgrep.
        Supports filtering by glob pattern and case sensitivity.
        """
        try:
            target_dir = path
            if not target_dir:
                target_dir = (
                    os.environ.get("REPOSITORY_MANAGER_WORKSPACE") or os.getcwd()
                )

            git = Git(
                path=target_dir,
                is_mcp_server=True,
            )

            response = git.search_codebase(
                query=query,
                path=path,
                glob_pattern=glob_pattern,
                case_sensitive=case_sensitive,
            )
            return response
        except Exception as e:
            logger.error(f"Error in search_codebase: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Find Files",
            "description": "Find files using find.",
            "readOnlyHint": True,
            "destructiveHint": False,
            "idempotentHint": True,
        },
        tags={"file_operations"},
    )
    async def find_files(
        name_pattern: str = Field(
            description="The name pattern to search for (e.g., '*.py')."
        ),
        path: Optional[str] = Field(
            description="The path to search in (absolute or relative to CWD). Defaults to CWD or workspace.",
            default=None,
        ),
    ) -> GitResult:
        """
        Find files in the codebase matching a name pattern.
        """
        try:
            target_dir = path
            if not target_dir:
                target_dir = (
                    os.environ.get("REPOSITORY_MANAGER_WORKSPACE") or os.getcwd()
                )

            git = Git(
                path=target_dir,
                is_mcp_server=True,
            )

            response = git.find_files(
                name_pattern=name_pattern,
                path=path,
            )
            return response
        except Exception as e:
            logger.error(f"Error in find_files: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Read File",
            "description": "Read a file from the codebase.",
            "readOnlyHint": True,
            "destructiveHint": False,
            "idempotentHint": True,
        },
        tags={"file_operations"},
    )
    async def read_file(
        path: str = Field(
            description="The path to the file to read (absolute or relative to CWD)."
        ),
        start_line: Optional[int] = Field(
            description="The starting line number (1-indexed).", default=None
        ),
        end_line: Optional[int] = Field(
            description="The ending line number (1-indexed).", default=None
        ),
    ) -> GitResult:
        """
        Reads the content of a file, optionally within a specific line range.
        use this to inspect code or configuration files.
        """
        try:
            git = Git(
                path=os.getcwd(),
                is_mcp_server=True,
            )

            response = git.read_file(
                path=path,
                start_line=start_line,
                end_line=end_line,
            )
            return response
        except Exception as e:
            logger.error(f"Error in read_file: {e}")
            raise

    @mcp.tool(
        annotations={
            "title": "Replace In File",
            "description": "Replace a block of text in a file.",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
        },
        tags={"file_operations"},
    )
    async def replace_in_file(
        path: str = Field(
            description="The path to the file to modify (absolute or relative to CWD)."
        ),
        target_content: str = Field(description="The exact content to be replaced."),
        replacement_content: str = Field(
            description="The new content to replace with."
        ),
    ) -> GitResult:
        """
        Replaces a specific block of text in a file with new content.
        Ensure target_content matches exactly, including whitespace.
        """
        try:
            git = Git(
                path=os.getcwd(),
                is_mcp_server=True,
            )

            response = git.replace_in_file(
                path=path,
                target_content=target_content,
                replacement_content=replacement_content,
            )
            return response
        except Exception as e:
            logger.error(f"Error in replace_in_file: {e}")
            raise


def register_system_operations_tools(mcp: FastMCP):
    @mcp.tool(
        annotations={
            "title": "Run Command",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
            "openWorldHint": True,
        },
        tags={"system_operations"},
    )
    async def run_command(
        command: str = Field(description="The command to run"),
        ctx: Context = Field(
            description="MCP context for progress reporting.", default=None
        ),
    ) -> Dict[str, Any]:
        """
        Executes a bash command on the local system.
        Use with caution. Returns exit code, stdout, and stderr.
        """
        if ctx:
            await ctx.report_progress(progress=0, total=100)

        result = await execute_bash_command(command)

        if ctx:
            await ctx.report_progress(progress=100, total=100)
        return result


def mcp_server():
    load_dotenv(find_dotenv())
    print(f"Repository Manager MCP v{__version__}")
    parser = create_mcp_parser()

    args = parser.parse_args()

    if hasattr(args, "help") and args.help:

        parser.print_help()

        sys.exit(0)

    if args.port < 0 or args.port > 65535:
        print(f"Error: Port {args.port} is out of valid range (0-65535).")
        sys.exit(1)

    config["enable_delegation"] = args.enable_delegation
    config["audience"] = args.audience or config["audience"]
    config["delegated_scopes"] = args.delegated_scopes or config["delegated_scopes"]
    config["oidc_config_url"] = args.oidc_config_url or config["oidc_config_url"]
    config["oidc_client_id"] = args.oidc_client_id or config["oidc_client_id"]
    config["oidc_client_secret"] = (
        args.oidc_client_secret or config["oidc_client_secret"]
    )

    if config["enable_delegation"]:
        if args.auth_type != "oidc-proxy":
            logger.error("Token delegation requires auth-type=oidc-proxy")
            sys.exit(1)
        if not config["audience"]:
            logger.error("audience is required for delegation")
            sys.exit(1)
        if not all(
            [
                config["oidc_config_url"],
                config["oidc_client_id"],
                config["oidc_client_secret"],
            ]
        ):
            logger.error(
                "Delegation requires complete OIDC configuration (oidc-config-url, oidc-client-id, oidc-client-secret)"
            )
            sys.exit(1)

        try:
            logger.info(
                "Fetching OIDC configuration",
                extra={"oidc_config_url": config["oidc_config_url"]},
            )
            oidc_config_resp = requests.get(config["oidc_config_url"])
            oidc_config_resp.raise_for_status()
            oidc_config = oidc_config_resp.json()
            config["token_endpoint"] = oidc_config.get("token_endpoint")
            if not config["token_endpoint"]:
                logger.error("No token_endpoint found in OIDC configuration")
                raise ValueError("No token_endpoint found in OIDC configuration")
            logger.info(
                "OIDC configuration fetched successfully",
                extra={"token_endpoint": config["token_endpoint"]},
            )
        except Exception as e:
            print(f"Failed to fetch OIDC configuration: {e}")
            logger.error(
                "Failed to fetch OIDC configuration",
                extra={"error_type": type(e).__name__, "error_message": str(e)},
            )
            sys.exit(1)

    auth = None
    allowed_uris = (
        args.allowed_client_redirect_uris.split(",")
        if args.allowed_client_redirect_uris
        else None
    )

    if args.auth_type == "none":
        auth = None
    elif args.auth_type == "static":
        auth = StaticTokenVerifier(
            tokens={
                "test-token": {"client_id": "test-user", "scopes": ["read", "write"]},
                "admin-token": {"client_id": "admin", "scopes": ["admin"]},
            }
        )
    elif args.auth_type == "jwt":
        jwks_uri = args.token_jwks_uri or os.getenv("FASTMCP_SERVER_AUTH_JWT_JWKS_URI")
        issuer = args.token_issuer or os.getenv("FASTMCP_SERVER_AUTH_JWT_ISSUER")
        audience = args.token_audience or os.getenv("FASTMCP_SERVER_AUTH_JWT_AUDIENCE")
        algorithm = args.token_algorithm
        secret_or_key = args.token_secret or args.token_public_key
        public_key_pem = None

        if not (jwks_uri or secret_or_key):
            logger.error(
                "JWT auth requires either --token-jwks-uri or --token-secret/--token-public-key"
            )
            sys.exit(1)
        if not (issuer and audience):
            logger.error("JWT requires --token-issuer and --token-audience")
            sys.exit(1)

        if args.token_public_key and os.path.isfile(args.token_public_key):
            try:
                with open(args.token_public_key, "r") as f:
                    public_key_pem = f.read()
                logger.info(f"Loaded static public key from {args.token_public_key}")
            except Exception as e:
                print(f"Failed to read public key file: {e}")
                logger.error(f"Failed to read public key file: {e}")
                sys.exit(1)
        elif args.token_public_key:
            public_key_pem = args.token_public_key

        if jwks_uri and (algorithm or secret_or_key):
            logger.warning(
                "JWKS mode ignores --token-algorithm and --token-secret/--token-public-key"
            )

        if algorithm and algorithm.startswith("HS"):
            if not secret_or_key:
                logger.error(f"HMAC algorithm {algorithm} requires --token-secret")
                sys.exit(1)
            if jwks_uri:
                logger.error("Cannot use --token-jwks-uri with HMAC")
                sys.exit(1)
            public_key = secret_or_key
        else:
            public_key = public_key_pem

        required_scopes = None
        if args.required_scopes:
            required_scopes = [
                s.strip() for s in args.required_scopes.split(",") if s.strip()
            ]

        try:
            auth = JWTVerifier(
                jwks_uri=jwks_uri,
                public_key=public_key,
                issuer=issuer,
                audience=audience,
                algorithm=(
                    algorithm if algorithm and algorithm.startswith("HS") else None
                ),
                required_scopes=required_scopes,
            )
            logger.info(
                "JWTVerifier configured",
                extra={
                    "mode": (
                        "JWKS"
                        if jwks_uri
                        else (
                            "HMAC"
                            if algorithm and algorithm.startswith("HS")
                            else "Static Key"
                        )
                    ),
                    "algorithm": algorithm,
                    "required_scopes": required_scopes,
                },
            )
        except Exception as e:
            print(f"Failed to initialize JWTVerifier: {e}")
            logger.error(f"Failed to initialize JWTVerifier: {e}")
            sys.exit(1)
    elif args.auth_type == "oauth-proxy":
        if not (
            args.oauth_upstream_auth_endpoint
            and args.oauth_upstream_token_endpoint
            and args.oauth_upstream_client_id
            and args.oauth_upstream_client_secret
            and args.oauth_base_url
            and args.token_jwks_uri
            and args.token_issuer
            and args.token_audience
        ):
            print(
                "oauth-proxy requires oauth-upstream-auth-endpoint, oauth-upstream-token-endpoint, "
                "oauth-upstream-client-id, oauth-upstream-client-secret, oauth-base-url, token-jwks-uri, "
                "token-issuer, token-audience"
            )
            logger.error(
                "oauth-proxy requires oauth-upstream-auth-endpoint, oauth-upstream-token-endpoint, "
                "oauth-upstream-client-id, oauth-upstream-client-secret, oauth-base-url, token-jwks-uri, "
                "token-issuer, token-audience",
                extra={
                    "auth_endpoint": args.oauth_upstream_auth_endpoint,
                    "token_endpoint": args.oauth_upstream_token_endpoint,
                    "client_id": args.oauth_upstream_client_id,
                    "base_url": args.oauth_base_url,
                    "jwks_uri": args.token_jwks_uri,
                    "issuer": args.token_issuer,
                    "audience": args.token_audience,
                },
            )
            sys.exit(1)
        token_verifier = JWTVerifier(
            jwks_uri=args.token_jwks_uri,
            issuer=args.token_issuer,
            audience=args.token_audience,
        )
        auth = OAuthProxy(
            upstream_authorization_endpoint=args.oauth_upstream_auth_endpoint,
            upstream_token_endpoint=args.oauth_upstream_token_endpoint,
            upstream_client_id=args.oauth_upstream_client_id,
            upstream_client_secret=args.oauth_upstream_client_secret,
            token_verifier=token_verifier,
            base_url=args.oauth_base_url,
            allowed_client_redirect_uris=allowed_uris,
        )
    elif args.auth_type == "oidc-proxy":
        if not (
            args.oidc_config_url
            and args.oidc_client_id
            and args.oidc_client_secret
            and args.oidc_base_url
        ):
            logger.error(
                "oidc-proxy requires oidc-config-url, oidc-client-id, oidc-client-secret, oidc-base-url",
                extra={
                    "config_url": args.oidc_config_url,
                    "client_id": args.oidc_client_id,
                    "base_url": args.oidc_base_url,
                },
            )
            sys.exit(1)
        auth = OIDCProxy(
            config_url=args.oidc_config_url,
            client_id=args.oidc_client_id,
            client_secret=args.oidc_client_secret,
            base_url=args.oidc_base_url,
            allowed_client_redirect_uris=allowed_uris,
        )
    elif args.auth_type == "remote-oauth":
        if not (
            args.remote_auth_servers
            and args.remote_base_url
            and args.token_jwks_uri
            and args.token_issuer
            and args.token_audience
        ):
            logger.error(
                "remote-oauth requires remote-auth-servers, remote-base-url, token-jwks-uri, token-issuer, token-audience",
                extra={
                    "auth_servers": args.remote_auth_servers,
                    "base_url": args.remote_base_url,
                    "jwks_uri": args.token_jwks_uri,
                    "issuer": args.token_issuer,
                    "audience": args.token_audience,
                },
            )
            sys.exit(1)
        auth_servers = [url.strip() for url in args.remote_auth_servers.split(",")]
        token_verifier = JWTVerifier(
            jwks_uri=args.token_jwks_uri,
            issuer=args.token_issuer,
            audience=args.token_audience,
        )
        auth = RemoteAuthProvider(
            token_verifier=token_verifier,
            authorization_servers=auth_servers,
            base_url=args.remote_base_url,
        )

    middlewares: List[
        Union[
            UserTokenMiddleware,
            ErrorHandlingMiddleware,
            RateLimitingMiddleware,
            TimingMiddleware,
            LoggingMiddleware,
            JWTClaimsLoggingMiddleware,
            EunomiaMcpMiddleware,
        ]
    ] = [
        ErrorHandlingMiddleware(include_traceback=True, transform_errors=True),
        RateLimitingMiddleware(max_requests_per_second=10.0, burst_capacity=20),
        TimingMiddleware(),
        LoggingMiddleware(),
        JWTClaimsLoggingMiddleware(),
    ]
    if config["enable_delegation"] or args.auth_type == "jwt":
        middlewares.insert(0, UserTokenMiddleware(config=config))

    if args.eunomia_type in ["embedded", "remote"]:
        try:
            from eunomia_mcp import create_eunomia_middleware

            policy_file = args.eunomia_policy_file or "mcp_policies.json"
            eunomia_endpoint = (
                args.eunomia_remote_url if args.eunomia_type == "remote" else None
            )
            eunomia_mw = create_eunomia_middleware(
                policy_file=policy_file, eunomia_endpoint=eunomia_endpoint
            )
            middlewares.append(eunomia_mw)
            logger.info(f"Eunomia middleware enabled ({args.eunomia_type})")
        except Exception as e:
            print(f"Failed to load Eunomia middleware: {e}")
            logger.error("Failed to load Eunomia middleware", extra={"error": str(e)})
            sys.exit(1)

    mcp = FastMCP(name="GitRepositoryManager", auth=auth)
    DEFAULT_MISCTOOL = to_boolean(os.getenv("MISCTOOL", "True"))
    if DEFAULT_MISCTOOL:
        register_misc_tools(mcp)
    DEFAULT_GIT_OPERATIONSTOOL = to_boolean(os.getenv("GIT_OPERATIONSTOOL", "True"))
    if DEFAULT_GIT_OPERATIONSTOOL:
        register_git_operations_tools(mcp)
    DEFAULT_FILE_OPERATIONSTOOL = to_boolean(os.getenv("FILE_OPERATIONSTOOL", "True"))
    if DEFAULT_FILE_OPERATIONSTOOL:
        register_file_operations_tools(mcp)
    DEFAULT_SYSTEM_OPERATIONSTOOL = to_boolean(
        os.getenv("SYSTEM_OPERATIONSTOOL", "True")
    )
    if DEFAULT_SYSTEM_OPERATIONSTOOL:
        register_system_operations_tools(mcp)

    for mw in middlewares:
        mcp.add_middleware(mw)

    print("\nStarting Git MCP Server")
    print(f"  Transport: {args.transport.upper()}")
    print(f"  Auth: {args.auth_type}")
    print(f"  Delegation: {'ON' if config['enable_delegation'] else 'OFF'}")
    print(f"  Eunomia: {args.eunomia_type}")

    if args.transport == "stdio":
        mcp.run(transport="stdio")
    elif args.transport == "streamable-http":
        mcp.run(transport="streamable-http", host=args.host, port=args.port)
    elif args.transport == "sse":
        mcp.run(transport="sse", host=args.host, port=args.port)
    else:
        logger.error("Invalid transport", extra={"transport": args.transport})
        sys.exit(1)


if __name__ == "__main__":
    mcp_server()
