import json
import logging
from typing import Optional
import httpx
from pydantic import ValidationError

from .models.sql_authorizer_models import (
    SQLAuthorizerRequest,
    SQLAuthorizerResponse,
)
from .plainid_exceptions import PlainIDSQLAuthorizerException

logger = logging.getLogger(__name__)


class PlainIDSQLAuthorizer:
    """Client for the PlainID SQL Database Authorizer API.

    Modifies an SQL query based on authorization policies.

    Authentication is performed using either client credentials (client_id +
    client_secret provided at construction time) or a per-request JWT token.
    At least one of client_secret or auth_token must be available when
    calling authorize_sql.

    Limitation:
        The SQL Authorizer does not currently support the runtime interface
        for primary and secondary identities (User + Agent). Only a single
        identity context is supported.
    """

    def __init__(
        self,
        base_url: str,
        client_id: str,
        client_secret: Optional[str] = None,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._client_id = client_id
        self._client_secret = client_secret

    def authorize_sql(
        self,
        request: SQLAuthorizerRequest,
        auth_token: Optional[str] = None,
    ) -> SQLAuthorizerResponse:
        """Submit a SQL query to the SQL Authorizer and receive the modified query.

        Args:
            request: The SQL authorization request containing the query and
                optional parameters (entity_id, entity_type_id, flags, etc.).
            auth_token: Optional JWT token for authentication. If provided,
                it is sent in the Authorization header. Either this or
                client_secret (from the constructor) must be present.

        Returns:
            SQLAuthorizerResponse with the modified SQL, modification flag,
            and any error message.

        Raises:
            PlainIDSQLAuthorizerException: On missing credentials, validation
                errors, HTTP errors, or unexpected failures.
        """
        if self._client_secret is None and auth_token is None:
            raise PlainIDSQLAuthorizerException(
                "Authentication required: provide either client_secret "
                "(at construction) or auth_token (per request)"
            )

        try:
            payload = self._build_payload(request)
            headers = self._build_headers(auth_token)

            logger.debug("SQL Authorizer request payload: %s", payload)

            response = httpx.post(
                f"{self._base_url}/resql",
                headers=headers,
                json=payload,
            )

            response.raise_for_status()

            response_data = response.json()

            logger.debug("SQL Authorizer response: %s", response_data)

            return SQLAuthorizerResponse.model_validate(response_data)
        except PlainIDSQLAuthorizerException:
            raise
        except ValidationError as e:
            logger.error("Validation error: %s", str(e))

            raise PlainIDSQLAuthorizerException(
                f"Invalid request or response data: {e}", e
            )
        except httpx.HTTPStatusError as e:
            logger.error("HTTP error response: %s", e.response.text)

            raise PlainIDSQLAuthorizerException(
                f"SQL Authorizer request failed with status {e.response.status_code}: "
                f"{e.response.text}",
                e,
            )
        except (json.JSONDecodeError, httpx.RequestError) as e:
            logger.error("Request/decode error: %s", str(e))

            raise PlainIDSQLAuthorizerException(
                f"Failed to process SQL Authorizer request: {e}", e
            )
        except Exception as e:
            logger.error("Unexpected error in authorize_sql: %s", str(e))

            raise PlainIDSQLAuthorizerException(
                "Unexpected error when calling SQL Authorizer", e
            )

    def _build_payload(self, request: SQLAuthorizerRequest) -> dict:
        payload = request.model_dump(by_alias=True, exclude_none=True)
        
        payload["clientID"] = self._client_id
        
        if self._client_secret is not None:
            payload["clientSecret"] = self._client_secret
        
        return payload

    def _build_headers(self, auth_token: Optional[str]) -> dict:
        headers = {"Content-Type": "application/json"}
        
        if auth_token is not None:
            if auth_token.lower().startswith("bearer "):
                headers["Authorization"] = auth_token
            else:
                headers["Authorization"] = f"Bearer {auth_token}"
        
        return headers
