"""
MorpheClient — synchronous HTTP client for emitting events to the Control Plane.
"""
from __future__ import annotations

import json
import os
from typing import Any, Dict, List, Optional

import httpx
import jsonschema

from .errors import ApiError, MorpheError, NetworkError, SchemaValidationError
from .hmac_utils import sign_body
from .retry import with_retry
from .types import EmitResult, EventPayload, JsonApiError, ValidationFailure
from .version import DEFAULT_ENDPOINT, extract_api_version

_DEFAULT_MAX_RETRIES = 3
_DEFAULT_BASE_DELAY = 0.5  # seconds


class MorpheClient:
    """
    Synchronous client for emitting events to the Morphe Control Plane.

    Supports:
    - Optional pre-emit JSON Schema validation (``validate_schema=True``)
    - HMAC-SHA256 request signing (when ``hmac_secret`` is provided)
    - Exponential backoff retry on 5xx / network errors
    - Structured JSON:API error surface (``ApiError``, ``SchemaValidationError``, ``NetworkError``)

    Example::

        with MorpheClient(token="...") as client:
            result = client.emit({
                "event_type": "dataops.v1.sync_failed",
                "system_id": "11111111-...",
                "payload": {"records": 5},
            })

    Args:
        token:           JWT Bearer token (contains ``company_id``; optionally ``credential_id``).
        endpoint:        Override the Control Plane base URL. Defaults to ``MORPHE_ENDPOINT``
                         env var, then the Morphe production endpoint. Only needed for local
                         development or self-hosted deployments.
        hmac_secret:     HMAC-SHA256 shared secret. Required when ``credential_id`` is in the JWT.
        max_retries:     Maximum retry attempts for transient failures. Default: 3.
        base_delay:      Base delay in seconds for exponential backoff. Default: 0.5.
        validate_schema: Validate payload against schema registry before emitting. Default: False.
    """

    def __init__(
        self,
        *,
        token: str,
        endpoint: Optional[str] = None,
        hmac_secret: Optional[str] = None,
        max_retries: int = _DEFAULT_MAX_RETRIES,
        base_delay: float = _DEFAULT_BASE_DELAY,
        validate_schema: bool = False,
    ) -> None:
        self.endpoint = (
            endpoint or os.environ.get("MORPHE_ENDPOINT") or DEFAULT_ENDPOINT
        ).rstrip("/")
        self.token = token
        self.hmac_secret = hmac_secret
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.validate_schema = validate_schema

        # In-memory cache: event_type → compiled JSON Schema dict
        self._schema_cache: Dict[str, Any] = {}
        self._http = httpx.Client(timeout=30.0)

    # ── Context manager ────────────────────────────────────────────────────────

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

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

    def close(self) -> None:
        """Close the underlying HTTP client and release connections."""
        self._http.close()

    # ── Public API ─────────────────────────────────────────────────────────────

    def emit(self, event: EventPayload) -> EmitResult:
        """
        Emit a single event to the Control Plane.

        - If ``validate_schema=True``, the payload is validated against the registered
          schema before any HTTP request is made. Raises ``SchemaValidationError`` on failure.
        - Retries on 5xx responses and network failures with exponential backoff.
        - Raises ``ApiError`` on non-retryable HTTP errors (4xx).
        - Raises ``NetworkError`` when the Control Plane cannot be reached.
        """
        if self.validate_schema:
            self._validate(event)

        return with_retry(
            lambda: self._do_request(event),
            max_retries=self.max_retries,
            base_delay=self.base_delay,
            should_retry=lambda err: (
                isinstance(err, NetworkError)
                or (isinstance(err, ApiError) and err.status >= 500)
            ),
        )

    # ── Private helpers ────────────────────────────────────────────────────────

    def _do_request(self, event: EventPayload) -> EmitResult:
        body = json.dumps(event, separators=(",", ":"))
        headers: Dict[str, str] = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.token}",
        }
        if self.hmac_secret:
            headers["X-Morphe-Signature"] = sign_body(body, self.hmac_secret)

        version = extract_api_version(event["event_type"])
        try:
            response = self._http.post(
                f"{self.endpoint}/api/{version}/events",
                content=body.encode("utf-8"),
                headers=headers,
            )
        except httpx.TransportError as exc:
            raise NetworkError("Failed to reach the Control Plane", exc)

        if response.status_code == 202:
            data = response.json()
            attrs = data["data"]["attributes"]
            return EmitResult(
                id=data["data"]["id"],
                received_at=attrs["received_at"],
                schema_valid=attrs["schema_valid"],
                schema_validation_errors=attrs.get("schema_validation_errors") or [],
            )

        errors = self._parse_errors(response)
        raise ApiError(response.status_code, errors)

    def _parse_errors(self, response: httpx.Response) -> List[JsonApiError]:
        try:
            body = response.json()
            return body.get("errors") or []
        except Exception:
            return [{"code": "parse_error", "title": "Could not parse error response"}]

    def _validate(self, event: EventPayload) -> None:
        """Validate event.payload against the cached or freshly-fetched JSON Schema."""
        event_type = event["event_type"]
        schema = self._schema_cache.get(event_type)

        if schema is None:
            schema = self._fetch_schema(event_type)
            self._schema_cache[event_type] = schema

        failures: List[ValidationFailure] = []
        validator = jsonschema.Draft7Validator(schema)
        for error in validator.iter_errors(event.get("payload", {})):
            field = "/" + "/".join(str(p) for p in error.absolute_path) if error.absolute_path else "/"
            failures.append({"field": field, "message": error.message})

        if failures:
            raise SchemaValidationError(failures)

    def _fetch_schema(self, event_type: str) -> Dict[str, Any]:
        """Fetch the JSON Schema for an event type from GET /api/{version}/schemas/{event_type}."""
        version = extract_api_version(event_type)
        url = f"{self.endpoint}/api/{version}/schemas/{event_type}"
        try:
            response = self._http.get(url, headers={"Authorization": f"Bearer {self.token}"})
        except httpx.TransportError as exc:
            raise NetworkError(f"Failed to fetch schema for {event_type}", exc)

        if not response.is_success:
            errors = self._parse_errors(response)
            raise ApiError(
                response.status_code,
                errors if errors else [
                    {"code": "schema_not_found", "title": f'No schema registered for event type "{event_type}"'}
                ],
            )

        data = response.json()
        return data["data"]["attributes"]["schema"]
