"""
FDE SDK - Async client with JWT+HMAC authentication.

Load credentials from environment variables::

    import os
    from fde_sdk import FdeClient

    client = FdeClient(
        endpoint="https://fde.run-true.com",
        jwt_signing_key=os.environ["FDE_JWT_SIGNING_KEY"],
        hmac_key=os.environ["FDE_HMAC_KEY"],
        client_id=os.environ["FDE_CLIENT_ID"],
    )
"""

from __future__ import annotations

import hashlib
import hmac as _hmac
import json
import time
from typing import Any, Dict, Optional

import httpx
import jwt  # PyJWT

from .types import (
    EvaluateRequest,
    OutcomeRequest,
    OutcomeResponse,
    RiskEvaluateResponse,
    SubmitLabelsRequest,
    SubmitLabelsResponse,
)


class FdeApiError(Exception):
    """Raised when the FDE API returns a non-2xx status code."""

    def __init__(self, status_code: int, detail: Any) -> None:
        self.status_code = status_code
        self.detail = detail
        super().__init__(f"FDE API error {status_code}: {detail}")


class FdeClient:
    """
    Async FDE API client.

    Handles JWT generation, HMAC body signing, and typed methods for
    all three V1 risk endpoints.

    Use as an async context manager::

        async with FdeClient(...) as client:
            resp = await client.evaluate_risk(req)

    Or close manually::

        client = FdeClient(...)
        try:
            resp = await client.evaluate_risk(req)
        finally:
            await client.close()
    """

    def __init__(
        self,
        endpoint: str,
        jwt_signing_key: str,
        hmac_key: str,
        client_id: str,
        jwt_issuer: str = "fde",
        jwt_audience: str = "fde-risk-api",
        timeout: float = 30.0,
    ) -> None:
        """
        Create an FDE client.

        Args:
            endpoint: Base URL, e.g. ``"https://fde.run-true.com"``. Trailing
                      slash is stripped automatically.
            jwt_signing_key: HS256 secret for signing JWT tokens.
                             Load from ``FDE_JWT_SIGNING_KEY`` env var.
            hmac_key: Hex-encoded bytes for HMAC-SHA256 body signing.
                      Load from ``FDE_HMAC_KEY`` env var.
            client_id: JWT ``sub`` claim and HMAC client identifier.
            jwt_issuer: JWT ``iss`` claim. Default: ``"fde"``.
            jwt_audience: JWT ``aud`` claim. Default: ``"fde-risk-api"``.
            timeout: HTTP request timeout in seconds. Default: 30.
        """
        self._endpoint = endpoint.rstrip("/")
        self._jwt_signing_key = jwt_signing_key
        self._hmac_key_bytes = bytes.fromhex(hmac_key)
        self._client_id = client_id
        self._jwt_issuer = jwt_issuer
        self._jwt_audience = jwt_audience
        self._http: Optional[httpx.AsyncClient] = None
        self._timeout = timeout

    # ------------------------------------------------------------------
    # Context manager
    # ------------------------------------------------------------------

    async def __aenter__(self) -> "FdeClient":
        self._http = httpx.AsyncClient(timeout=self._timeout)
        return self

    async def __aexit__(self, *_: Any) -> None:
        await self.close()

    async def close(self) -> None:
        """Close the underlying HTTP session."""
        if self._http is not None:
            await self._http.aclose()
            self._http = None

    # ------------------------------------------------------------------
    # Auth helpers
    # ------------------------------------------------------------------

    def _build_jwt(self) -> str:
        """Generate a short-lived HS256 JWT (5-minute expiry)."""
        now = int(time.time())
        payload = {
            "iss": self._jwt_issuer,
            "aud": self._jwt_audience,
            "sub": self._client_id,
            "iat": now,
            "exp": now + 300,
        }
        return jwt.encode(payload, self._jwt_signing_key, algorithm="HS256")

    def _compute_hmac(self, timestamp: str, body_bytes: bytes) -> str:
        """
        HMAC-SHA256 over ``timestamp + newline + SHA256(body)``.

        Matches server algorithm in app/auth/hmac_auth.py exactly:

          body_hash   = SHA256(body_bytes).hexdigest()
          sign_string = timestamp + newline + body_hash
          signature   = HMAC-SHA256(key_bytes, sign_string).hexdigest()
        """
        body_hash = hashlib.sha256(body_bytes).hexdigest()
        sign_string = f"{timestamp}\n{body_hash}"
        return _hmac.new(
            self._hmac_key_bytes, sign_string.encode(), hashlib.sha256
        ).hexdigest()

    def _build_headers(self, body_bytes: bytes) -> Dict[str, str]:
        """Build Authorization, X-FDE-Timestamp, X-FDE-Signature, Content-Type."""
        timestamp = str(int(time.time()))
        bearer = self._build_jwt()
        signature = self._compute_hmac(timestamp, body_bytes)
        return {
            "Authorization": f"Bearer {bearer}",
            "X-FDE-Timestamp": timestamp,
            "X-FDE-Signature": signature,
            "Content-Type": "application/json",
        }

    # ------------------------------------------------------------------
    # Internal request dispatch
    # ------------------------------------------------------------------

    def _get_client(self) -> httpx.AsyncClient:
        if self._http is None:
            self._http = httpx.AsyncClient(timeout=self._timeout)
        return self._http

    async def _post(
        self,
        path: str,
        payload: Dict[str, Any],
        params: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Serialize payload to JSON bytes ONCE, sign with HMAC, then POST.

        CRITICAL: ``content=body_bytes`` is passed directly to httpx so
        the library does not re-serialize — the bytes signed and the bytes
        sent are identical.
        """
        body_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")
        headers = self._build_headers(body_bytes)
        url = f"{self._endpoint}{path}"

        response = await self._get_client().post(
            url,
            content=body_bytes,
            headers=headers,
            params=params,
        )

        if response.status_code >= 400:
            try:
                detail: Any = response.json()
            except Exception:
                detail = response.text
            raise FdeApiError(response.status_code, detail)

        return response.json()  # type: ignore[no-any-return]

    # ------------------------------------------------------------------
    # Public API methods
    # ------------------------------------------------------------------

    async def evaluate_risk(
        self,
        request: EvaluateRequest,
        explain: bool = False,
    ) -> RiskEvaluateResponse:
        """
        POST /v1/risk/evaluate - real-time fraud risk assessment.

        Args:
            request: The evaluate request payload.
            explain: When True the response includes a SHAP explanation.

        Returns:
            RiskEvaluateResponse dict. Guaranteed keys: ``request_id``,
            ``risk_score`` (int), ``risk_level`` (str), ``decision`` (str).
            Optional: ``banking_rule_details``, ``explanation``,
            ``risk_labels``, ``data_quality``, ``list_actions``.

        Raises:
            FdeApiError: On HTTP 4xx/5xx.
        """
        params: Dict[str, Any] = {}
        if explain:
            params["explain"] = "true"
        result = await self._post("/v1/risk/evaluate", request.to_dict(), params=params)
        return RiskEvaluateResponse(result)

    async def submit_outcome(self, request: OutcomeRequest) -> OutcomeResponse:
        """
        POST /v1/risk/outcome - record a transaction outcome.

        Args:
            request: The outcome request payload.

        Returns:
            OutcomeResponse dict with key ``request_id``.

        Raises:
            FdeApiError: On HTTP 4xx/5xx.
        """
        result = await self._post("/v1/risk/outcome", request.to_dict())
        return OutcomeResponse(result)

    async def submit_labels(self, request: SubmitLabelsRequest) -> SubmitLabelsResponse:
        """
        POST /v1/risk/labels - submit fraud labels for prior events.

        Args:
            request: Up to 100 label items.

        Returns:
            SubmitLabelsResponse dict. Keys: ``request_id``, ``created``,
            ``not_found``, ``errors``, ``results``.

        Raises:
            FdeApiError: On HTTP 4xx/5xx.
        """
        result = await self._post("/v1/risk/labels", request.to_dict())
        return SubmitLabelsResponse(result)
