"""
HTTP client for the 1id.com Enrollment API.

Handles all HTTP communication with the 1id.com server, including:
- Enrollment requests (declared and sovereign tiers)
- Identity lookups
- Handle management
- Error response mapping to SDK exceptions

All responses follow the 1id.com API envelope:
  {"ok": true, "data": {...}, "error": null}
  {"ok": false, "data": null, "error": {"code": "...", "message": "..."}}
"""

from __future__ import annotations

import logging
from typing import Any

import httpx

from .credentials import DEFAULT_API_BASE_URL
from .exceptions import (
  EnrollmentError,
  NetworkError,
  raise_from_server_error_response,
)

logger = logging.getLogger("oneid.client")

# -- HTTP client configuration --
DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
from ._version import USER_AGENT


class OneIDAPIClient:
  """HTTP client for the 1id.com enrollment and identity API.

  Wraps httpx.Client with 1id-specific error handling. All methods
  raise SDK exceptions on failure, never raw HTTP errors.

  Args:
      api_base_url: Base URL for the 1id.com API (default: https://1id.com).
      timeout_seconds: HTTP request timeout in seconds.
  """

  def __init__(
    self,
    api_base_url: str = DEFAULT_API_BASE_URL,
    timeout_seconds: float = DEFAULT_HTTP_TIMEOUT_SECONDS,
  ) -> None:
    self.api_base_url = api_base_url.rstrip("/")
    self.timeout_seconds = timeout_seconds

  def _make_request(
    self,
    method: str,
    path: str,
    json_body: dict[str, Any] | None = None,
    headers: dict[str, str] | None = None,
  ) -> dict[str, Any]:
    """Make an HTTP request to the 1id.com API and parse the envelope response.

    Args:
        method: HTTP method ('GET', 'POST', 'DELETE').
        path: API path (e.g., '/api/v1/enroll/declared'). Will be appended to base URL.
        json_body: JSON request body (for POST requests).
        headers: Additional HTTP headers.

    Returns:
        The 'data' field from the successful response envelope.

    Raises:
        NetworkError: If the request fails due to connection issues.
        EnrollmentError (or subclass): If the server returns an error response.
    """
    url = f"{self.api_base_url}{path}"

    request_headers = {
      "User-Agent": USER_AGENT,
      "Accept": "application/json",
    }
    if headers:
      request_headers.update(headers)

    try:
      with httpx.Client(timeout=self.timeout_seconds) as http_client:
        response = http_client.request(
          method=method,
          url=url,
          json=json_body,
          headers=request_headers,
        )
    except httpx.ConnectError as connection_error:
      raise NetworkError(
        f"Could not connect to {self.api_base_url}: {connection_error}"
      ) from connection_error
    except httpx.TimeoutException as timeout_error:
      raise NetworkError(
        f"Request to {url} timed out after {self.timeout_seconds}s: {timeout_error}"
      ) from timeout_error
    except httpx.HTTPError as http_error:
      raise NetworkError(
        f"HTTP error communicating with {url}: {http_error}"
      ) from http_error

    # Parse the response envelope
    try:
      response_body = response.json()
    except Exception as json_parse_error:
      raise NetworkError(
        f"Invalid JSON response from {url} (HTTP {response.status_code}): {json_parse_error}"
      ) from json_parse_error

    # Check for the standard 1id error envelope
    if not response_body.get("ok", False):
      error_info = response_body.get("error", {})
      error_code = error_info.get("code", "UNKNOWN_ERROR")
      error_message = error_info.get("message", f"Server returned HTTP {response.status_code}")
      logger.warning("API error from %s: [%s] %s", url, error_code, error_message)
      raise_from_server_error_response(error_code, error_message)

    return response_body.get("data", {})

  def enroll_declared(
    self,
    software_key_pem: str,
    key_algorithm: str,
    operator_email: str | None = None,
    requested_handle: str | None = None,
    display_name: str | None = None,
  ) -> dict[str, Any]:
    """Enroll a new identity at the declared trust tier (no HSM required).

    Args:
        software_key_pem: PEM-encoded public key generated by the SDK.
        key_algorithm: Algorithm of the key (e.g., 'ed25519', 'ecdsa-p256').
        operator_email: Optional human operator contact email.
        requested_handle: Optional vanity handle to claim (without '@' prefix).
        display_name: Optional friendly name for the agent.

    Returns:
        Server response data containing identity, credentials, and initial tokens.

    Raises:
        HandleTakenError: If the requested handle is already in use.
        HandleInvalidError: If the requested handle violates naming rules.
        HandleRetiredError: If the requested handle is permanently retired.
        EnrollmentError: If enrollment fails for any other reason.
        NetworkError: If the server cannot be reached.
    """
    request_body: dict[str, Any] = {
      "software_key_pem": software_key_pem,
      "key_algorithm": key_algorithm,
    }
    if operator_email is not None:
      request_body["operator_email"] = operator_email
    if requested_handle is not None:
      request_body["requested_handle"] = requested_handle
    if display_name is not None:
      request_body["display_name"] = display_name

    return self._make_request("POST", "/api/v1/enroll/declared", json_body=request_body)

  def enroll_begin(
    self,
    ek_certificate_pem: str,
    ak_public_key_pem: str,
    ak_tpmt_public_b64: str = "",
    ek_public_key_pem: str = "",
    ek_certificate_chain_pem: list[str] | None = None,
    hsm_type: str = "tpm",
    operator_email: str | None = None,
    requested_handle: str | None = None,
    display_name: str | None = None,
  ) -> dict[str, Any]:
    """Begin TPM/HSM-based enrollment (sovereign/virtual tiers).

    This is the first step of the two-phase enrollment flow. The server
    validates the attestation chain and returns a credential activation
    challenge (credential_blob + encrypted_secret) that the TPM must
    decrypt via ActivateCredential to prove possession.

    Args:
        ek_certificate_pem: PEM-encoded EK certificate (TPM) or attestation cert (YubiKey).
        ak_public_key_pem: PEM-encoded public key of the Attestation Identity Key.
        ak_tpmt_public_b64: Base64-encoded marshaled TPMT_PUBLIC of the AK.
            Required for TPM enrollment (server uses it to compute AK Name for MakeCredential).
        ek_certificate_chain_pem: Optional intermediate CA certs found on the client (best-effort).
        hsm_type: Type of HSM ('tpm', 'yubikey', 'nitrokey', etc.).
        operator_email: Optional human operator contact email.
        requested_handle: Optional vanity handle to claim.
        display_name: Optional friendly name for the agent.

    Returns:
        Server response containing enrollment_session_id, credential_blob,
        encrypted_secret, trust_tier classification, and session expiry.

    Raises:
        AlreadyEnrolledError: If this HSM is already enrolled.
        HandleTakenError: If the requested handle is taken.
        EnrollmentError: If the EK cert is invalid or chain is untrusted.
        NetworkError: If the server cannot be reached.
    """
    request_body: dict[str, Any] = {
      "ek_certificate_pem": ek_certificate_pem,
      "ek_public_key_pem": ek_public_key_pem,
      "ak_public_key_pem": ak_public_key_pem,
      "ak_tpmt_public_b64": ak_tpmt_public_b64,
      "hsm_type": hsm_type,
    }
    if ek_certificate_chain_pem:
      request_body["ek_certificate_chain_pem"] = ek_certificate_chain_pem
    if operator_email is not None:
      request_body["operator_email"] = operator_email
    if requested_handle is not None:
      request_body["requested_handle"] = requested_handle
    if display_name is not None:
      request_body["display_name"] = display_name

    return self._make_request("POST", "/api/v1/enroll/begin", json_body=request_body)

  def enroll_begin_piv(
    self,
    attestation_cert_pem: str,
    attestation_chain_pem: list[str],
    signing_key_public_pem: str,
    hsm_type: str = "yubikey",
    operator_email: str | None = None,
    requested_handle: str | None = None,
    display_name: str | None = None,
  ) -> dict[str, Any]:
    """Begin PIV-based enrollment (portable tier).

    This is the first step of the two-phase PIV enrollment flow. The server
    validates the attestation certificate chain against the Yubico Root CA,
    checks the anti-Sybil registry by device serial number, and returns a
    nonce challenge that the PIV key must sign to prove possession.

    Args:
        attestation_cert_pem: PEM-encoded slot attestation certificate from the PIV device.
        attestation_chain_pem: List of PEM-encoded intermediate CA certs (typically the F9 cert).
        signing_key_public_pem: PEM-encoded ECDSA public key of the PIV signing key (slot 9a).
        hsm_type: Type of PIV device ('yubikey').
        operator_email: Optional human operator contact email.
        requested_handle: Optional vanity handle to claim.
        display_name: Optional friendly name for the agent.

    Returns:
        Server response containing enrollment_session_id, nonce_challenge,
        trust_tier classification, device_serial, and session expiry.

    Raises:
        AlreadyEnrolledError: If this PIV device serial is already enrolled.
        HandleTakenError: If the requested handle is taken.
        EnrollmentError: If the attestation cert is invalid or chain is untrusted.
        NetworkError: If the server cannot be reached.
    """
    request_body: dict[str, Any] = {
      "attestation_cert_pem": attestation_cert_pem,
      "attestation_chain_pem": attestation_chain_pem,
      "signing_key_public_pem": signing_key_public_pem,
      "hsm_type": hsm_type,
    }
    if operator_email is not None:
      request_body["operator_email"] = operator_email
    if requested_handle is not None:
      request_body["requested_handle"] = requested_handle
    if display_name is not None:
      request_body["display_name"] = display_name

    return self._make_request("POST", "/api/v1/enroll/begin/piv", json_body=request_body)

  def enroll_activate(
    self,
    enrollment_session_id: str,
    decrypted_credential: str,
  ) -> dict[str, Any]:
    """Complete TPM/HSM-based enrollment by proving HSM possession.

    This is the second step. The agent sends back the decrypted credential
    challenge, proving the AK is inside the TPM/HSM that owns the EK.

    Args:
        enrollment_session_id: Session ID from enroll_begin().
        decrypted_credential: Base64-encoded decrypted challenge from the TPM/HSM.

    Returns:
        Server response containing identity, credentials, and initial tokens.
    """
    return self._make_request("POST", "/api/v1/enroll/activate", json_body={
      "enrollment_session_id": enrollment_session_id,
      "decrypted_credential": decrypted_credential,
    })

  def get_identity(self, agent_id: str) -> dict[str, Any]:
    """Look up public identity information for an agent.

    Args:
        agent_id: The agent's internal ID (e.g., '1id-a7b3c9d2').

    Returns:
        Public identity data (internal_id, handle, trust_tier, etc.).
    """
    return self._make_request("GET", f"/api/v1/identity/{agent_id}")

  def get_token_with_client_credentials(
    self,
    client_id: str,
    client_secret: str,
  ) -> dict[str, Any]:
    """Get an OAuth2 access token using the client_credentials grant.

    Args:
        client_id: The Keycloak client ID (e.g., '1id-a7b3c9d2').
        client_secret: The Keycloak client secret.

    Returns:
        Token response dict containing 'access_token', 'token_type',
        'expires_in', and optionally 'refresh_token'.

    Raises:
        NetworkError: If the token endpoint cannot be reached.
        EnrollmentError: If the token request fails.
    """
    token_url = f"{self.api_base_url}/realms/agents/protocol/openid-connect/token"

    try:
      with httpx.Client(timeout=self.timeout_seconds) as http_client:
        response = http_client.post(
          token_url,
          data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
          },
          headers={"User-Agent": USER_AGENT},
        )
        response.raise_for_status()
        return response.json()
    except httpx.ConnectError as connection_error:
      raise NetworkError(
        f"Could not connect to token endpoint {token_url}: {connection_error}"
      ) from connection_error
    except httpx.HTTPStatusError as status_error:
      raise EnrollmentError(
        f"Token request failed (HTTP {status_error.response.status_code}): "
        f"{status_error.response.text}"
      ) from status_error
    except httpx.HTTPError as http_error:
      raise NetworkError(
        f"HTTP error requesting token from {token_url}: {http_error}"
      ) from http_error

  def check_handle_availability(self, handle_name: str) -> dict[str, Any]:
    """Check whether a vanity handle is available.

    Args:
        handle_name: The handle to check (without '@' prefix).

    Returns:
        Dict with 'status' field: 'available', 'taken', or 'retired'.
    """
    return self._make_request("GET", f"/api/v1/handle/{handle_name}")
