"""Async HTTP client for the Propel code review API."""

from __future__ import annotations

import asyncio

import httpx

MAX_DIFF_BYTES = 1_000_000
MAX_REPOSITORY_LENGTH = 255
MAX_BASE_COMMIT_LENGTH = 255
MAX_RETRIES = 3
INITIAL_BACKOFF_SECONDS = 1.0

# Review job statuses — mirrors ReviewJobStatus in api/models/review_job.go
REVIEW_STATUS_QUEUED = "queued"
REVIEW_STATUS_RUNNING = "running"
REVIEW_STATUS_COMPLETED = "completed"
REVIEW_STATUS_FAILED = "failed"
REVIEW_TERMINAL_STATUSES = {REVIEW_STATUS_COMPLETED, REVIEW_STATUS_FAILED}


class PropelAPIError(Exception):
    """Base exception for Propel API errors."""

    def __init__(self, message: str, status_code: int | None = None):
        self.status_code = status_code
        super().__init__(message)


class PropelAuthError(PropelAPIError):
    """Raised for 401/403 authentication and authorization errors."""


class PropelValidationError(PropelAPIError):
    """Raised for 400 bad request errors."""


class PropelNotFoundError(PropelAPIError):
    """Raised for 404 not found errors."""


class PropelDiffTooLargeError(PropelAPIError):
    """Raised when the diff exceeds the 1 MB size limit."""


class PropelClient:
    def __init__(
        self,
        api_token: str,
        base_url: str = "https://api.propelcode.ai",
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers={
                "Authorization": f"Bearer {api_token}",
                "Content-Type": "application/json",
            },
            timeout=httpx.Timeout(30.0),
        )

    async def create_review(
        self,
        diff: str,
        repository: str,
        base_commit: str | None = None,
    ) -> dict:
        """Submit a new review job.

        Args:
            diff: Unified git diff content.
            repository: Repository identifier (name, full name, or URL).
            base_commit: Optional base commit SHA for additional context.

        Returns:
            Parsed ``ReviewJobResponse`` dict.

        Raises:
            PropelDiffTooLargeError: If the diff exceeds 1 MB.
            PropelValidationError: If the request is malformed.
            PropelAuthError: If the token is invalid or lacks scope.
            PropelNotFoundError: If the repository is not found.
            PropelAPIError: On server errors.
        """
        diff = diff.strip()
        repository = repository.strip()
        if base_commit:
            base_commit = base_commit.strip()

        if not diff:
            raise PropelValidationError("diff is required", status_code=400)

        if not repository:
            raise PropelValidationError("repository is required", status_code=400)

        diff_bytes = len(diff.encode("utf-8"))
        if diff_bytes > MAX_DIFF_BYTES:
            raise PropelDiffTooLargeError(
                f"Diff too large ({diff_bytes:,} bytes) — maximum size is 1 MB ({MAX_DIFF_BYTES:,} bytes)",
                status_code=413,
            )

        if len(repository) > MAX_REPOSITORY_LENGTH:
            raise PropelValidationError(
                f"Repository name too long — maximum is {MAX_REPOSITORY_LENGTH} characters",
                status_code=400,
            )

        if base_commit and len(base_commit) > MAX_BASE_COMMIT_LENGTH:
            raise PropelValidationError(
                f"Base commit too long — maximum is {MAX_BASE_COMMIT_LENGTH} characters",
                status_code=400,
            )

        body: dict = {"diff": diff, "repository": repository}
        if base_commit:
            body["base_commit"] = base_commit

        return await self._request("POST", "/v1/reviews", json=body)

    async def get_review(self, review_id: str) -> dict:
        """Get review job status and results.

        Args:
            review_id: Public review job identifier.

        Returns:
            Parsed ``ReviewJobResponse`` dict.

        Raises:
            PropelAuthError: If the token is invalid or lacks scope.
            PropelNotFoundError: If the review is not found.
            PropelAPIError: On server errors.
        """
        return await self._request("GET", f"/v1/reviews/{review_id}")

    async def aclose(self) -> None:
        await self._client.aclose()

    async def _request(
        self,
        method: str,
        path: str,
        **kwargs,
    ) -> dict:
        last_exception: Exception | None = None
        backoff = INITIAL_BACKOFF_SECONDS

        for attempt in range(1, MAX_RETRIES + 1):
            try:
                response = await self._client.request(method, path, **kwargs)
            except httpx.TimeoutException as exc:
                last_exception = PropelAPIError(
                    "Request timed out — please try again later",
                    status_code=None,
                )
                if attempt < MAX_RETRIES:
                    await asyncio.sleep(backoff)
                    backoff *= 2
                    continue
                raise last_exception from exc

            if response.status_code >= 500:
                last_exception = PropelAPIError(
                    "Propel API error — please try again later",
                    status_code=response.status_code,
                )
                if attempt < MAX_RETRIES:
                    await asyncio.sleep(backoff)
                    backoff *= 2
                    continue
                raise last_exception

            self._raise_for_status(response)
            return response.json()

        raise last_exception or PropelAPIError("Request failed after retries")

    @staticmethod
    def _raise_for_status(response: httpx.Response) -> None:
        if response.is_success:
            return

        error_message = ""
        try:
            body = response.json()
            error_message = body.get("error", "")
        except Exception:
            pass

        status = response.status_code

        if status == 400:
            raise PropelValidationError(
                error_message
                or "Invalid request — check your diff format and repository name",
                status_code=400,
            )

        if status == 401:
            raise PropelAuthError(
                error_message or "Missing or invalid authorization token",
                status_code=401,
            )

        if status == 403:
            raise PropelAuthError(
                error_message
                or "Authorization token has expired or lacks required scope",
                status_code=403,
            )

        if status == 404:
            raise PropelNotFoundError(
                error_message or "Review or repository not found",
                status_code=404,
            )

        if status == 413:
            raise PropelDiffTooLargeError(
                error_message or "Diff too large — maximum size is 1 MB",
                status_code=413,
            )

        raise PropelAPIError(
            error_message or f"Unexpected error (HTTP {status})",
            status_code=status,
        )
