import argparse
import os
import sys
from contextlib import suppress

import anyio
from lean_lsp_mcp.client_utils import infer_project_path
from lean_lsp_mcp.server import mcp

_TRANSPORT_CLOSE_HINTS = (
    "transport closed",
    "connection closed",
    "broken pipe",
)

_TRANSPORT_CLOSE_EXCEPTIONS = (
    BrokenPipeError,
    ConnectionResetError,
    EOFError,
    anyio.BrokenResourceError,
    anyio.ClosedResourceError,
    anyio.EndOfStream,
)


def _is_transport_closed_error(exc: BaseException) -> bool:
    """Walk exception tree (groups, __cause__, __context__) for transport errors."""
    stack: list[BaseException] = [exc]
    seen: set[int] = set()

    while stack:
        current = stack.pop()
        if id(current) in seen:
            continue
        seen.add(id(current))

        if isinstance(current, _TRANSPORT_CLOSE_EXCEPTIONS):
            return True
        if any(h in str(current).lower() for h in _TRANSPORT_CLOSE_HINTS):
            return True

        # Traverse ExceptionGroup-like .exceptions and chained causes
        nested = getattr(current, "exceptions", None)
        if isinstance(nested, (tuple, list)):
            stack.extend(e for e in nested if isinstance(e, BaseException))
        for linked in (current.__cause__, current.__context__):
            if isinstance(linked, BaseException):
                stack.append(linked)

    return False


def _silence_stdout() -> None:
    with suppress(Exception):
        sys.stdout.flush()

    with suppress(Exception):
        sys.stdout = open(os.devnull, "w", encoding="utf-8")


def main():
    parser = argparse.ArgumentParser(description="Lean LSP MCP Server")
    parser.add_argument(
        "--transport",
        type=str,
        choices=["stdio", "streamable-http", "sse"],
        default="stdio",
        help="Transport method for the server. Default is 'stdio'.",
    )
    parser.add_argument(
        "--host",
        type=str,
        default="127.0.0.1",
        help="Host address for transport",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=8000,
        help="Host port for transport",
    )
    parser.add_argument(
        "--lean-project-path",
        type=str,
        help=("Path to a Lean project root or to a file/dir inside it."),
    )
    parser.add_argument(
        "--loogle-local",
        action="store_true",
        help="Enable local loogle (auto-installs on first run, ~5-10 min). "
        "Avoids rate limits and network dependencies.",
    )
    parser.add_argument(
        "--loogle-cache-dir",
        type=str,
        help="Override loogle cache location (default: ~/.cache/lean-lsp-mcp/loogle)",
    )
    parser.add_argument(
        "--repl",
        action="store_true",
        help="Enable fast REPL-based multi-attempt (~5x faster). Requires Lean REPL.",
    )
    parser.add_argument(
        "--repl-timeout",
        type=int,
        help="REPL command timeout in seconds (default: 60)",
    )
    args = parser.parse_args()

    # Set env vars from CLI args (CLI takes precedence over env vars)
    if args.lean_project_path:
        project_path = infer_project_path(args.lean_project_path)
        if project_path is None:
            parser.error(f"No lean-toolchain found for: {args.lean_project_path}")
        os.environ["LEAN_PROJECT_PATH"] = str(project_path)
    if args.loogle_local:
        os.environ["LEAN_LOOGLE_LOCAL"] = "true"
    if args.loogle_cache_dir:
        os.environ["LEAN_LOOGLE_CACHE_DIR"] = args.loogle_cache_dir
    if args.repl:
        os.environ["LEAN_REPL"] = "true"
    if args.repl_timeout:
        os.environ["LEAN_REPL_TIMEOUT"] = str(args.repl_timeout)
    os.environ["LEAN_LSP_MCP_ACTIVE_TRANSPORT"] = args.transport

    mcp.settings.host = args.host
    mcp.settings.port = args.port
    try:
        mcp.run(transport=args.transport)
    except KeyboardInterrupt:
        return 130
    except Exception as exc:
        if args.transport == "stdio" and _is_transport_closed_error(exc):
            _silence_stdout()
            return 0
        raise
    return 0
