"""Veculo API client."""

from __future__ import annotations

import os
from typing import Any, Optional

import httpx

from veculo.errors import AuthenticationError, NotFoundError, ValidationError, VeculoError

_DEFAULT_ENDPOINT = "https://api.veculo.com"
_DEFAULT_TIMEOUT = 30.0
_API_VERSION = "v1"


class VeculoClient:
    """Client for the Veculo managed graph+vector database API.

    The client reads configuration from explicit arguments, then falls back to
    environment variables:

    * ``VECULO_ENDPOINT``  -- API base URL (default ``https://api.veculo.com``)
    * ``VECULO_API_KEY``   -- API key for authentication
    * ``VECULO_CLUSTER_ID`` -- Target cluster identifier

    Example::

        from veculo import VeculoClient

        client = VeculoClient(api_key="vk-...", cluster_id="cl-a7f3b2")
        client.put_vertex(id="alice", label="person", properties={"name": "Alice"})
        result = client.get_vertex(id="alice")
    """

    def __init__(
        self,
        endpoint: str | None = None,
        api_key: str | None = None,
        cluster_id: str | None = None,
        cluster_name: str | None = None,
        *,
        timeout: float = _DEFAULT_TIMEOUT,
    ) -> None:
        """Initialize the Veculo client.

        Provide either ``cluster_id`` or ``cluster_name``. If
        ``cluster_name`` is given, the client resolves it to an ID via
        the API on first use.

        Args:
            endpoint: API endpoint URL. Defaults to ``VECULO_ENDPOINT`` env var
                or ``https://api.veculo.com``.
            api_key: Veculo API key. Defaults to ``VECULO_API_KEY`` env var.
            cluster_id: Cluster ID (e.g., ``cl-a7f3b2``). Defaults to
                ``VECULO_CLUSTER_ID`` env var.
            cluster_name: Cluster display name (e.g., ``"production"``).
                Defaults to ``VECULO_CLUSTER_NAME`` env var. Resolved to
                cluster_id automatically.
            timeout: HTTP request timeout in seconds.

        Raises:
            VeculoError: If ``api_key`` or cluster cannot be resolved.
        """
        self.endpoint = (
            endpoint or os.environ.get("VECULO_ENDPOINT") or _DEFAULT_ENDPOINT
        ).rstrip("/")
        self.api_key = api_key or os.environ.get("VECULO_API_KEY")
        self._timeout = timeout

        if not self.api_key:
            raise VeculoError(
                "API key is required. Pass api_key= or set VECULO_API_KEY."
            )

        # Resolve cluster: ID takes priority, then name lookup
        self.cluster_id = cluster_id or os.environ.get("VECULO_CLUSTER_ID")
        cluster_name = cluster_name or os.environ.get("VECULO_CLUSTER_NAME")

        if not self.cluster_id and cluster_name:
            self.cluster_id = self._resolve_cluster_name(cluster_name)
        elif not self.cluster_id:
            raise VeculoError(
                "Cluster is required. Pass cluster_id=, cluster_name=, "
                "or set VECULO_CLUSTER_ID / VECULO_CLUSTER_NAME."
            )

        self._client = httpx.Client(
            base_url=f"{self.endpoint}/{_API_VERSION}/{self.cluster_id}",
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
                "User-Agent": "veculo-python/0.1.0",
            },
            timeout=timeout,
        )

    def _resolve_cluster_name(self, name: str) -> str:
        """Look up a cluster ID by display name via the API."""
        response = httpx.get(
            f"{self.endpoint}/tenants",
            headers={"Authorization": f"Bearer {self.api_key}"},
            timeout=self._timeout,
        )
        if not response.is_success:
            raise VeculoError(
                f"Failed to list clusters: HTTP {response.status_code}"
            )
        clusters = response.json()
        for cluster in clusters:
            if cluster.get("name") == name:
                return cluster["tenant_id"]
        raise NotFoundError(f"No cluster found with name '{name}'")

    # ------------------------------------------------------------------
    # Vertex operations
    # ------------------------------------------------------------------

    def put_vertex(
        self,
        id: str,
        label: str = "default",
        properties: dict[str, Any] | None = None,
        embedding: list[float] | None = None,
        visibility: str | None = None,
    ) -> dict:
        """Insert or update a vertex, optionally with a vector embedding.

        Args:
            id: Unique vertex identifier.
            label: Vertex label / type.
            properties: Key-value properties to store.
            embedding: Optional vector embedding for similarity search.
            visibility: Accumulo-style visibility expression.

        Returns:
            API response body as a dictionary.
        """
        body: dict[str, Any] = {"id": id, "label": label}
        if properties is not None:
            body["properties"] = properties
        if embedding is not None:
            body["embedding"] = embedding
        if visibility is not None:
            body["visibility"] = visibility
        return self._request("POST", "/vertices", json=body)

    def get_vertex(
        self,
        id: str,
        authorizations: str | None = None,
    ) -> dict:
        """Retrieve a vertex by ID.

        Args:
            id: Vertex identifier.
            authorizations: Comma-separated visibility authorizations.

        Returns:
            Vertex data as a dictionary.
        """
        params: dict[str, str] | None = None
        if authorizations is not None:
            params = {"authorizations": authorizations}
        return self._request("GET", f"/vertices/{id}", params=params)

    # ------------------------------------------------------------------
    # Edge operations
    # ------------------------------------------------------------------

    def put_edge(
        self,
        source: str,
        target: str,
        edge_type: str,
        properties: dict[str, Any] | None = None,
        visibility: str | None = None,
    ) -> dict:
        """Create or update a directed edge between two vertices.

        Args:
            source: Source vertex ID.
            target: Target vertex ID.
            edge_type: Relationship type (e.g., ``"knows"``).
            properties: Key-value properties to store on the edge.
            visibility: Accumulo-style visibility expression.

        Returns:
            API response body as a dictionary.
        """
        body: dict[str, Any] = {
            "source": source,
            "target": target,
            "edge_type": edge_type,
        }
        if properties is not None:
            body["properties"] = properties
        if visibility is not None:
            body["visibility"] = visibility
        return self._request("POST", "/edges", json=body)

    # ------------------------------------------------------------------
    # Query operations
    # ------------------------------------------------------------------

    def get_neighbors(
        self,
        vertex_id: str,
        edge_type: str,
        depth: int = 1,
        authorizations: str | None = None,
    ) -> dict:
        """Traverse the graph to find neighbors of a vertex.

        Args:
            vertex_id: Starting vertex ID.
            edge_type: Filter traversal to this edge type.
            depth: Maximum traversal depth (default 1).
            authorizations: Comma-separated visibility authorizations.

        Returns:
            Neighboring vertices and edges as a dictionary.
        """
        params: dict[str, Any] = {"edge_type": edge_type, "depth": str(depth)}
        if authorizations is not None:
            params["authorizations"] = authorizations
        return self._request("GET", f"/vertices/{vertex_id}/neighbors", params=params)

    def query(
        self,
        embedding: list[float] | None = None,
        top_k: int = 10,
        edge_type: str | None = None,
        depth: int = 1,
        authorizations: str | None = None,
    ) -> dict:
        """Execute a hybrid vector similarity + graph traversal query.

        Finds the ``top_k`` most similar vertices by embedding, then
        optionally expands the result set via graph traversal.

        Args:
            embedding: Query vector for similarity search.
            top_k: Number of nearest neighbors to return (default 10).
            edge_type: Optional edge type filter for graph expansion.
            depth: Graph traversal depth (default 1).
            authorizations: Comma-separated visibility authorizations.

        Returns:
            Combined vector and graph results as a dictionary.
        """
        body: dict[str, Any] = {"top_k": top_k, "depth": depth}
        if embedding is not None:
            body["embedding"] = embedding
        if edge_type is not None:
            body["edge_type"] = edge_type
        if authorizations is not None:
            body["authorizations"] = authorizations
        return self._request("POST", "/query", json=body)

    def get_metrics(self) -> dict:
        """Retrieve cluster health and performance metrics.

        Returns:
            Cluster metrics as a dictionary.
        """
        return self._request("GET", "/metrics")

    # ------------------------------------------------------------------
    # Internals
    # ------------------------------------------------------------------

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, str] | None = None,
        json: dict[str, Any] | None = None,
    ) -> dict:
        """Send an HTTP request and return the parsed JSON response.

        Raises:
            AuthenticationError: On 401 or 403 responses.
            NotFoundError: On 404 responses.
            ValidationError: On 400 or 422 responses.
            VeculoError: On any other non-2xx response.
        """
        response = self._client.request(method, path, params=params, json=json)

        if response.is_success:
            if response.status_code == 204:
                return {}
            return response.json()

        # Parse error body
        try:
            error_body = response.json()
            message = error_body.get("error", {}).get("message", response.text)
        except Exception:
            error_body = None
            message = response.text or f"HTTP {response.status_code}"

        status = response.status_code
        if status in (401, 403):
            raise AuthenticationError(message, status_code=status, body=error_body)
        if status == 404:
            raise NotFoundError(message, status_code=status, body=error_body)
        if status in (400, 422):
            raise ValidationError(message, status_code=status, body=error_body)
        raise VeculoError(message, status_code=status, body=error_body)

    def close(self) -> None:
        """Close the underlying HTTP client."""
        self._client.close()

    def __enter__(self) -> "VeculoClient":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def __repr__(self) -> str:
        return (
            f"VeculoClient(endpoint={self.endpoint!r}, "
            f"cluster_id={self.cluster_id!r})"
        )
