"""
aiel_sdk.client
===============

Root SDK client for calling AIEL APIs.

This module defines :class:`~aiel_sdk.client.AielClient`, an "AWS-SDK style" entry point that
centralizes configuration (base URL, auth, headers, timeouts) and exposes service clients
(e.g., IntegrationsService, MemoryService).

Auth resolution
---------------
`AielClient` resolves an API token in the following order:

1) Explicit `api_key` passed to the constructor
2) Environment variable `AIEL_TOKEN`
3) OS keyring entry (via `keyring.get_password("aiel", "default")`) when available

If no token is found, requests are sent without Authorization headers (and may fail
depending on server policy).

Services
--------
After initialization, the client exposes:
- ``client.integrations``: :class:`~aiel_sdk.services.integrations.IntegrationsService`
- ``client.memory``: :class:`~aiel_sdk.services.memory.MemoryService`

These service objects use the same underlying transport, retry policy, and headers.

Transport, retries, and timeouts
--------------------------------
Requests are sent through a configurable :class:`~aiel_sdk.transport.Transport`.
By default, the SDK uses :class:`~aiel_sdk.transport.UrllibTransport` with a default
:class:`~aiel_sdk.transport.RetryPolicy`.

The `timeout` parameter controls request timeouts (seconds) unless overridden per-call.

Request correlation
-------------------
If a `request_id` is provided to `_request`, the transport may attach it for correlation
(implementation depends on the transport).
"""

from __future__ import annotations

import os
import re
from typing import Any, Dict, Mapping, Optional
from urllib.parse import urlparse

from ..services.integrations import IntegrationsService
from ..services.memory import MemoryService
from ..transport import RetryPolicy, Transport, UrllibTransport


def _try_get_keyring_token(service: str = "aiel", username: str = "default") -> str | None:
    """
    Attempt to read an API token from the OS keyring.

    This helper is best-effort:
    - If the `keyring` package is not installed or fails to load, returns None.
    - If the keyring backend is unavailable or lookup fails, returns None.

    Args:
        service:
            Keyring service name. Defaults to "aiel".
        username:
            Keyring username entry. Defaults to "default".

    Returns:
        str | None:
            Token string if found, otherwise None.
    """
    try:
        import keyring  # type: ignore
    except Exception:
        return None
    try:
        return keyring.get_password(service, username)
    except Exception:
        return None


def _resolve_token(explicit: str | None) -> str | None:
    """
    Resolve an API token using the SDK's standard precedence order.

    Precedence:
    1) `explicit` argument (if provided and truthy)
    2) Environment variable `AIEL_TOKEN`
    3) OS keyring token (see `_try_get_keyring_token`)

    Args:
        explicit:
            Token string passed directly by the caller (e.g., constructor `api_key`).

    Returns:
        str | None:
            The resolved token, or None if no token source was available.
    """
    if explicit:
        return explicit
    env_token = os.getenv("AIEL_TOKEN")
    if env_token:
        return env_token
    kr = _try_get_keyring_token()
    if kr:
        return kr
    return None


_SCOPE_FROM_PATH_RE = re.compile(r"/v1/workspaces/([^/]+)/projects/([^/]+)(?:/|$)")


def _extract_scope_from_url(url: str | None) -> tuple[str, str] | None:
    if not url:
        return None
    try:
        path = urlparse(url).path or url
    except Exception:
        path = url
    m = _SCOPE_FROM_PATH_RE.search(path)
    if not m:
        return None
    ws, proj = (m.group(1) or "").strip(), (m.group(2) or "").strip()
    if not ws or not proj:
        return None
    return ws, proj


class AielClient:
    """
    Root client that wires auth + transport + service clients.

    This class stores shared configuration and exposes typed service wrappers
    (e.g., memory and integrations).

    Args:
        base_url:
            Base URL for the AIEL API. Trailing slashes are stripped.
        api_key:
            Optional API token. If omitted, token resolution falls back to
            environment variable `AIEL_TOKEN`, then OS keyring.
        timeout:
            Default request timeout in seconds (applies to all requests unless overridden).
        extra_headers:
            Additional HTTP headers merged into every request.
        use_x_api_token_fallback:
            When True and a token is available, also send `X-API-Token: <token>` in addition
            to `Authorization: Bearer <token>`. Useful for servers that support either header.
        transport:
            Optional custom transport implementing :class:`~aiel_sdk.transport.Transport`.
            Defaults to :class:`~aiel_sdk.transport.UrllibTransport`.
        retry_policy:
            Optional retry policy. Defaults to :class:`~aiel_sdk.transport.RetryPolicy`.
        user_agent:
            Optional User-Agent header value. Defaults to "aiel-sdk/0.1 (urllib)".

    Attributes:
        base_url:
            Normalized base URL (no trailing slash).
        timeout:
            Default timeout (seconds).
        extra_headers:
            Extra headers included with every request.
        use_x_api_token_fallback:
            Whether to include `X-API-Token` when a token is present.
        transport:
            Transport instance used to execute HTTP requests.
        retry_policy:
            Retry policy used by the transport.
        user_agent:
            User-Agent string used for requests.
        integrations:
            Integrations service client.
        memory:
            Memory service client.

    Raises:
        ValueError:
            If `base_url` is empty after normalization.
    """

    def __init__(
        self,
        *,
        base_url: str,
        api_key: str | None = None,
        workspace_id: str | None = None,
        project_id: str | None = None,
        timeout: float = 30.0,
        extra_headers: Optional[Dict[str, str]] = None,
        use_x_api_token_fallback: bool = False,
        transport: Optional[Transport] = None,
        retry_policy: Optional[RetryPolicy] = None,
        user_agent: Optional[str] = None,
    ) -> None:
        """
        Initialize the client and its service clients.

        This performs:
        - base_url normalization and validation
        - token resolution
        - transport and retry policy configuration
        - service client construction (integrations, memory)

        See class docstring for parameter details.
        """
        self.base_url = base_url.rstrip("/")
        if not self.base_url:
            raise ValueError("base_url is required")

        self.timeout = timeout
        self.extra_headers = extra_headers or {}
        self.use_x_api_token_fallback = use_x_api_token_fallback

        self._token = _resolve_token(api_key)
        self.workspace_id = workspace_id
        self.project_id = project_id

        self.transport = transport or UrllibTransport()
        self.retry_policy = retry_policy or RetryPolicy()
        self.user_agent = user_agent or "aiel-sdk/0.1 (urllib)"

        # Services
        self.integrations = IntegrationsService(self)
        self.memory = MemoryService(self)

    def resolve_scope(self) -> tuple[str | None, str | None]:
        """
        Resolve workspace/project IDs from explicit client config, environment,
        or URL path when available.
        """
        ws = self.workspace_id or os.getenv("AIEL_WORKSPACE_ID")
        proj = self.project_id or os.getenv("AIEL_PROJECT_ID")
        if ws and proj:
            return ws, proj

        parsed = _extract_scope_from_url(self.base_url)
        if parsed:
            p_ws, p_proj = parsed
            return ws or p_ws, proj or p_proj

        for key in ("AIEL_EXECUTE_URL", "AIEL_REQUEST_URL", "AIEL_URL"):
            parsed = _extract_scope_from_url(os.getenv(key))
            if parsed:
                p_ws, p_proj = parsed
                return ws or p_ws, proj or p_proj

        return ws, proj

    def _build_headers(self) -> Dict[str, str]:
        """
        Build request headers for an API call.

        Includes:
        - Accept: application/json
        - User-Agent
        - any `extra_headers`
        - Authorization: Bearer <token> (when token is available)
        - X-API-Token: <token> (optional fallback when enabled)

        Returns:
            dict[str, str]:
                Headers to send with the request.
        """
        headers = {
            "Accept": "application/json",
            "User-Agent": self.user_agent,
            **self.extra_headers,
        }
        if self._token:
            headers["Authorization"] = f"Bearer {self._token}"
            if self.use_x_api_token_fallback:
                headers["X-API-Token"] = self._token
        return headers

    def _request(
        self,
        service: str,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        json_body: Any | None = None,
        timeout: float | None = None,
        request_id: str | None = None,
    ) -> Any:
        """
        Execute an HTTP request via the configured transport.

        This is the internal low-level request primitive used by service clients.

        Args:
            service:
                Logical service name (used for tracing/logging/routing by the transport).
            method:
                HTTP method (e.g., "GET", "POST", "PATCH").
            path:
                Request path relative to `base_url` (must start with "/").
            params:
                Optional query string parameters.
            json_body:
                Optional JSON payload to send (serialized by the transport).
            timeout:
                Optional per-request timeout override. If None, uses `self.timeout`.
            request_id:
                Optional correlation/request id forwarded to the transport.

        Returns:
            Any:
                Parsed response object as returned by the transport (often a dict or model).

        Raises:
            Exception:
                Transport-specific exceptions (HTTP errors, network errors, retries exhausted).
                Exact exception types depend on the transport implementation.
        """
        return self.transport.request(
            service=service,
            method=method,
            base_url=self.base_url,
            path=path,
            headers=self._build_headers(),
            params=params,
            json_body=json_body,
            timeout=self.timeout if timeout is None else timeout,
            retry=self.retry_policy,
            request_id=request_id,
        )
