"""Main HaxClient class for interacting with the HAX API."""

from __future__ import annotations

import contextlib
import time
from typing import TYPE_CHECKING, Any

from hax.http import HttpClient
from hax.models.delivery import DeliveryConfig
from hax.models.form_request import FormRequestResponse, FormRequestStatus
from hax.models.request import CreatedRequest, Request, RequestSummary

DEFAULT_BASE_URL = "https://hax-sdk-production.up.railway.app/api/v1"


class HaxClient:
    """Client for interacting with the HAX API.

    Example:
        >>> from hax import HaxClient
        >>>
        >>> client = HaxClient(api_key="hax_live_...")
        >>> request = client.create_request(
        ...     type="text-approval-v1",
        ...     payload={"text": "Approve deployment?"},
        ...     webhook_url="https://myapp.com/webhook",
        ... )
        >>> print(f"Share this URL: {request.url}")

    With encryption:
        >>> client = HaxClient(
        ...     api_key="hax_live_...",
        ...     encryption_key="my-secret-passphrase",
        ... )
        >>> # Public key is automatically sent with requests
        >>> # Responses are automatically decrypted when retrieved
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        webhook_url: str | None = None,
        timeout: float = 30.0,
        encryption_key: str | None = None,
        public_key: dict[str, Any] | None = None,
    ) -> None:
        """Initialize the HAX client.

        Args:
            api_key: Your HAX API key (e.g., "hax_live_...")
            base_url: Base URL for the HAX API. Defaults to production.
            webhook_url: Default webhook URL for all requests. Can be overridden per-request.
            timeout: Request timeout in seconds.
            encryption_key: Optional encryption passphrase for end-to-end response encryption.
                If provided, a deterministic RSA keypair is generated from this passphrase.
                The public key is sent to the server with each request, and the server
                will encrypt user responses with it. Responses are automatically decrypted
                when retrieved.
            public_key: Optional public key for end-to-end response encryption.
                Use this when you want to provide your own RSA keypair instead of
                deriving one from a passphrase. When using this option, responses
                will NOT be automatically decrypted - use decrypt_response() manually
                with your private key.
        """
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._default_webhook_url = webhook_url
        self._http = HttpClient(api_key=api_key, base_url=base_url, timeout=timeout)

        # Set up encryption if key provided
        self._key_pair: tuple[dict[str, Any], dict[str, Any]] | None = None
        self._public_key_only: dict[str, Any] | None = None

        if encryption_key is not None:
            from hax.crypto import generate_key_pair

            self._key_pair = generate_key_pair(encryption_key)
        elif public_key is not None:
            self._public_key_only = public_key

    @property
    def public_key(self) -> dict[str, Any] | None:
        """Get the public key for this client (if encryption is enabled).

        Useful for verifying the public key that will be sent to the server.
        """
        if self._key_pair:
            return self._key_pair[0]
        return self._public_key_only

    @property
    def private_key(self) -> dict[str, Any] | None:
        """Get the private key for this client (if derived from passphrase).

        Useful for manual decryption or storing for later use.
        Returns None if using user-provided public key only.
        """
        if self._key_pair:
            return self._key_pair[1]
        return None

    def create_request(
        self,
        type: str,
        payload: dict[str, Any],
        *,
        title: str | None = None,
        description: str | None = None,
        webhook_url: str | None = None,
        expires_in_seconds: int | None = None,
        metadata: dict[str, Any] | None = None,
        delivery: DeliveryConfig | None = None,
        user_id: str | None = None,
    ) -> CreatedRequest:
        """Create a new human input request.

        Args:
            type: The template type (e.g., "text-approval-v1", "collect-email-v1").
            payload: Template-specific payload. See HAX docs for schema.
            title: Optional title displayed to the user.
            description: Optional description displayed to the user.
            webhook_url: URL to receive webhook notifications. Overrides client default.
            expires_in_seconds: Optional expiration time in seconds.
            metadata: Optional metadata dict to be returned in webhooks.
            delivery: Optional delivery config to send the request URL via email/SMS.

        Returns:
            The created request with id, url, status, and optional delivery result.

        Example:
            >>> request = client.create_request(
            ...     type="text-approval-v1",
            ...     payload={"text": "Deploy to production?", "approveLabel": "Ship It!"},
            ...     webhook_url="https://myapp.com/webhook",
            ...     expires_in_seconds=3600,
            ...     metadata={"pr_number": 123},
            ... )
            >>> print(f"Send this URL: {request.url}")
        """
        body: dict[str, Any] = {
            "type": type,
            "payload": payload,
        }

        if title is not None:
            body["title"] = title
        if description is not None:
            body["description"] = description

        # Use request-level webhook_url, fall back to client default
        effective_webhook_url = webhook_url or self._default_webhook_url
        if effective_webhook_url is not None:
            body["webhookUrl"] = effective_webhook_url

        if expires_in_seconds is not None:
            body["expiresInSeconds"] = expires_in_seconds
        if metadata is not None:
            body["metadata"] = metadata

        if delivery is not None:
            body["delivery"] = delivery.model_dump(exclude_none=True)
        if user_id is not None:
            body["userId"] = user_id

        # Include public key for response encryption
        public_key = self.public_key
        if public_key is not None:
            body["publicKey"] = public_key

        data = self._http.post("/requests", json=body)
        return CreatedRequest.model_validate(data)

    def request_via_email(
        self,
        type: str,
        payload: dict[str, Any],
        *,
        to_email: str,
        subject: str | None = None,
        message: str | None = None,
        **kwargs: Any,
    ) -> CreatedRequest:
        """Create a request and send it via email.

        Args:
            type: The template type.
            payload: Template-specific payload.
            to_email: The recipient email address.
            subject: Optional custom email subject.
            message: Optional custom message in the email.
            **kwargs: Additional arguments passed to create_request.

        Returns:
            The created request with delivery status.

        Example:
            >>> request = client.request_via_email(
            ...     type="confirm-action-v1",
            ...     payload={"title": "Approve?", "confirmText": "Yes"},
            ...     to_email="manager@company.com",
            ...     subject="Approval Needed",
            ... )
        """
        return self.create_request(
            type=type,
            payload=payload,
            delivery=DeliveryConfig(
                channel="email",
                recipient=to_email,
                subject=subject,
                message=message,
            ),
            **kwargs,
        )

    def request_via_sms(
        self,
        type: str,
        payload: dict[str, Any],
        *,
        to_phone: str,
        message: str | None = None,
        **kwargs: Any,
    ) -> CreatedRequest:
        """Create a request and send it via SMS.

        Args:
            type: The template type.
            payload: Template-specific payload.
            to_phone: The recipient phone number in E.164 format (e.g., +15551234567).
            message: Optional custom message in the SMS.
            **kwargs: Additional arguments passed to create_request.

        Returns:
            The created request with delivery status.

        Example:
            >>> request = client.request_via_sms(
            ...     type="confirm-action-v1",
            ...     payload={"title": "Confirm?", "confirmText": "Yes"},
            ...     to_phone="+15551234567",
            ... )
        """
        return self.create_request(
            type=type,
            payload=payload,
            delivery=DeliveryConfig(
                channel="sms",
                recipient=to_phone,
                message=message,
            ),
            **kwargs,
        )

    def configure_messaging(
        self,
        *,
        email: dict[str, Any] | None = None,
        sms: dict[str, Any] | None = None,
    ) -> None:
        """Configure messaging providers for the project.

        Args:
            email: Email provider configuration.
                Example: {"provider": "sendgrid", "apiKey": "SG.xxx",
                         "fromEmail": "noreply@example.com", "fromName": "My App"}
            sms: SMS provider configuration.
                Example: {"provider": "twilio", "accountSid": "ACxxx",
                         "authToken": "xxx", "fromNumber": "+15551234567"}

        Example:
            >>> client.configure_messaging(
            ...     email={
            ...         "provider": "sendgrid",
            ...         "apiKey": "SG.xxx",
            ...         "fromEmail": "noreply@example.com",
            ...     },
            ... )
        """
        body: dict[str, Any] = {"messaging": {}}
        if email is not None:
            body["messaging"]["email"] = email
        if sms is not None:
            body["messaging"]["sms"] = sms

        self._http.patch("/projects/settings", json=body)

    def get_request(self, request_id: str) -> Request:
        """Get a request by ID.

        Args:
            request_id: The request ID.

        Returns:
            The Request object with full details.
            If encryption_key was provided to the client, encrypted response fields
            are automatically decrypted.
        """
        data = self._http.get(f"/requests/{request_id}")
        request_data = data.get("request", data)

        # Auto-decrypt response if we have a private key
        if self.private_key is not None:
            request_data = self._decrypt_response_field(request_data)

        request = Request.model_validate(request_data)
        request.set_base_url(self._base_url)
        return request

    def list_requests(self) -> list[RequestSummary]:
        """List recent requests for the project.

        Returns:
            List of RequestSummary objects (latest 25).
            If encryption_key was provided, encrypted response fields are decrypted.
        """
        data = self._http.get("/requests")
        requests_data = data.get("requests", [])

        # Auto-decrypt each request's response if we have a private key
        if self.private_key is not None:
            requests_data = [self._decrypt_response_field(r) for r in requests_data]

        return [RequestSummary.model_validate(r) for r in requests_data]

    def cancel_request(self, request_id: str) -> Request:
        """Cancel a pending request.

        Args:
            request_id: The request ID to cancel.

        Returns:
            The updated Request object.
        """
        data = self._http.patch(f"/requests/{request_id}", json={"status": "cancelled"})
        request_data = data.get("request", data)
        request = Request.model_validate(request_data)
        request.set_base_url(self._base_url)
        return request

    def submit_response(
        self,
        request_id: str,
        response: dict[str, Any],
    ) -> Request:
        """Submit a response to a request (for testing/internal use).

        Args:
            request_id: The request ID.
            response: The response data matching the template type.

        Returns:
            The updated Request object.
        """
        data = self._http.post(
            f"/requests/{request_id}/response", json={"response": response}
        )
        request_data = data.get("request", data)
        request = Request.model_validate(request_data)
        request.set_base_url(self._base_url)
        return request

    def list_types(self) -> list[dict[str, str]]:
        """List available template types.

        Returns:
            List of dicts with "name" and "description" keys.
        """
        data = self._http.get("/types")
        return data.get("types", [])  # type: ignore[no-any-return]

    def wait_for_response(
        self,
        request_id: str,
        poll_interval: float = 2.0,
        timeout: float | None = None,
    ) -> Request:
        """Poll until a request is completed, expired, or cancelled.

        Args:
            request_id: The request ID to poll.
            poll_interval: Seconds between polls. Default 2.0.
            timeout: Optional timeout in seconds. None means no timeout.

        Returns:
            The completed Request object.

        Raises:
            TimeoutError: If timeout is reached before completion.
        """
        start_time = time.time()

        while True:
            request = self.get_request(request_id)

            if request.is_completed or request.is_expired or request.is_cancelled:
                return request

            if timeout is not None:
                elapsed = time.time() - start_time
                if elapsed >= timeout:
                    raise TimeoutError(
                        f"Request {request_id} did not complete within {timeout} seconds"
                    )

            time.sleep(poll_interval)

    def create_form_request(
        self,
        form: FormBuilder,
        *,
        title: str | None = None,
        description: str | None = None,
        webhook_url: str | None = None,
        expires_in_seconds: int | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> FormRequestHandle:
        """Create a form request with typed responses.

        Uses the FormBuilder to create a form-builder request and returns a
        typed handle for waiting and retrieving responses.

        Args:
            form: A FormBuilder instance defining the form fields.
            title: Optional title displayed to the user.
            description: Optional description displayed to the user.
            webhook_url: URL to receive webhook notifications.
            expires_in_seconds: Optional expiration time in seconds.
            metadata: Optional metadata dict to be returned in webhooks.

        Returns:
            A FormRequestHandle for waiting and retrieving typed responses.

        Example:
            >>> from hax.form_builder import FormBuilder
            >>>
            >>> form = (FormBuilder()
            ...     .title("Registration")
            ...     .input("email", variant="email", required=True)
            ...     .number("age", min=0, max=120)
            ...     .checkbox("newsletter", checkbox_label="Subscribe"))
            >>>
            >>> handle = client.create_form_request(form,
            ...     webhook_url="https://myapp.com/webhook")
            >>>
            >>> print(f"Form URL: {handle.url}")
            >>>
            >>> response = handle.wait_for_response()
            >>> print(response.values.email)  # str
            >>> print(response.values.age)    # float
        """
        created = self.create_request(
            type="form-builder",
            payload=form.to_payload(),
            title=title,
            description=description,
            webhook_url=webhook_url,
            expires_in_seconds=expires_in_seconds,
            metadata=metadata,
        )
        return FormRequestHandle(self, created.id, created.url, form)

    def _decrypt_response_field(self, data: dict[str, Any]) -> dict[str, Any]:
        """Decrypt response field if encrypted.

        Handles two encryption formats:
        1. Legacy: response = { _encrypted: "..." } (entire response encrypted)
        2. New: response = { values: { _encrypted: "..." }, ... } (only values encrypted)

        Args:
            data: Request data dict from API.

        Returns:
            Data dict with decrypted response field.
        """
        from hax.crypto import decrypt_response, is_encrypted_response

        if self.private_key is None:
            return data

        # Make a copy to avoid mutating the original
        data = dict(data)
        response = data.get("response")

        if response is None:
            return data

        # Format 1: Entire response is encrypted { _encrypted: "..." }
        if is_encrypted_response(response):
            data["response"] = decrypt_response(
                response["_encrypted"], self.private_key
            )
        # Format 2: Only values are encrypted { values: { _encrypted: "..." }, ... }
        elif isinstance(response, dict) and is_encrypted_response(response.get("values")):
            response = dict(response)  # Copy response
            response["values"] = decrypt_response(
                response["values"]["_encrypted"], self.private_key
            )
            data["response"] = response

        return data

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

    def __enter__(self) -> HaxClient:
        return self

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


class FormRequestHandle:
    """Handle for a form request with typed responses.

    Provides methods to wait for and retrieve typed form responses.
    """

    def __init__(
        self,
        client: HaxClient,
        request_id: str,
        url: str,
        form: FormBuilder,
    ) -> None:
        """Initialize the handle.

        Args:
            client: The HaxClient instance.
            request_id: The request ID.
            url: The URL for the human to respond.
            form: The FormBuilder instance for parsing responses.
        """
        self._client = client
        self.id = request_id
        self.url = url
        self._form = form

    def wait_for_response(
        self,
        poll_interval: float = 2.0,
        timeout: float | None = None,
    ) -> FormRequestResponse[Any]:
        """Wait for the form to be completed.

        Polls until completed, expired, or cancelled.

        Args:
            poll_interval: Seconds between polls. Default 2.0.
            timeout: Optional timeout in seconds.

        Returns:
            FormRequestResponse with typed values.

        Raises:
            TimeoutError: If timeout is reached before completion.
        """
        from datetime import datetime

        result = self._client.wait_for_response(
            self.id,
            poll_interval=poll_interval,
            timeout=timeout,
        )

        response_data = result.response or {}
        meta = response_data.get("meta", {})
        submitted_at_str = meta.get("submittedAt")
        submitted_at = None
        if submitted_at_str:
            with contextlib.suppress(ValueError, AttributeError):
                submitted_at = datetime.fromisoformat(submitted_at_str.replace("Z", "+00:00"))

        return FormRequestResponse(
            values=self._form.parse_response(result.response),
            submitted_at=submitted_at,
            responded_by=result.responded_by,
            responded_at=result.responded_at,
        )

    def get_status(self) -> FormRequestStatus[Any]:
        """Get the current status of the form request.

        Returns:
            FormRequestStatus with status and optional typed response.
        """
        from datetime import datetime

        result = self._client.get_request(self.id)
        completed = result.is_completed

        response = None
        if completed and result.response:
            response_data = result.response or {}
            meta = response_data.get("meta", {})
            submitted_at_str = meta.get("submittedAt")
            submitted_at = None
            if submitted_at_str:
                with contextlib.suppress(ValueError, AttributeError):
                    submitted_at = datetime.fromisoformat(submitted_at_str.replace("Z", "+00:00"))

            response = FormRequestResponse(
                values=self._form.parse_response(result.response),
                submitted_at=submitted_at,
                responded_by=result.responded_by,
                responded_at=result.responded_at,
            )

        return FormRequestStatus(
            status=result.status.value,
            completed=completed,
            response=response,
        )


if TYPE_CHECKING:
    from hax.form_builder import FormBuilder
