"""
Standalone Prometheus metrics exporter for asgi-runway.

Provides two ways to decouple metric scraping from the application's event
loop, so Prometheus can always scrape even when the app is under load.

Embedded mode (single container — plain Docker, EC2, bare metal)
----------------------------------------------------------------
Start a background thread that serves metrics on a separate port.
The thread is fully isolated from the uvicorn asyncio event loop::

    from asgi_runway import setup
    setup(app, metrics_port=9091)

    # or manually:
    from asgi_runway.exporter import start_metrics_server
    start_metrics_server(port=9091)

Sidecar mode (Kubernetes / Docker Compose)
------------------------------------------
Run this module as a separate process alongside the app container.
Both must share ``PROMETHEUS_MULTIPROC_DIR`` as a mounted volume::

    PROMETHEUS_MULTIPROC_DIR=/tmp/prom python -m asgi_runway.exporter --port 9091

See the README for Docker Compose and Kubernetes manifest examples.
"""

import errno
import sys
import threading
from http.server import ThreadingHTTPServer
from typing import Optional

from prometheus_client import MetricsHandler

from asgi_runway._metrics import _MULTIPROCESS, _make_registry

# Singleton: prevents double-bind when the same process calls setup() twice
# (e.g. during hot-reload). Does not help across worker processes — that is
# handled by catching EADDRINUSE below.
_server: Optional[ThreadingHTTPServer] = None
_server_lock = threading.Lock()


def start_metrics_server(port: int = 9091, addr: str = "") -> Optional[ThreadingHTTPServer]:
    """
    Start a Prometheus metrics server in a background daemon thread.

    The server runs in its own OS thread, completely independent of the uvicorn
    asyncio event loop. It stays responsive even when the application is
    saturated with requests.

    Works in both single-process and multiprocess (gunicorn + uvicorn workers)
    deployments. When ``PROMETHEUS_MULTIPROC_DIR`` is set, metrics are
    automatically aggregated across all worker files.

    Parameters
    ----------
    port:
        Port to listen on. Prometheus convention is ``9091`` for sidecar
        exporters. Pick any free port that does not conflict with the app.
    addr:
        Address to bind to. Defaults to all interfaces (equivalent to
        ``0.0.0.0``).

    Returns
    -------
    ThreadingHTTPServer
        The running server instance. Call ``.shutdown()`` to stop it cleanly
        (e.g. in a lifespan handler).

    Example::

        from asgi_runway.exporter import start_metrics_server

        server = start_metrics_server(port=9091)
        # Runs as a daemon thread — no further action required.
        # To stop cleanly: server.shutdown()
    """
    global _server
    with _server_lock:
        if _server is not None:
            return _server  # already running in this process

        registry = _make_registry()
        handler = MetricsHandler.factory(registry)
        try:
            httpd = ThreadingHTTPServer((addr, port), handler)
        except OSError as exc:
            if exc.errno == errno.EADDRINUSE:
                # Another worker process already bound this port — normal in
                # multi-worker deployments. The metrics server is running; this
                # worker just doesn't own it.
                import warnings
                warnings.warn(
                    f"runway-exporter: port {port} already in use — another "
                    "worker has started the metrics server. For multi-worker "
                    "deployments, the sidecar pattern (python -m "
                    "asgi_runway.exporter) avoids this race entirely.",
                    RuntimeWarning,
                    stacklevel=2,
                )
                return None
            raise

        thread = threading.Thread(
            target=httpd.serve_forever,
            name="runway-metrics-server",
            daemon=True,
        )
        thread.start()
        _server = httpd
        return httpd


def serve_forever(port: int = 9091, addr: str = "") -> None:
    """
    Start a blocking Prometheus metrics server.

    Intended for use as a sidecar process (``python -m asgi_runway.exporter``).
    Blocks until interrupted with SIGINT / Ctrl-C.

    Requires ``PROMETHEUS_MULTIPROC_DIR`` to be set so the sidecar can read
    metric files written by the application workers. For single-container
    setups (no shared dir), use :func:`start_metrics_server` instead.

    Parameters
    ----------
    port:
        Port to listen on.
    addr:
        Address to bind to.
    """
    if not _MULTIPROCESS:
        print(
            "ERROR: PROMETHEUS_MULTIPROC_DIR is not set.\n"
            "\n"
            "The sidecar exporter reads metrics from files written to this shared\n"
            "directory by the application process(es). Both the app container and\n"
            "this exporter must mount the same volume at PROMETHEUS_MULTIPROC_DIR.\n"
            "\n"
            "For single-container setups, use the embedded server instead:\n"
            "    setup(app, metrics_port=9091)\n"
            "    # or: from asgi_runway.exporter import start_metrics_server",
            file=sys.stderr,
        )
        sys.exit(1)

    registry = _make_registry()
    handler = MetricsHandler.factory(registry)

    bind = addr or "0.0.0.0"
    print(f"runway-exporter: serving on http://{bind}:{port}/metrics", flush=True)

    with ThreadingHTTPServer((addr, port), handler) as httpd:
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print("\nrunway-exporter: shutting down", flush=True)


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        prog="python -m asgi_runway.exporter",
        description=(
            "Prometheus metrics sidecar for asgi-runway.\n\n"
            "Reads metrics from PROMETHEUS_MULTIPROC_DIR and exposes them on a\n"
            "dedicated port, fully decoupled from the application uvicorn process."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
requirements:
  PROMETHEUS_MULTIPROC_DIR must be set and mounted as a shared volume between
  the app container and this exporter container.

Docker Compose example:
  services:
    app:
      environment:
        PROMETHEUS_MULTIPROC_DIR: /tmp/prom
      volumes: [prom_data:/tmp/prom]
    runway-exporter:
      command: python -m asgi_runway.exporter --port 9091
      environment:
        PROMETHEUS_MULTIPROC_DIR: /tmp/prom
      volumes: [prom_data:/tmp/prom]
  volumes:
    prom_data:

For single-container setups (no shared volume), use the embedded server:
  setup(app, metrics_port=9091)
""",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=9091,
        help="Port to listen on (default: 9091).",
    )
    parser.add_argument(
        "--addr",
        default="",
        help="Address to bind to (default: all interfaces).",
    )

    args = parser.parse_args()
    serve_forever(port=args.port, addr=args.addr)
