"""Tools for calling an instrument via SCPI commands."""

import argparse
import functools
import json
import logging
import sys
import typing

try:
    # noinspection PyUnusedImports
    from fastmcp import FastMCP
    # noinspection PyUnusedImports
    from starlette.responses import JSONResponse

    MCP_INSTALLED = True
except ImportError:
    MCP_INSTALLED = False

# noinspection PyProtectedMember
from RsInstrument import RsInstrument, __version__

logger = logging.getLogger(__name__)


def safe_tool(fn):
    """Decorator converting any raised Exception to standardized 'Error: <msg>' string.

    Applied to MCP-exposed functions so they never raise.
    """

    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        """Wrapper function catching exceptions."""
        try:
            return fn(*args, **kwargs)
        except Exception as e:  # pylint: disable=broad-except
            logger.error("Exception in tool %s: %s", fn.__name__, e)
            return f"Error: {e}"

    return wrapper


@safe_tool
def instrument_query_scpi(command: str, resource: str, opc_timeout: int = 5000) -> str:
    """Query a command from an instrument via RsInstrument.

    Args:
        command: The SCPI query command to send to the instrument.
        resource: The VISA resource string of the instrument.
        opc_timeout: Timeout in milliseconds for the operation complete (OPC) query.
            Default is 5000 ms.

    Returns:
        The response from the instrument.
    """
    with RsInstrument(resource) as inst:
        inst.opc_timeout = opc_timeout
        response = inst.query(command)
        return response.strip()


@safe_tool
def instrument_write_scpi(command: str, resource: str, opc_timeout: int = 5000) -> str:
    """Write a command to an instrument via RsInstrument.

    Args:
        command: The SCPI write command to send to the instrument.
        resource: The VISA resource string of the instrument.
        opc_timeout: Timeout in milliseconds for the operation complete (OPC) query.
            Default is 5000 ms.

    Returns:
        The response from the instrument.
    """
    with RsInstrument(resource) as inst:
        inst.opc_timeout = opc_timeout
        inst.write(command)
        return "Write command executed successfully."


@safe_tool
def instrument_fetch_errors(resource: str, opc_timeout: int = 5000) -> str:
    """Fetch errors from an instrument via RsInstrument.

    Args:
        resource: The VISA resource string of the instrument.
        opc_timeout: Timeout in milliseconds for the operation complete (OPC) query.
            Default is 5000 ms.

    Returns:
        The response from the instrument.
    """
    with RsInstrument(resource) as inst:
        inst.opc_timeout = opc_timeout
        errors = inst.query_all_errors_with_codes()
        if not errors:
            return "No errors."

        return json.dumps([{"code": code, "message": msg} for code, msg in errors])


@safe_tool
def instrument_reset(resource: str, opc_timeout: int = 5000) -> str:
    """Reset an instrument via RsInstrument.

    Args:
        resource: The VISA resource string of the instrument.
        opc_timeout: Timeout in milliseconds for the operation complete (OPC) query.
            Default is 5000 ms.

    Returns:
        The response from the instrument.
    """
    with RsInstrument(resource) as inst:
        inst.opc_timeout = opc_timeout
        inst.reset()
        return "Instrument reset successfully."


def create_fastmcp_server(
    *args,
    tools: typing.Sequence[typing.Tuple[str, str, typing.Callable]] | None = None,
    health_endpoint: str = "/healthz",
    **kwargs,
):
    """Create a FastMCP tool for SCPI commands.

    Args:
        *args: Positional arguments to pass to FastMCP.
        tools: Register a sequence of tuples containing tool names, descriptions and their corresponding callables.
        health_endpoint: The FastMCP health endpoint.
        **kwargs: Keyword arguments to pass to FastMCP.
    """
    if not MCP_INSTALLED:
        raise ImportError(
            "mcp is required for this module. Please install with 'pip install RsInstrument[mcp]'"
        )
    if tools is None:
        tools = []
    name = f"{__package__}-mcp"
    kwargs.setdefault("name", name)
    fastmcp = FastMCP(*args, **kwargs)  # type: ignore

    @fastmcp.custom_route(health_endpoint, methods=["GET"])
    async def health_check(_):
        """Health check endpoint."""
        return JSONResponse(  # type: ignore
            {"status": "healthy", "service": name, "version": __version__}
        )

    fastmcp.tool(
        name="Instrument-Query-SCPI",
        description="Query a command from an instrument via RsInstrument.",
    )(instrument_query_scpi)
    fastmcp.tool(
        name="Instrument-Write-SCPI",
        description="Write a command to an instrument via RsInstrument.",
    )(instrument_write_scpi)
    fastmcp.tool(
        name="Instrument-Fetch-Errors",
        description="Fetch errors from an instrument via RsInstrument.",
    )(instrument_fetch_errors)
    fastmcp.tool(name="Instrument-Reset", description="Reset the instrument")(
        instrument_reset
    )
    for tool_name, tool_description, tool_callable in tools:
        fastmcp.tool(name=tool_name, description=tool_description)(tool_callable)
    return fastmcp


def run(
    *args,
    host: str = "localhost",
    port: int = 8000,
    transport: typing.Literal["stdio", "sse", "streamable-http"] = "stdio",
    tools: typing.Sequence[typing.Tuple[str, str, typing.Callable]] | None = None,
    health_endpoint: str = "/healthz",
    show_fastmcp_banner: bool = False,
    **kwargs,
):
    """Run the MCP server.

    Args:
        *args: Positional arguments to pass to FastMCP.
        host: The FastMCP Hostname/IP to bind.
        port: The FastMCP port to bind.
        transport: The FastMCP transport protocol to use. Options are 'stdio', 'sse', and 'streamable-http'.
        tools: Register a sequence of tuples containing tool names, descriptions and their corresponding callables.
        health_endpoint: The FastMCP health endpoint.
        show_fastmcp_banner: Whether to show the FastMCP banner on startup. Defaults to False.
        **kwargs: Keyword arguments to pass to FastMCP.
    """
    mcp = create_fastmcp_server(*args, tools=tools, health_endpoint=health_endpoint, **kwargs)
    if transport != "stdio":
        run_kwargs = {"host": host, "port": port}
    else:
        run_kwargs = {}
    if not show_fastmcp_banner:
        logger.info("Starting RsInstrument-mcp server...")
    mcp.run(transport=transport, show_banner=show_fastmcp_banner, **run_kwargs)


def create_parser() -> argparse.ArgumentParser:
    """Create command line argument parser."""
    parser = argparse.ArgumentParser(description="Run the RsInstrument MCP server.")
    parser.add_argument(
        "-V",
        "--version",
        help="Show version number and exit",
        action="version",
        version=__version__,
    )
    parser.add_argument(
        "-v",
        "--verbose",
        dest="verbose",
        help="Increase output (Option is additive to increase verbosity)",
        action="count",
        default=0,
    )
    parser.add_argument(
        "-q",
        "--quiet",
        dest="quiet",
        help="Reduce output (Option is additive to decrease verbosity)",
        action="count",
        default=0,
    )
    parser.add_argument(
        "--host",
        dest="host",
        default="localhost",
        help="FastMCP Hostname/IP to bind (default: %(default)s)",
    )
    parser.add_argument(
        "--port",
        dest="port",
        type=int,
        default=8000,
        help="FastMCP port to bind (default: %(default)s)",
    )
    parser.add_argument(
        "--transport",
        dest="transport",
        type=str,
        choices=["stdio", "sse", "streamable-http"],
        default="streamable-http",
        help="FastMCP transport protocol (default: %(default)s)",
    )
    parser.add_argument(
        "--health-endpoint",
        dest="health_endpoint",
        type=str,
        default="/healthz",
        help="FastMCP health endpoint (default: %(default)s)",
    )
    parser.add_argument(
        "--show-fastmcp-banner",
        dest="show_fastmcp_banner",
        action="store_true",
        help="Show FastMCP banner on startup",
    )
    return parser


def main(argv: typing.Sequence[str] | None = None):
    """Main entry point for command line execution."""
    args = create_parser().parse_args(argv)

    logging.basicConfig(
        format="{asctime} [{levelname:^8}] ({filename}:{lineno}) {message}",
        datefmt="%Y-%m-%d %H:%M:%S",
        style="{",
    )

    default_log_level = logging.WARNING
    verbosity = default_log_level - ((args.verbose - args.quiet) * 10)
    log_level = min(logging.CRITICAL, max(logging.DEBUG, verbosity))
    logger.setLevel(log_level)
    try:
        run(
            transport=args.transport,
            host=args.host,
            port=args.port,
            health_endpoint=args.health_endpoint,
            show_fastmcp_banner=args.show_fastmcp_banner,
        )
    except (
        Exception
    ) as error:  # pragma: no cover - exercised in tests with forced exception
        if verbosity < default_log_level or default_log_level <= logging.DEBUG:
            logger.exception(error)
        else:
            logger.error(error)
            logger.warning("Hint: Rerun with '--verbose' to show exception traceback.")
        sys.exit(1)
    except KeyboardInterrupt:  # pragma: no cover - exercised in tests
        logger.warning("Aborted by user")
        sys.exit(1)


if __name__ == "__main__":
    main()
