"""
Adapter Manager — Registry and factory for tool adapters.

Adapter classes register themselves with a URL scheme (e.g. ``mcp``,
``rest``, ``cli``).  At runtime the manager picks the right class based
on the URL's scheme and instantiates it.

Includes URL sniffing: when an exact scheme match fails, heuristic rules
inspect the URL to infer the correct adapter (e.g. ``postgresql://`` →
``sql``, a path ending in ``openapi.json`` → ``openapi``).

All stdlib — no external packages.
"""

from __future__ import annotations

import re
from typing import Callable, Dict, List, Tuple, Type
from urllib.parse import urlparse

from runtime.adapters.base import BaseAdapter

# ── Built-in sniff rules ────────────────────────────────────────────────
# Each rule is a (description, callable) pair.  Rules are evaluated in
# order; the first non-None result wins.

_DB_SCHEMES = frozenset({
    "postgresql", "postgres", "mysql", "mariadb",
    "sqlite", "mssql", "oracle",
})


def _sniff_sql(parsed: object) -> "str | None":
    """Map common database URL schemes to the ``sql`` adapter."""
    scheme = getattr(parsed, "scheme", "").lower().split("+")[0]
    if scheme in _DB_SCHEMES:
        return "sql"
    return None


def _sniff_openapi(parsed: object) -> "str | None":
    """Detect OpenAPI spec URLs (path ends with openapi.json / swagger.json / openapi.yaml)."""
    path = getattr(parsed, "path", "").lower()
    if re.search(r"(openapi|swagger)\.(json|yaml|yml)$", path):
        return "openapi"
    return None


def _sniff_graphql(parsed: object) -> "str | None":
    """Detect GraphQL endpoints (path ends with /graphql)."""
    path = getattr(parsed, "path", "").lower().rstrip("/")
    if path.endswith("/graphql") or path == "graphql":
        return "graphql"
    return None


def _sniff_websocket(parsed: object) -> "str | None":
    """Map ws:// and wss:// to the ``websocket`` adapter."""
    scheme = getattr(parsed, "scheme", "").lower()
    if scheme in ("ws", "wss"):
        return "websocket"
    return None


_BUILTIN_SNIFF_RULES: List[Tuple[str, Callable[[object], "str | None"]]] = [
    ("database URL → sql", _sniff_sql),
    ("openapi/swagger spec → openapi", _sniff_openapi),
    ("graphql endpoint → graphql", _sniff_graphql),
    ("ws/wss → websocket", _sniff_websocket),
]


class AdapterManager:
    """Registers adapter classes and instantiates them by URL scheme.

    When an exact scheme match is not found, built-in *sniff rules* inspect
    the URL to infer the correct adapter.  Custom sniff rules can be added
    via :meth:`add_sniff_rule`.
    """

    def __init__(self) -> None:
        self._registry: Dict[str, Type[BaseAdapter]] = {}
        self._sniff_rules: List[Tuple[str, Callable[[object], "str | None"]]] = list(
            _BUILTIN_SNIFF_RULES
        )

    # ── registration ────────────────────────────────────────────────────

    def register(self, scheme: str, adapter_cls: Type[BaseAdapter]) -> None:
        """Register an adapter class for a given URL scheme.

        Parameters
        ----------
        scheme:
            URL scheme this adapter handles (e.g. ``"mcp"``, ``"rest"``).
        adapter_cls:
            A concrete subclass of :class:`BaseAdapter`.

        Raises
        ------
        TypeError
            If *adapter_cls* is not a subclass of :class:`BaseAdapter`.
        ValueError
            If *scheme* is already registered.
        """
        if not (isinstance(adapter_cls, type) and issubclass(adapter_cls, BaseAdapter)):
            raise TypeError(
                f"adapter_cls must be a BaseAdapter subclass, got {adapter_cls!r}"
            )
        scheme = scheme.lower()
        if scheme in self._registry:
            raise ValueError(f"Scheme {scheme!r} is already registered")
        self._registry[scheme] = adapter_cls

    def add_sniff_rule(
        self, description: str, rule: Callable[[object], "str | None"]
    ) -> None:
        """Append a custom URL sniff rule.

        Parameters
        ----------
        description:
            Human-readable label for debugging.
        rule:
            Callable that receives a :class:`urllib.parse.ParseResult` and
            returns a scheme name (``str``) or ``None``.
        """
        self._sniff_rules.append((description, rule))

    # ── factory ─────────────────────────────────────────────────────────

    def create_adapter(self, url: str) -> BaseAdapter:
        """Instantiate an adapter from a URL.

        Resolution order:

        1. **Exact scheme match** — the URL's scheme is looked up directly
           in the registry.
        2. **Sniff rules** — each registered sniff rule is tried in order.
           The first rule that returns a scheme present in the registry wins.
        3. **ValueError** — raised if neither step finds a match.

        Parameters
        ----------
        url:
            A URL whose scheme matches a registered adapter
            (e.g. ``"mcp://localhost:9000"``), or one that can be inferred
            via sniffing (e.g. ``"postgresql://db/mydb"``).

        Returns
        -------
        BaseAdapter
            A new adapter instance.

        Raises
        ------
        ValueError
            If no adapter can be resolved for the URL.
        """
        parsed = urlparse(url)
        scheme = parsed.scheme.lower()

        # 1. Exact scheme match
        if scheme in self._registry:
            return self._registry[scheme](url)

        # 2. URL sniffing — try heuristic rules
        for _desc, rule in self._sniff_rules:
            sniffed = rule(parsed)
            if sniffed and sniffed in self._registry:
                return self._registry[sniffed](url)

        registered = ", ".join(sorted(self._registry)) or "(none)"
        raise ValueError(
            f"No adapter registered for scheme {scheme!r} "
            f"(sniffing also failed). "
            f"Registered schemes: {registered}"
        )
