"""
GraphQL Adapter — Expose a GraphQL endpoint as Brynq tools.

Accepts a GraphQL endpoint URL (e.g. ``https://api.example.com/graphql``).
The schema is discovered via the standard introspection query, and top-level
Query and Mutation fields are surfaced as tools via :meth:`introspect`.
Individual operations can be called via :meth:`execute`.

All stdlib — no external packages.
"""

from __future__ import annotations

import json
from typing import Any, Dict, List

from runtime.adapters.base import BaseAdapter

_VERSION = "0.1.0"

# Standard GraphQL introspection query — fetches top-level Query/Mutation
# fields with their arguments and return types.
_INTROSPECTION_QUERY = """
query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    types {
      name
      kind
      fields {
        name
        description
        args {
          name
          description
          type {
            name
            kind
            ofType { name kind }
          }
          defaultValue
        }
        type {
          name
          kind
          ofType { name kind }
        }
      }
    }
  }
}
""".strip()


class GraphQLAdapter(BaseAdapter):
    """Adapter that turns a GraphQL endpoint into callable tools.

    Parameters
    ----------
    endpoint:
        URL of the GraphQL endpoint
        (e.g. ``https://api.example.com/graphql``).
    """

    def __init__(self, endpoint: str) -> None:
        self._endpoint = endpoint
        self._schema: Dict[str, Any] | None = None
        self._tools: List[Dict[str, Any]] | None = None

    # ── BaseAdapter interface ────────────────────────────────────────────

    def introspect(self) -> Dict[str, Any]:
        """Parse the introspection result and return discovered tools."""
        self._ensure_loaded()
        return {
            "tools": list(self._tools),  # type: ignore[arg-type]
            "version": _VERSION,
            "endpoint": self._endpoint,
        }

    def execute(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
        """Execute a GraphQL operation by tool name.

        Constructs a GraphQL query or mutation string from the tool
        definition and the supplied arguments, sends it via HTTP POST,
        and returns the response data.

        Parameters
        ----------
        tool_name:
            Name of a tool returned by :meth:`introspect`.
        args:
            Keyword arguments forwarded as GraphQL variables / field args.
        """
        self._ensure_loaded()

        tool = self._find_tool(tool_name)
        if tool is None:
            return {"status": "error", "result": f"Unknown tool: {tool_name}"}

        query_string = self._build_query(tool, args)

        try:
            response = self._post_query(query_string)
        except Exception as exc:  # noqa: BLE001
            return {"status": "error", "result": str(exc)}

        if "errors" in response and response["errors"]:
            messages = [e.get("message", str(e)) for e in response["errors"]]
            return {"status": "error", "result": "; ".join(messages)}

        data = response.get("data", {})
        return {"status": "ok", "result": data}

    # ── Schema loading ───────────────────────────────────────────────────

    def _ensure_loaded(self) -> None:
        """Load and parse the schema lazily (once)."""
        if self._schema is not None:
            return
        self._schema = self._fetch_schema()
        self._tools = self._extract_tools(self._schema)

    def _fetch_schema(self) -> Dict[str, Any]:
        """Send the standard introspection query and cache the result."""
        response = self._post_query(_INTROSPECTION_QUERY)

        if "errors" in response and response["errors"]:
            messages = [e.get("message", str(e)) for e in response["errors"]]
            raise ValueError(f"Introspection failed: {'; '.join(messages)}")

        data = response.get("data")
        if not isinstance(data, dict) or "__schema" not in data:
            raise ValueError("Introspection response missing __schema")

        return data["__schema"]

    # ── Query building ───────────────────────────────────────────────────

    @staticmethod
    def _build_query(tool: Dict[str, Any], args: Dict[str, Any]) -> str:
        """Construct a GraphQL query/mutation string from a tool and args.

        Produces a query like::

            query { users(limit: 10) }
            mutation { createUser(name: "Alice") }
        """
        operation_type = tool.get("operation_type", "query")
        field_name = tool["field_name"]

        if args:
            arg_parts = []
            for key, value in args.items():
                arg_parts.append(f"{key}: {_encode_graphql_value(value)}")
            args_str = f"({', '.join(arg_parts)})"
        else:
            args_str = ""

        return f"{operation_type} {{ {field_name}{args_str} }}"

    # ── Tool extraction ──────────────────────────────────────────────────

    @staticmethod
    def _extract_tools(schema: Dict[str, Any]) -> List[Dict[str, Any]]:
        """Walk the introspection schema and produce a tool per top-level field."""
        tools: List[Dict[str, Any]] = []
        types_by_name: Dict[str, Dict[str, Any]] = {}

        for t in schema.get("types") or []:
            if isinstance(t, dict) and "name" in t:
                types_by_name[t["name"]] = t

        # Extract Query fields
        query_type = schema.get("queryType")
        if isinstance(query_type, dict) and query_type.get("name"):
            _add_fields(types_by_name, query_type["name"], "query", tools)

        # Extract Mutation fields
        mutation_type = schema.get("mutationType")
        if isinstance(mutation_type, dict) and mutation_type.get("name"):
            _add_fields(types_by_name, mutation_type["name"], "mutation", tools)

        return tools

    def _find_tool(self, tool_name: str) -> Dict[str, Any] | None:
        """Look up a tool by name (case-sensitive)."""
        assert self._tools is not None  # noqa: S101
        for tool in self._tools:
            if tool["name"] == tool_name:
                return tool
        return None

    # ── HTTP ─────────────────────────────────────────────────────────────

    def _post_query(self, query: str) -> Dict[str, Any]:
        """POST a GraphQL query to the endpoint and return parsed JSON."""
        import urllib.request  # noqa: E402 — deferred to avoid import cost

        payload = json.dumps({"query": query}).encode("utf-8")
        req = urllib.request.Request(
            self._endpoint,
            data=payload,
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            method="POST",
        )
        with urllib.request.urlopen(req, timeout=30) as resp:  # noqa: S310
            raw = resp.read().decode("utf-8")
        return json.loads(raw)


# ── helpers ──────────────────────────────────────────────────────────────


def _add_fields(
    types_by_name: Dict[str, Dict[str, Any]],
    type_name: str,
    operation_type: str,
    tools: List[Dict[str, Any]],
) -> None:
    """Add tool descriptors for each field in the given type."""
    type_def = types_by_name.get(type_name)
    if not isinstance(type_def, dict):
        return
    for field in type_def.get("fields") or []:
        if not isinstance(field, dict) or "name" not in field:
            continue
        parameters = _extract_parameters(field)
        return_type = _resolve_type_name(field.get("type"))
        tools.append({
            "name": field["name"],
            "description": field.get("description") or field["name"],
            "parameters": parameters,
            "operation_type": operation_type,
            "field_name": field["name"],
            "return_type": return_type,
        })


def _extract_parameters(field: Dict[str, Any]) -> List[Dict[str, Any]]:
    """Extract parameter descriptors from a field's args."""
    params: List[Dict[str, Any]] = []
    for arg in field.get("args") or []:
        if not isinstance(arg, dict) or "name" not in arg:
            continue
        arg_type = _resolve_type_name(arg.get("type"))
        required = _is_non_null(arg.get("type"))
        params.append({
            "name": arg["name"],
            "description": arg.get("description") or "",
            "required": required,
            "type": arg_type,
        })
    return params


def _resolve_type_name(type_info: Any) -> str:
    """Resolve a GraphQL type to a human-readable name."""
    if not isinstance(type_info, dict):
        return "unknown"
    # NON_NULL wrapper — recurse into ofType
    if type_info.get("kind") == "NON_NULL":
        inner = type_info.get("ofType")
        if isinstance(inner, dict):
            return _resolve_type_name(inner) + "!"
        return "unknown!"
    # LIST wrapper
    if type_info.get("kind") == "LIST":
        inner = type_info.get("ofType")
        if isinstance(inner, dict):
            return f"[{_resolve_type_name(inner)}]"
        return "[unknown]"
    # Named type
    return type_info.get("name") or "unknown"


def _is_non_null(type_info: Any) -> bool:
    """Check if a GraphQL type is NON_NULL (i.e. required)."""
    if isinstance(type_info, dict):
        return type_info.get("kind") == "NON_NULL"
    return False


def _encode_graphql_value(value: Any) -> str:
    """Encode a Python value as a GraphQL literal."""
    if value is None:
        return "null"
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, int):
        return str(value)
    if isinstance(value, float):
        return str(value)
    if isinstance(value, str):
        # Escape backslashes and double quotes for GraphQL string literals
        escaped = value.replace("\\", "\\\\").replace('"', '\\"')
        return f'"{escaped}"'
    if isinstance(value, list):
        items = ", ".join(_encode_graphql_value(v) for v in value)
        return f"[{items}]"
    if isinstance(value, dict):
        fields = ", ".join(
            f"{k}: {_encode_graphql_value(v)}" for k, v in value.items()
        )
        return f"{{{fields}}}"
    # Fallback — treat as string
    return f'"{value}"'
