"""A360 MCP CLI: argument resolution, env expansion, lifecycle, and transport.

The CLI is intentionally lean — it has none of the sibling ServiceNow MCP's
multi-instance alias resolution, install/remove subcommands, or update checks.
A360 has one Control Room per process; what it does need is:

* **3-tier precedence.** CLI arg → environment variable → built-in default.
  ``${VAR}`` references expand *only* when the whole value is a single
  reference. A partial expansion like ``${VAULT}_prod`` is risky: the result
  looks intentional but silently becomes the literal string. We reject it
  loudly rather than guess.
* **Fail-fast on unfilled placeholders.** Templates ship with
  ``REPLACE_WITH_...`` / ``changeme`` markers. Booting with those left in is
  worse than refusing to start — the server would auth-fail in a confusing way.
* **PID-tagged logging.** Multiple processes sharing one log file corrupt
  each other's rotations (``RotatingFileHandler`` is per-process). The PID is
  in the filename so concurrent runs coexist, and a 7-day sweep keeps the
  directory from growing without bound.
* **Parent watchdog.** When the parent process (usually the MCP host) dies,
  the kernel reparents us to PID 1 but does NOT kill us. Without an explicit
  ``ppid`` poll we leak ghost servers. ``os._exit(0)`` is intentional on that
  path: the watchdog runs on a daemon thread, so graceful cleanup is not an
  option.
* **Logout on exit.** A360 has a hard 5-session cap per account. We register
  ``AuthManager.logout()`` on ``atexit`` AND call it from the watchdog before
  ``os._exit``, so the slot is released on both the graceful path and the
  parent-died path.
* **BrokenPipe is normal exit.** When the client hangs up the stdio pipes,
  we get ``BrokenPipeError`` / ``ConnectionResetError`` (possibly wrapped in
  an ``ExceptionGroup``). Treating that as a failure would log a scary
  traceback for a routine shutdown.

Transport is stdio by default. ``streamable-http`` is wired through argparse
so configs can request it, but the runner is a stub until the optional
``uvicorn`` + ``starlette`` deps land — the spec calls it an "option", so
deferring is acceptable for Phase 8.
"""

from __future__ import annotations

import argparse
import atexit
import logging
import os
import re
import sys
import threading
import time
from collections.abc import Callable, Iterable
from logging.handlers import RotatingFileHandler
from pathlib import Path
from urllib.parse import urlparse

import anyio
from pydantic import SecretStr

from .auth.auth_manager import AuthManager
from .server import A360MCPServer
from .utils.config import A360AuthConfig, ServerConfig
from .version import __version__

logger = logging.getLogger(__name__)

# --------------------------------------------------------------------------- #
# Environment variable names
# --------------------------------------------------------------------------- #
# All A360-prefixed so a shared shell env does not collide with other tools.
ENV_CONTROL_ROOM_URL = "A360_CONTROL_ROOM_URL"
ENV_USERNAME = "A360_USERNAME"
ENV_API_KEY = "A360_API_KEY"
ENV_PASSWORD = "A360_PASSWORD"
ENV_MULTIPLE_LOGIN = "A360_MULTIPLE_LOGIN"
ENV_ALLOW_WRITES = "A360_ALLOW_WRITES"
ENV_REQUEST_TIMEOUT = "A360_REQUEST_TIMEOUT"
ENV_CA_BUNDLE = "A360_CA_BUNDLE"
ENV_TLS_VERIFY = "A360_TLS_VERIFY"
ENV_TOOL_PACKAGE = "A360_TOOL_PACKAGE"
ENV_LOG_FILE = "A360_LOG_FILE"
ENV_LOG_LEVEL = "A360_LOG_LEVEL"
ENV_TRANSPORT = "A360_TRANSPORT"
ENV_HTTP_HOST = "A360_HTTP_HOST"
ENV_HTTP_PORT = "A360_HTTP_PORT"
ENV_HTTP_PATH = "A360_HTTP_PATH"

# --------------------------------------------------------------------------- #
# Defaults and constants
# --------------------------------------------------------------------------- #
DEFAULT_HTTP_HOST = "127.0.0.1"
DEFAULT_HTTP_PORT = 8080
DEFAULT_HTTP_PATH = "/mcp"

_LOG_RETENTION_DAYS = 7
_LOG_MAX_BYTES = 10_000_000
_LOG_BACKUP_COUNT = 3
_WATCHDOG_POLL_SECONDS = 5

# Full-value ${VAR} reference only. Anchored at both ends so partial refs
# (``${VAULT}_prod``) are rejected by ``has_env_reference`` rather than
# silently becoming a literal string.
_ENV_REF_PATTERN = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")

# Markers that template authors leave behind when they forget to fill in a
# value. Booting with one of these is worse than failing to start — the auth
# flow would 401 in a confusing way. Match case-insensitively on substrings.
_PLACEHOLDER_MARKERS = ("replace_with", "your_username", "your_password", "changeme")

# Pattern used by ``_sweep_stale_logs`` to recognise our process log files.
# The slug in the middle is the control-room host, the PID is in the suffix.
_LOG_FILE_PATTERN_TEMPLATE = "a360-mcp_{slug}.{pid}.log"

_TRANSPORT_STDIO = "stdio"
_TRANSPORT_HTTP = "streamable-http"
_VALID_TRANSPORTS = (_TRANSPORT_STDIO, _TRANSPORT_HTTP)


# --------------------------------------------------------------------------- #
# ${ENV} expansion + placeholder detection
# --------------------------------------------------------------------------- #


def has_env_reference(value: str | None) -> bool:
    """True when ``value`` textually contains a ``${...}`` marker.

    Callers use this to refuse *partial* references: the resolver only handles
    the full-value form, so a partial ref would otherwise slip through as the
    literal string.
    """
    return isinstance(value, str) and "${" in value


def resolve_env_reference(value: str | None) -> str | None:
    """Resolve a full-value ``${VAR}`` reference. None/empty passthrough.

    Returns None when:
    * the env var is unset (so callers can fall back to another source)
    * the resolved value is self-referential (``A360_FOO=${A360_FOO}`` would
      loop forever; we treat that the same as unset).
    Partial references (``${VAR}_x``) are returned unchanged so the caller can
    decide; ``has_env_reference`` is the canonical way to detect them.
    """
    if value is None or value == "":
        return value
    if not isinstance(value, str):
        return value
    match = _ENV_REF_PATTERN.match(value.strip())
    if not match:
        return value
    var_name = match.group(1)
    resolved = os.environ.get(var_name)
    if resolved is None:
        return None
    if resolved.strip() == value.strip():
        # Self-reference: would loop. Treat as unset.
        return None
    return resolved


def looks_like_unfilled_placeholder(value: str | None) -> bool:
    """True when ``value`` looks like a template author forgot to fill in."""
    if not isinstance(value, str) or not value:
        return False
    lowered = value.lower()
    return any(marker in lowered for marker in _PLACEHOLDER_MARKERS)


def _as_secret(value: str | None) -> SecretStr | None:
    """Wrap a resolved credential so it stops rendering in reprs and logs."""
    return SecretStr(value) if value is not None else None


def _pick_first_resolved(*values: str | None) -> str | None:
    """Return the first value that resolves to a non-empty, non-placeholder string.

    Resolution order is the caller's responsibility — this helper just walks
    the list left-to-right and applies ``${ENV}`` expansion + placeholder
    rejection uniformly.
    """
    for raw in values:
        if raw is None:
            continue
        if has_env_reference(raw):
            # Full ref: resolve. Partial ref: refuse loudly so the user knows.
            if not _ENV_REF_PATTERN.match(raw.strip()):
                raise ValueError(
                    f"Partial ${{ENV}} reference is not supported (got {raw!r}). "
                    "Use a full ${{VAR}} value or a literal."
                )
            resolved = resolve_env_reference(raw)
            if resolved is None or resolved == "":
                continue
            if looks_like_unfilled_placeholder(resolved):
                raise ValueError(f"Environment variable resolved to a placeholder: {resolved!r}")
            return resolved
        stripped = raw.strip() if isinstance(raw, str) else raw
        if stripped == "" or stripped is None:
            continue
        if looks_like_unfilled_placeholder(stripped):
            raise ValueError(f"Value looks like an unfilled template placeholder: {stripped!r}")
        return stripped
    return None


def _env_bool(value: str | None, *, default: bool = False) -> bool:
    """Parse a boolean from an env string. Empty → default."""
    if value is None or value == "":
        return default
    return value.strip().lower() in ("1", "true", "yes", "on")


def _env_int(value: str | None, *, default: int) -> int:
    """Parse an int from an env string. Empty/invalid → default."""
    if value is None or value == "":
        return default
    try:
        return int(value.strip())
    except (TypeError, ValueError):
        return default


# --------------------------------------------------------------------------- #
# Logging
# --------------------------------------------------------------------------- #


def _control_room_host_slug(url: str | None) -> str:
    """Derive a filesystem-safe slug from the Control Room URL host.

    Used to namespace per-process log files so concurrent servers (different
    CRs, or the same CR run with different packages) don't rotate each other.
    """
    if not url:
        return "default"
    try:
        host = urlparse(url).hostname or ""
    except ValueError:
        host = ""
    slug = re.sub(r"[^A-Za-z0-9.-]", "-", host).strip("-")
    return slug or "default"


def _resolve_log_file(log_file_env: str | None) -> Path | None:
    """Resolve ``A360_LOG_FILE`` into a concrete path.

    * Empty/unset → None (stderr only).
    * Existing file path → used directly.
    * Directory path → ``a360-mcp_{slug}.{pid}.log`` inside it. The PID is in
      the filename so concurrent processes don't fight over rotations.
    """
    if not log_file_env or not log_file_env.strip():
        return None
    raw = log_file_env.strip()
    path = Path(raw)
    if path.is_dir() or raw.endswith(os.sep):
        slug = _control_room_host_slug(os.environ.get(ENV_CONTROL_ROOM_URL))
        name = _LOG_FILE_PATTERN_TEMPLATE.format(slug=slug, pid=os.getpid())
        return path / name
    return path


def _sweep_stale_logs(log_dir: Path, *, slug: str) -> int:
    """Delete our process log files older than ``_LOG_RETENTION_DAYS``.

    Returns the count removed. Best-effort: OSError on a single file is logged
    and skipped so one unreadable file doesn't abort the sweep.
    """
    if not log_dir.is_dir():
        return 0
    cutoff = time.time() - _LOG_RETENTION_DAYS * 86400
    prefix = f"a360-mcp_{slug}."
    removed = 0
    for entry in log_dir.iterdir():
        try:
            if not entry.name.startswith(prefix):
                continue
            if not entry.is_file():
                continue
            if entry.stat().st_mtime < cutoff:
                entry.unlink(missing_ok=True)
                removed += 1
        except OSError as exc:
            logger.warning("Could not sweep stale log %s: %s", entry, exc)
    return removed


def configure_logging(*, force: bool = False) -> None:
    """Configure root logging. Idempotent unless ``force=True``.

    Opt-in file logging via ``A360_LOG_FILE``. When set to a directory, the
    filename embeds the control-room slug and PID (see ``_resolve_log_file``)
    so concurrent servers do not corrupt each other's rotations.
    """
    root = logging.getLogger()
    if root.handlers and not force:
        return
    if force:
        for handler in list(root.handlers):
            root.removeHandler(handler)

    level_name = (os.environ.get(ENV_LOG_LEVEL) or "INFO").upper()
    level = getattr(logging, level_name, logging.INFO)
    root.setLevel(level)

    formatter = logging.Formatter(
        fmt="%(asctime)s - pid=%(process)d - %(name)s - %(levelname)s - %(message)s"
    )

    stderr_handler = logging.StreamHandler(stream=sys.stderr)
    stderr_handler.setFormatter(formatter)
    root.addHandler(stderr_handler)

    try:
        log_file = _resolve_log_file(os.environ.get(ENV_LOG_FILE))
    except OSError as exc:
        logger.warning("Could not resolve A360_LOG_FILE (%s); stderr-only logging.", exc)
        return

    if log_file is None:
        return

    try:
        log_file.parent.mkdir(parents=True, exist_ok=True)
        handler = RotatingFileHandler(
            log_file,
            maxBytes=_LOG_MAX_BYTES,
            backupCount=_LOG_BACKUP_COUNT,
            encoding="utf-8",
        )
        handler.setFormatter(formatter)
        root.addHandler(handler)
        _sweep_stale_logs(
            log_file.parent, slug=_control_room_host_slug(os.environ.get(ENV_CONTROL_ROOM_URL))
        )
        logger.info("File logging enabled at %s", log_file)
    except OSError as exc:
        # File logging is opt-in; never block startup over it.
        logger.warning("Could not attach file handler at %s: %s", log_file, exc)


# --------------------------------------------------------------------------- #
# BrokenPipe handling
# --------------------------------------------------------------------------- #


def _is_broken_pipe(exc: BaseException) -> bool:
    """True for BrokenPipeError or ConnectionResetError leaves."""
    return isinstance(exc, (BrokenPipeError, ConnectionResetError))


def _is_broken_pipe_group(exc: BaseException) -> bool:
    """True when ``exc`` is (or wraps only) broken-pipe leaves.

    A mixed ExceptionGroup is NOT a clean shutdown — some other error is
    hiding in there and we want the real traceback.
    """
    if _is_broken_pipe(exc):
        return True
    if isinstance(exc, BaseExceptionGroup):
        leaves = exc.exceptions
        return bool(leaves) and all(_is_broken_pipe_group(leaf) for leaf in leaves)
    return False


# --------------------------------------------------------------------------- #
# Parent watchdog
# --------------------------------------------------------------------------- #


def _start_parent_watchdog(
    *,
    on_parent_gone: Callable[[], None] | None = None,
    poll_seconds: int = _WATCHDOG_POLL_SECONDS,
) -> threading.Thread | None:
    """Start a daemon thread that exits the process when its parent dies.

    On Linux/macOS, an orphaned process is reparented to PID 1 but NOT killed.
    Without this watchdog a crashed MCP host leaks a ghost server that holds
    an A360 session slot until the token TTL (20min) expires.

    ``on_parent_gone`` is the callback invoked before the hard exit. The
    default uses ``os._exit(0)`` because the watchdog runs on a daemon thread
    — there is no graceful path. Tests override it to assert behaviour without
    exiting the test runner.
    """
    parent_pid = os.getppid()
    if parent_pid <= 1:
        # Already detached (init/launchd is our parent). Nothing to watch.
        return None

    def _watch() -> None:
        while True:
            time.sleep(poll_seconds)
            current = os.getppid()
            if current != parent_pid or current <= 1:
                logger.info(
                    "Parent process %d is gone (now %d); shutting down watchdog.",
                    parent_pid,
                    current,
                )
                if on_parent_gone is not None:
                    try:
                        on_parent_gone()
                    except BaseException:  # noqa: BLE001 - never raise from watchdog
                        logger.exception("Watchdog shutdown callback raised; forcing exit.")
                os._exit(0)
                # Defensive: ``os._exit`` should never return. In tests it is
                # stubbed, so ``return`` here ensures the daemon thread does
                # not outlive the test (a real ``os._exit`` would have killed
                # the whole process).
                return

    thread = threading.Thread(target=_watch, name="parent-watchdog", daemon=True)
    thread.start()
    return thread


def _shutdown(auth_manager: AuthManager | None) -> None:
    """Best-effort ``logout`` for use via ``atexit`` and the watchdog callback.

    A360 enforces a 5-session cap per account. Without an explicit logout on
    exit we leak slots until the token TTL (20min) expires — three quick
    restarts in a row would lock the account out.
    """
    if auth_manager is None:
        return
    try:
        auth_manager.logout()
    except BaseException as exc:  # noqa: BLE001 - shutdown must never raise
        logger.warning("Logout during shutdown failed: %s", exc)


# --------------------------------------------------------------------------- #
# Argument parsing
# --------------------------------------------------------------------------- #


def _build_parser() -> argparse.ArgumentParser:
    """Build the CLI parser. Defaults read env first (2nd-tier fallback)."""
    parser = argparse.ArgumentParser(
        prog="a360-mcp",
        description="A360 Control Room diagnosis MCP server.",
    )
    parser.add_argument("--version", action="version", version=f"a360-mcp {__version__}")

    server_group = parser.add_argument_group("Server configuration")
    server_group.add_argument(
        "--control-room-url",
        default=os.environ.get(ENV_CONTROL_ROOM_URL),
        help="Control Room base URL (env A360_CONTROL_ROOM_URL).",
    )
    server_group.add_argument(
        "--username",
        default=os.environ.get(ENV_USERNAME),
        help="Control Room username (env A360_USERNAME).",
    )
    server_group.add_argument(
        "--api-key",
        default=os.environ.get(ENV_API_KEY),
        help="API key from My Settings (env A360_API_KEY). Mutually exclusive with --password.",
    )
    server_group.add_argument(
        "--password",
        default=os.environ.get(ENV_PASSWORD),
        help="Password (env A360_PASSWORD). Prefer --api-key.",
    )
    server_group.add_argument(
        "--multiple-login",
        action="store_true",
        default=_env_bool(os.environ.get(ENV_MULTIPLE_LOGIN)),
        help="Allow concurrent sessions for this account (env A360_MULTIPLE_LOGIN).",
    )
    server_group.add_argument(
        "--allow-writes",
        action="store_true",
        default=_env_bool(os.environ.get(ENV_ALLOW_WRITES)),
        help="Opt-in to mutating tools (env A360_ALLOW_WRITES). Default off.",
    )
    server_group.add_argument(
        "--request-timeout",
        type=int,
        default=_env_int(os.environ.get(ENV_REQUEST_TIMEOUT), default=60),
        help="HTTP request timeout in seconds (env A360_REQUEST_TIMEOUT).",
    )
    server_group.add_argument(
        "--ca-bundle",
        default=os.environ.get(ENV_CA_BUNDLE),
        help="Path to CA bundle for private/self-signed certs (env A360_CA_BUNDLE).",
    )
    server_group.add_argument(
        "--tls-verify",
        dest="tls_verify",
        action="store_true",
        default=_env_bool(os.environ.get(ENV_TLS_VERIFY), default=True),
        help="Verify TLS certificates (env A360_TLS_VERIFY). Default on.",
    )
    server_group.add_argument(
        "--no-tls-verify",
        dest="tls_verify",
        action="store_false",
        help="Disable TLS verification. Insecure; use only for local testing.",
    )

    pkg_group = parser.add_argument_group("Tool package")
    pkg_group.add_argument(
        "--tool-package",
        default=os.environ.get(ENV_TOOL_PACKAGE),
        help=(
            "Which tool package to advertise: core, bot_read, log_read, "
            "diagnose, full (env A360_TOOL_PACKAGE)."
        ),
    )

    transport_group = parser.add_argument_group("Transport")
    transport_group.add_argument(
        "--transport",
        choices=_VALID_TRANSPORTS,
        default=os.environ.get(ENV_TRANSPORT) or _TRANSPORT_STDIO,
        help="MCP transport (env A360_TRANSPORT). Default stdio.",
    )
    transport_group.add_argument(
        "--http-host",
        default=os.environ.get(ENV_HTTP_HOST) or DEFAULT_HTTP_HOST,
        help=f"Host for streamable-http transport (env A360_HTTP_HOST). Default {DEFAULT_HTTP_HOST}.",
    )
    transport_group.add_argument(
        "--http-port",
        type=int,
        default=_env_int(os.environ.get(ENV_HTTP_PORT), default=DEFAULT_HTTP_PORT),
        help=f"Port for streamable-http transport (env A360_HTTP_PORT). Default {DEFAULT_HTTP_PORT}.",
    )
    transport_group.add_argument(
        "--http-path",
        default=os.environ.get(ENV_HTTP_PATH) or DEFAULT_HTTP_PATH,
        help=f"Path for streamable-http transport (env A360_HTTP_PATH). Default {DEFAULT_HTTP_PATH}.",
    )

    parser.add_argument(
        "--debug",
        action="store_true",
        default=False,
        help="Enable DEBUG-level logging on the root logger.",
    )

    return parser


def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace:
    """Parse CLI args. argparse applies env defaults first; CLI overrides them."""
    parser = _build_parser()
    return parser.parse_args(args=list(argv) if argv is not None else None)


# --------------------------------------------------------------------------- #
# Config construction (3-tier precedence with ${ENV} expansion + fail-fast)
# --------------------------------------------------------------------------- #


def create_config(args: argparse.Namespace) -> ServerConfig:
    """Build a ``ServerConfig`` from CLI args + env, applying 3-tier precedence.

    Tier 1: explicit CLI flag (highest).
    Tier 2: environment variable, with full ``${VAR}`` expansion.
    Tier 3: built-in default (lowest).

    Fail-fast on:
    * unfilled placeholders (``REPLACE_WITH_...``, ``changeme``)
    * partial ``${ENV}`` references (``${VAULT}_prod``)
    * missing required values (control_room_url, username, exactly one secret)
    """
    control_room_url = _pick_first_resolved(
        getattr(args, "control_room_url", None),
        os.environ.get(ENV_CONTROL_ROOM_URL),
    )
    if control_room_url is None:
        raise ValueError(
            f"Control Room URL is required (CLI --control-room-url or env {ENV_CONTROL_ROOM_URL})."
        )

    username = _pick_first_resolved(
        getattr(args, "username", None),
        os.environ.get(ENV_USERNAME),
    )
    if username is None:
        raise ValueError(f"Username is required (CLI --username or env {ENV_USERNAME}).")

    api_key = _pick_first_resolved(
        getattr(args, "api_key", None),
        os.environ.get(ENV_API_KEY),
    )
    password = _pick_first_resolved(
        getattr(args, "password", None),
        os.environ.get(ENV_PASSWORD),
    )

    auth = A360AuthConfig(
        username=username,
        api_key=_as_secret(api_key),
        password=_as_secret(password),
        multiple_login=bool(getattr(args, "multiple_login", False)),
    )

    request_timeout = int(getattr(args, "request_timeout", 60) or 60)
    if request_timeout <= 0:
        raise ValueError(f"request_timeout must be positive, got {request_timeout}")

    ca_bundle = _pick_first_resolved(
        getattr(args, "ca_bundle", None),
        os.environ.get(ENV_CA_BUNDLE),
    )

    return ServerConfig(
        control_room_url=control_room_url,
        auth=auth,
        allow_writes=bool(getattr(args, "allow_writes", False)),
        request_timeout=request_timeout,
        ca_bundle=ca_bundle,
        tls_verify=bool(getattr(args, "tls_verify", True)),
    )


# --------------------------------------------------------------------------- #
# Server runners
# --------------------------------------------------------------------------- #


async def arun_server(server: A360MCPServer) -> None:
    """Run ``server`` over stdio. The CLI registers shutdown hooks separately."""
    await server.run_stdio()


async def arun_http_server(server: A360MCPServer, args: argparse.Namespace) -> None:
    """Run ``server`` over streamable-http.

    Phase 8 ships the stdio path fully. The HTTP transport needs
    ``uvicorn`` + ``starlette`` (optional deps not yet declared); accept the
    arg so configs can request it, but fail loudly with an actionable error.
    """
    try:
        import uvicorn  # noqa: F401
        from starlette.applications import Starlette  # noqa: F401
    except ImportError as exc:
        raise NotImplementedError(
            "streamable-http transport requires 'uvicorn' and 'starlette'. "
            "Install a360-mcp[http] (or pip install uvicorn starlette) and retry. "
            f"Original error: {exc}"
        ) from exc
    # When deps land, wire the MCP streamable-http app on (host, port, path).
    # Until then this branch is unreachable in tests.
    raise NotImplementedError("streamable-http transport is not yet implemented.")


# --------------------------------------------------------------------------- #
# Entry point
# --------------------------------------------------------------------------- #


def main(argv: Iterable[str] | None = None) -> int:
    """CLI entry point. Returns an exit code; never raises BrokenPipe.

    Order matters:
    1. Configure logging (force) so later steps have a working logger.
    2. Parse args.
    3. Bump root log level if --debug.
    4. Propagate --tool-package to env so ``A360MCPServer`` picks it up.
    5. Build config (fail-fast on placeholders/missing).
    6. Construct server + register atexit logout.
    7. Start the parent watchdog.
    8. Run the transport. BrokenPipe/ConnectionReset → exit 0.
    """
    configure_logging(force=True)

    args = parse_args(argv)

    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)

    logger.info("Starting a360-mcp %s", __version__)

    if args.tool_package:
        os.environ[ENV_TOOL_PACKAGE] = args.tool_package

    try:
        config = create_config(args)
    except ValueError as exc:
        logger.error("Configuration error: %s", exc)
        return 1

    server = A360MCPServer(config)
    atexit.register(_shutdown, server.auth_manager)
    _start_parent_watchdog(on_parent_gone=lambda: _shutdown(server.auth_manager))

    transport = args.transport
    try:
        if transport == _TRANSPORT_HTTP:
            anyio.run(arun_http_server, server, args)
        else:
            anyio.run(arun_server, server)
    except (BrokenPipeError, ConnectionResetError) as exc:
        # Routine shutdown: the client hung up the pipes. Log at INFO so the
        # default log level does not look like a crash.
        logger.info("Client closed the connection (%s); exiting.", type(exc).__name__)
        return 0
    except BaseException as exc:  # noqa: BLE001 - boundary converts to exit code
        if _is_broken_pipe_group(exc):
            logger.info("Client closed the connection; exiting.")
            return 0
        logger.exception("Server crashed.")
        return 1

    return 0


__all__ = [
    "ENV_ALLOW_WRITES",
    "ENV_API_KEY",
    "ENV_CA_BUNDLE",
    "ENV_CONTROL_ROOM_URL",
    "ENV_HTTP_HOST",
    "ENV_HTTP_PATH",
    "ENV_HTTP_PORT",
    "ENV_LOG_FILE",
    "ENV_LOG_LEVEL",
    "ENV_MULTIPLE_LOGIN",
    "ENV_PASSWORD",
    "ENV_REQUEST_TIMEOUT",
    "ENV_TLS_VERIFY",
    "ENV_TOOL_PACKAGE",
    "ENV_TRANSPORT",
    "ENV_USERNAME",
    "arun_http_server",
    "arun_server",
    "configure_logging",
    "create_config",
    "has_env_reference",
    "looks_like_unfilled_placeholder",
    "main",
    "parse_args",
    "resolve_env_reference",
]
