"""
RBSP HTTP API Server.

Provides a REST/JSON interface to the RBSP engine using only Python
stdlib.  Designed for local development and lightweight deployment.

Features:
    - ThreadPoolExecutor with bounded concurrency and in-flight tracking
    - HTTP/1.1 keep-alive with configurable socket timeout
    - Throttle integration (429 / 503) with ``Retry-After`` headers
    - Metrics integration (counters, histograms, gauges)
    - SlowQueryLog for queries exceeding a latency threshold
    - CORS headers and optional API-key authentication
    - Graceful shutdown that drains in-flight requests

Usage::

    from rbsp.runtime.server import RBSPServer
    server = RBSPServer(host="127.0.0.1", port=7342)
    server.serve_forever()

Endpoints::

    GET  /health                 Liveness probe
    GET  /api/v1/status          Engine status and statistics
    POST /api/v1/resolve         Full query (JSON body)
    GET  /api/v1/search?q=...    Query via query parameters
    POST /api/v1/index           Trigger indexing
    POST /api/v1/init            Initialize the engine
    POST /api/v1/watcher/start   Start file watcher
    POST /api/v1/watcher/stop    Stop file watcher
    GET  /api/v1/watcher         Watcher status
"""

from __future__ import annotations

import json
import signal
import socket
import threading
import time
import traceback
from concurrent.futures import ThreadPoolExecutor
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.parse import urlparse, parse_qs

from rbsp.runtime.engine import RBSPEngine, Result, IndexStats
from rbsp.runtime.monitoring import Metrics, SlowQueryLog
from rbsp.runtime.throttle import Throttle


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_DEFAULT_HOST = "127.0.0.1"
_DEFAULT_PORT = 7342
_MAX_BODY_SIZE = 4 * 1024 * 1024  # 4 MiB
_DEFAULT_WORKERS = 8
_DEFAULT_QUEUE_SIZE = 64
_DEFAULT_SOCKET_TIMEOUT = 30  # seconds


# ---------------------------------------------------------------------------
# JSON Helpers
# ---------------------------------------------------------------------------

def _json_response(handler: "RBSPHandler", status: int, data: Any) -> None:
    """Write a JSON response with proper headers."""
    body = json.dumps(data, default=str).encode("utf-8")
    handler.send_response(status)
    handler.send_header("Content-Type", "application/json; charset=utf-8")
    handler.send_header("Content-Length", str(len(body)))
    _add_cors_headers(handler)
    handler.end_headers()
    handler.wfile.write(body)


def _error_response(
    handler: "RBSPHandler",
    status: int,
    message: str,
    retry_after: Optional[int] = None,
) -> None:
    """Write a JSON error response with optional ``Retry-After`` header."""
    body = json.dumps({"error": message, "status": status}, default=str).encode("utf-8")
    handler.send_response(status)
    handler.send_header("Content-Type", "application/json; charset=utf-8")
    handler.send_header("Content-Length", str(len(body)))
    if retry_after is not None:
        handler.send_header("Retry-After", str(retry_after))
    _add_cors_headers(handler)
    handler.end_headers()
    handler.wfile.write(body)


def _read_json_body(handler: "RBSPHandler") -> Optional[Dict[str, Any]]:
    """Read and parse a JSON request body.

    Returns the parsed dict, an empty dict for zero-length bodies,
    or ``None`` when an error response has already been sent.
    """
    content_length = int(handler.headers.get("Content-Length", 0))
    if content_length <= 0:
        return {}
    if content_length > _MAX_BODY_SIZE:
        _error_response(handler, 413, "Request body too large")
        return None
    raw = handler.rfile.read(content_length)
    try:
        data = json.loads(raw.decode("utf-8"))
        if not isinstance(data, dict):
            _error_response(handler, 400, "Request body must be a JSON object")
            return None
        return data
    except (json.JSONDecodeError, UnicodeDecodeError) as exc:
        _error_response(handler, 400, f"Invalid JSON: {exc}")
        return None


def _add_cors_headers(handler: "RBSPHandler") -> None:
    """Add CORS headers for browser access."""
    handler.send_header("Access-Control-Allow-Origin", "*")
    handler.send_header(
        "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"
    )
    handler.send_header("Access-Control-Allow-Headers", "Content-Type, X-API-Key")


# ---------------------------------------------------------------------------
# Result serialization
# ---------------------------------------------------------------------------

def _serialize_result(r: Result) -> Dict[str, Any]:
    """Convert a Result dataclass to a JSON-safe dict."""
    return {
        "path": r.path,
        "score": round(r.score, 6),
        "snippet": r.snippet,
        "line": r.line,
        "metadata": r.metadata,
    }


def _serialize_index_stats(s: IndexStats) -> Dict[str, Any]:
    """Convert IndexStats to a JSON-safe dict."""
    return {
        "files_indexed": s.files_indexed,
        "files_skipped": s.files_skipped,
        "total_size": s.total_size,
        "duration": round(s.duration, 4),
        "index_path": s.index_path,
    }


# ---------------------------------------------------------------------------
# Route handlers
# ---------------------------------------------------------------------------

def _handle_health(handler: "RBSPHandler", _params: Dict) -> None:
    """GET /health - Liveness check."""
    _json_response(handler, 200, {"status": "ok", "timestamp": time.time()})


def _handle_status(handler: "RBSPHandler", _params: Dict) -> None:
    """GET /api/v1/status - Engine status, metrics, and throttle stats."""
    engine = RBSPEngine.get_instance()
    status_data = engine.status()

    # Enrich with metrics and throttle info from the server
    metrics = getattr(handler.server, "rbsp_metrics", None)
    if metrics is not None:
        status_data["metrics"] = metrics.snapshot()

    throttle = getattr(handler.server, "rbsp_throttle", None)
    if throttle is not None:
        status_data["throttle"] = throttle.stats()

    httpd = handler.server
    in_flight_count = getattr(httpd, "_in_flight_count", 0)
    status_data["http_connections_active"] = in_flight_count

    _json_response(handler, 200, status_data)


def _handle_resolve(handler: "RBSPHandler", _params: Dict) -> None:
    """POST /api/v1/resolve - Execute a search query.

    Request body::

        {"query": "...", "scope": ".", "max_results": 50, "threshold": 0.35}
    """
    body = _read_json_body(handler)
    if body is None:
        return

    query = body.get("query", "")
    if not query or not isinstance(query, str):
        _error_response(handler, 400, "Missing or invalid 'query' field")
        return

    kwargs: Dict[str, Any] = {}
    scope = body.get("scope", ".")
    if "max_results" in body:
        try:
            kwargs["max_results"] = int(body["max_results"])
        except (TypeError, ValueError):
            _error_response(handler, 400, "'max_results' must be an integer")
            return
    if "threshold" in body:
        try:
            kwargs["threshold"] = float(body["threshold"])
        except (TypeError, ValueError):
            _error_response(handler, 400, "'threshold' must be a number")
            return

    # Throttle check
    throttle = getattr(handler.server, "rbsp_throttle", None)
    metrics = getattr(handler.server, "rbsp_metrics", None)
    if throttle is not None:
        if throttle._query_bp.should_shed_load():
            if metrics is not None:
                metrics.counter("http.503_shed")
            _error_response(handler, 503, "Service overloaded", retry_after=1)
            return
        if not throttle.allow_query():
            if metrics is not None:
                metrics.counter("http.429_throttled")
            _error_response(handler, 429, "Too many requests", retry_after=1)
            return

    if metrics is not None:
        metrics.counter("http.requests.query")

    engine = RBSPEngine.get_instance()
    if not engine.is_initialized:
        engine.init(scope)

    start_ms = time.monotonic()
    results = engine.resolve(query, scope=scope, **kwargs)
    elapsed_ms = (time.monotonic() - start_ms) * 1000.0

    if metrics is not None:
        metrics.histogram("http.query.latency_ms", elapsed_ms)

    if throttle is not None:
        throttle.release_query(elapsed_ms)

    slow_log = getattr(handler.server, "rbsp_slow_log", None)
    if slow_log is not None:
        slow_log.log(query, elapsed_ms, len(results))

    _json_response(handler, 200, {
        "query": query,
        "count": len(results),
        "results": [_serialize_result(r) for r in results],
    })


def _handle_search(handler: "RBSPHandler", _params: Dict) -> None:
    """GET /api/v1/search?q=...&scope=.&max_results=50

    Query-param based search (convenience for browser/curl).
    """
    parsed = urlparse(handler.path)
    qs = parse_qs(parsed.query)

    query = qs.get("q", [""])[0]
    if not query:
        _error_response(handler, 400, "Missing 'q' query parameter")
        return

    scope = qs.get("scope", ["."])[0]
    kwargs: Dict[str, Any] = {}
    if "max_results" in qs:
        try:
            kwargs["max_results"] = int(qs["max_results"][0])
        except (TypeError, ValueError):
            _error_response(handler, 400, "'max_results' must be an integer")
            return
    if "threshold" in qs:
        try:
            kwargs["threshold"] = float(qs["threshold"][0])
        except (TypeError, ValueError):
            _error_response(handler, 400, "'threshold' must be a number")
            return

    # Throttle check
    throttle = getattr(handler.server, "rbsp_throttle", None)
    metrics = getattr(handler.server, "rbsp_metrics", None)
    if throttle is not None:
        if throttle._query_bp.should_shed_load():
            if metrics is not None:
                metrics.counter("http.503_shed")
            _error_response(handler, 503, "Service overloaded", retry_after=1)
            return
        if not throttle.allow_query():
            if metrics is not None:
                metrics.counter("http.429_throttled")
            _error_response(handler, 429, "Too many requests", retry_after=1)
            return

    if metrics is not None:
        metrics.counter("http.requests.query")

    engine = RBSPEngine.get_instance()
    if not engine.is_initialized:
        engine.init(scope)

    start_ms = time.monotonic()
    results = engine.resolve(query, scope=scope, **kwargs)
    elapsed_ms = (time.monotonic() - start_ms) * 1000.0

    if metrics is not None:
        metrics.histogram("http.query.latency_ms", elapsed_ms)

    if throttle is not None:
        throttle.release_query(elapsed_ms)

    slow_log = getattr(handler.server, "rbsp_slow_log", None)
    if slow_log is not None:
        slow_log.log(query, elapsed_ms, len(results))

    _json_response(handler, 200, {
        "query": query,
        "count": len(results),
        "results": [_serialize_result(r) for r in results],
    })


def _handle_index(handler: "RBSPHandler", _params: Dict) -> None:
    """POST /api/v1/index - Trigger indexing.

    Request body::

        {"path": ".", "force": false}
    """
    body = _read_json_body(handler)
    if body is None:
        return

    path = body.get("path", ".")
    force = bool(body.get("force", False))

    # Throttle check for indexing
    throttle = getattr(handler.server, "rbsp_throttle", None)
    metrics = getattr(handler.server, "rbsp_metrics", None)
    if throttle is not None:
        if not throttle.allow_index():
            if metrics is not None:
                metrics.counter("http.429_index_throttled")
            _error_response(handler, 429, "Too many indexing requests", retry_after=5)
            return

    if metrics is not None:
        metrics.counter("http.requests.index")

    engine = RBSPEngine.get_instance()
    if not engine.is_initialized:
        engine.init(path)

    start_ms = time.monotonic()
    stats = engine.index(path, force=force)
    elapsed_ms = (time.monotonic() - start_ms) * 1000.0

    if metrics is not None:
        metrics.histogram("http.index.latency_ms", elapsed_ms)

    if throttle is not None:
        throttle.release_index(elapsed_ms)

    _json_response(handler, 200, _serialize_index_stats(stats))


def _handle_init(handler: "RBSPHandler", _params: Dict) -> None:
    """POST /api/v1/init - Initialize RBSP in a directory.

    Request body::

        {"path": ".", "overrides": {}}
    """
    body = _read_json_body(handler)
    if body is None:
        return

    path = body.get("path", ".")
    overrides = body.get("overrides")

    engine = RBSPEngine.get_instance()
    engine.init(path, overrides=overrides)
    _json_response(handler, 200, {"initialized": True, "path": path})


def _handle_watcher_start(handler: "RBSPHandler", _params: Dict) -> None:
    """POST /api/v1/watcher/start - Start the file watcher.

    Request body::

        {"path": "."}
    """
    body = _read_json_body(handler)
    if body is None:
        return

    path = body.get("path", ".")
    engine = RBSPEngine.get_instance()
    started = engine.start_watcher(path)
    _json_response(
        handler, 200, {"started": started, "watching": engine.is_watching()}
    )


def _handle_watcher_stop(handler: "RBSPHandler", _params: Dict) -> None:
    """POST /api/v1/watcher/stop - Stop the file watcher."""
    engine = RBSPEngine.get_instance()
    engine.stop_watcher()
    _json_response(
        handler, 200, {"stopped": True, "watching": engine.is_watching()}
    )


def _handle_watcher_status(handler: "RBSPHandler", _params: Dict) -> None:
    """GET /api/v1/watcher - Watcher status."""
    engine = RBSPEngine.get_instance()
    _json_response(handler, 200, {"watching": engine.is_watching()})


# ---------------------------------------------------------------------------
# Route table
# ---------------------------------------------------------------------------

# Each route: (method, path, handler_func)
# Path matching is exact after stripping query string + trailing slash.
_ROUTES: List[Tuple[str, str, Callable[["RBSPHandler", Dict], None]]] = [
    ("GET", "/health", _handle_health),
    ("GET", "/api/v1/status", _handle_status),
    ("POST", "/api/v1/resolve", _handle_resolve),
    ("GET", "/api/v1/search", _handle_search),
    ("POST", "/api/v1/index", _handle_index),
    ("POST", "/api/v1/init", _handle_init),
    ("POST", "/api/v1/watcher/start", _handle_watcher_start),
    ("POST", "/api/v1/watcher/stop", _handle_watcher_stop),
    ("GET", "/api/v1/watcher", _handle_watcher_status),
]


def _match_route(
    method: str, path: str
) -> Optional[Tuple[Callable[["RBSPHandler", Dict], None], Dict]]:
    """Find a matching route handler for the given method and path.

    Returns ``(handler_func, params)`` or ``None``.
    """
    clean_path = path.split("?")[0].rstrip("/") or "/"

    for route_method, route_path, handler_func in _ROUTES:
        if method == route_method and clean_path == route_path.rstrip("/"):
            return handler_func, {}

    return None


# ---------------------------------------------------------------------------
# Request Handler
# ---------------------------------------------------------------------------

class RBSPHandler(BaseHTTPRequestHandler):
    """HTTP request handler for the RBSP API.

    Attributes set by :class:`RBSPServer` on the dynamically-created subclass:

    api_key : str or None
        If set, requests must include an ``X-API-Key`` header.
    server_ref : RBSPServer or None
        Back-reference for shutdown coordination.
    """

    # Enable HTTP/1.1 keep-alive
    protocol_version = "HTTP/1.1"
    timeout = _DEFAULT_SOCKET_TIMEOUT

    api_key: Optional[str] = None
    server_ref: Optional["RBSPServer"] = None

    def log_message(self, format: str, *args: Any) -> None:  # noqa: A002
        """Override to suppress noisy default logging."""
        pass

    def handle_one_request(self) -> None:
        """Override to enforce socket timeout for keep-alive connections."""
        try:
            if self.connection is not None:
                self.connection.settimeout(self.timeout)
        except (OSError, AttributeError):
            pass
        try:
            super().handle_one_request()
        except socket.timeout:
            self.close_connection = True

    def _check_auth(self) -> bool:
        """Validate API key if authentication is configured.

        Returns ``True`` if the request is authorized.
        """
        if self.api_key is None:
            return True
        provided = self.headers.get("X-API-Key", "")
        if provided != self.api_key:
            _error_response(self, 401, "Invalid or missing API key")
            return False
        return True

    def _dispatch(self, method: str) -> None:
        """Route dispatch for a given HTTP method."""
        metrics = getattr(self.server, "rbsp_metrics", None)
        if metrics is not None:
            metrics.counter("http.requests.total")

        if not self._check_auth():
            return

        match = _match_route(method, self.path)
        if match is None:
            if metrics is not None:
                metrics.counter("http.requests.error")
            _error_response(self, 404, f"Not found: {method} {self.path}")
            return

        handler_func, params = match
        try:
            handler_func(self, params)
        except Exception as exc:
            if metrics is not None:
                metrics.counter("http.requests.error")
            traceback.format_exc()  # consume for debugging
            _error_response(self, 500, f"Internal server error: {exc}")

    def do_GET(self) -> None:
        """Handle GET requests."""
        self._dispatch("GET")

    def do_POST(self) -> None:
        """Handle POST requests."""
        self._dispatch("POST")

    def do_OPTIONS(self) -> None:
        """Handle CORS preflight requests."""
        self.send_response(204)
        _add_cors_headers(self)
        self.end_headers()

    def do_DELETE(self) -> None:
        """Handle DELETE requests."""
        self._dispatch("DELETE")

    def do_PUT(self) -> None:
        """Handle PUT requests."""
        self._dispatch("PUT")


# ---------------------------------------------------------------------------
# ThreadPoolHTTPServer
# ---------------------------------------------------------------------------

class _ThreadPoolHTTPServer(HTTPServer):
    """HTTPServer that dispatches requests to a bounded ThreadPoolExecutor.

    Tracks in-flight request count for graceful shutdown and status
    reporting.
    """

    allow_reuse_address = True

    def __init__(
        self,
        server_address: Tuple[str, int],
        handler_class: type,
        *,
        max_workers: int = _DEFAULT_WORKERS,
        request_queue_size: int = _DEFAULT_QUEUE_SIZE,
    ) -> None:
        self.request_queue_size = request_queue_size
        self._executor = ThreadPoolExecutor(
            max_workers=max_workers,
            thread_name_prefix="rbsp-http",
        )
        self._in_flight_count = 0
        self._in_flight_lock = threading.Lock()
        # These are set by RBSPServer after construction
        self.rbsp_metrics: Optional[Metrics] = None
        self.rbsp_throttle: Optional[Throttle] = None
        self.rbsp_slow_log: Optional[SlowQueryLog] = None
        super().__init__(server_address, handler_class)

    def process_request(self, request: Any, client_address: Any) -> None:
        """Submit to thread pool instead of handling synchronously."""
        self._executor.submit(self._handle_in_pool, request, client_address)

    def _handle_in_pool(self, request: Any, client_address: Any) -> None:
        """Process a single request inside the pool, tracking in-flight count."""
        with self._in_flight_lock:
            self._in_flight_count += 1
        if self.rbsp_metrics is not None:
            self.rbsp_metrics.gauge(
                "http.connections.active", self._in_flight_count
            )
        try:
            self.finish_request(request, client_address)
        except Exception:
            self.handle_error(request, client_address)
        finally:
            self.shutdown_request(request)
            with self._in_flight_lock:
                self._in_flight_count -= 1
            if self.rbsp_metrics is not None:
                self.rbsp_metrics.gauge(
                    "http.connections.active", self._in_flight_count
                )

    def graceful_shutdown(self, timeout: float = 30.0) -> None:
        """Drain in-flight requests, then shut down the thread pool.

        Args:
            timeout: Maximum seconds to wait for in-flight requests.
        """
        # Stop accepting new connections
        self.server_close()

        # Drain in-flight requests up to timeout
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            with self._in_flight_lock:
                if self._in_flight_count == 0:
                    break
            time.sleep(0.1)

        # Shut down thread pool
        self._executor.shutdown(wait=True, cancel_futures=False)


# ---------------------------------------------------------------------------
# Server
# ---------------------------------------------------------------------------

class RBSPServer:
    """RBSP HTTP API Server.

    Wraps :class:`_ThreadPoolHTTPServer` with throttle, metrics, slow-query
    logging, optional API-key authentication, and graceful shutdown.

    Parameters
    ----------
    host : str
        Bind address (default ``"127.0.0.1"``).
    port : int
        Bind port (default ``7342``).
    workers : int
        Thread pool size for concurrent requests (default ``8``).
    request_queue_size : int
        OS TCP backlog size (default ``64``).
    api_key : str or None
        If set, every request must include ``X-API-Key: <key>`` header.
    auto_init : str or None
        If set, auto-initialize the engine with this path on startup.
    queries_per_second : float
        Rate limit for query endpoints (default ``100``).
    max_concurrent_queries : int
        Backpressure ceiling for concurrent queries (default ``10``).
    max_concurrent_indexing : int
        Backpressure ceiling for concurrent index ops (default ``2``).
    slow_query_threshold_ms : float
        Queries slower than this are logged (default ``200.0``).
    """

    __slots__ = (
        "_host",
        "_port",
        "_workers",
        "_request_queue_size",
        "_api_key",
        "_auto_init",
        "_shutdown_event",
        "_httpd",
        "_throttle",
        "_metrics",
        "_slow_log",
    )

    def __init__(
        self,
        *,
        host: str = _DEFAULT_HOST,
        port: int = _DEFAULT_PORT,
        workers: int = _DEFAULT_WORKERS,
        request_queue_size: int = _DEFAULT_QUEUE_SIZE,
        api_key: Optional[str] = None,
        auto_init: Optional[str] = None,
        queries_per_second: float = 100,
        max_concurrent_queries: int = 10,
        max_concurrent_indexing: int = 2,
        slow_query_threshold_ms: float = 200.0,
    ) -> None:
        if workers < 1:
            raise ValueError(f"workers must be >= 1, got {workers}")
        if not (0 < port < 65536):
            raise ValueError(f"port must be in 1..65535, got {port}")
        self._host = host
        self._port = port
        self._workers = workers
        self._request_queue_size = request_queue_size
        self._api_key = api_key
        self._auto_init = auto_init
        self._shutdown_event = threading.Event()
        self._httpd: Optional[_ThreadPoolHTTPServer] = None
        self._throttle = Throttle(
            queries_per_second=queries_per_second,
            max_concurrent_queries=max_concurrent_queries,
            max_concurrent_indexing=max_concurrent_indexing,
        )
        self._metrics = Metrics.get_instance()
        self._slow_log = SlowQueryLog(threshold_ms=slow_query_threshold_ms)

    def __repr__(self) -> str:
        return (
            f"RBSPServer(host={self._host!r}, port={self._port}, "
            f"workers={self._workers}, running={self.is_running})"
        )

    @property
    def address(self) -> Tuple[str, int]:
        """Return the ``(host, port)`` the server is bound to."""
        return (self._host, self._port)

    @property
    def is_running(self) -> bool:
        """Return ``True`` if the server is running."""
        return self._httpd is not None and not self._shutdown_event.is_set()

    @property
    def metrics(self) -> Metrics:
        """The metrics collector used by this server."""
        return self._metrics

    @property
    def throttle(self) -> Throttle:
        """The throttle controller used by this server."""
        return self._throttle

    @property
    def slow_log(self) -> SlowQueryLog:
        """The slow query log used by this server."""
        return self._slow_log

    def serve_forever(self) -> None:
        """Start the server and block until shutdown is requested.

        Call :meth:`shutdown` from another thread to stop.
        """
        # Auto-init engine if requested
        if self._auto_init is not None:
            engine = RBSPEngine.get_instance()
            if not engine.is_initialized:
                engine.init(self._auto_init)

        # Configure handler class with server-level settings via dynamic subclass
        handler_class = type(
            "_BoundHandler",
            (RBSPHandler,),
            {"api_key": self._api_key, "server_ref": self},
        )

        self._httpd = _ThreadPoolHTTPServer(
            (self._host, self._port),
            handler_class,
            max_workers=self._workers,
            request_queue_size=self._request_queue_size,
        )
        # Attach throttle, metrics, slow log so handlers can access via self.server
        self._httpd.rbsp_metrics = self._metrics
        self._httpd.rbsp_throttle = self._throttle
        self._httpd.rbsp_slow_log = self._slow_log

        try:
            self._httpd.serve_forever(poll_interval=0.5)
        finally:
            self._cleanup()

    def shutdown(self, timeout: float = 30.0) -> None:
        """Signal the server to shut down gracefully.

        Args:
            timeout: Max seconds to wait for in-flight requests to drain.
        """
        self._shutdown_event.set()
        httpd = self._httpd
        if httpd is not None:
            httpd.shutdown()
            httpd.graceful_shutdown(timeout=timeout)

    def _cleanup(self) -> None:
        """Release server resources."""
        httpd = self._httpd
        self._httpd = None
        if httpd is not None:
            try:
                httpd.server_close()
            except Exception:
                pass
