"""Kling video and image generation resource."""
from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING

from ..types.kling import (
    KlingVideoModel,
    KlingO3VideoModel,
    KlingVideoDuration,
    KlingO3VideoDuration,
    KlingVideoAspectRatio,
    KlingMotionDirection,
    KlingPhotoResolution,
    KlingPhotoAspectRatio,
    KlingStatus,
    KlingVideoGenerateParams,
    KlingO1VideoGenerateParams,
    KlingO3VideoGenerateParams,
    KlingO1PhotoGenerateParams,
    KlingO3PhotoGenerateParams,
    KlingGenerateResponse,
    KlingVideoStatusResponse,
    KlingPhotoGenerateResponse,
    KlingPhotoStatusResponse,
)
from .._errors import GenerationTimeoutError

if TYPE_CHECKING:
    from ..client import RunBlob

logger = logging.getLogger("runblob.kling")

_POLL_INITIAL_DELAY = 2.0
_POLL_MAX_DELAY = 10.0
_POLL_BACKOFF_FACTOR = 1.5
_POLL_DEFAULT_TIMEOUT = 600.0


class Kling:
    """Kling video and image generation.

    Available via ``client.kling``.
    """

    _client: RunBlob

    def __init__(self, client: RunBlob) -> None:
        self._client = client

    async def generate(
        self,
        *,
        prompt: str,
        model: KlingVideoModel | str = KlingVideoModel.KLING_2_5_TURBO,
        duration: KlingVideoDuration | str = KlingVideoDuration.FIVE,
        aspect_ratio: KlingVideoAspectRatio | str = KlingVideoAspectRatio.LANDSCAPE,
        negative_prompt: str | None = None,
        cfg_scale: float = 0.5,
        seed: int | None = None,
        image_url: str | None = None,
        image_base64: str | None = None,
        reference_video_url: str | None = None,
        reference_video_base64: str | None = None,
        reference_image_url: str | None = None,
        reference_image_base64: str | None = None,
        motion_direction: KlingMotionDirection | str = KlingMotionDirection.VIDEO,
        callback_url: str | None = None,
    ) -> KlingGenerateResponse:
        """Generate a video with Kling.

        Supports text-to-video, image-to-video, and motion control modes.

        Args:
            prompt: Text description (1-2500 chars).
            model: Kling model version (default "kling_2.5_turbo").
            duration: "5" or "10" seconds.
            aspect_ratio: "16:9", "9:16", or "1:1".
            negative_prompt: What to avoid in generation.
            cfg_scale: Creativity vs adherence 0.0-1.0 (default 0.5).
            seed: Reproducibility seed (0-2147483647).
            image_url: Input image URL for image-to-video.
            image_base64: Input image base64 for image-to-video.
            reference_video_url: Motion control reference video URL.
            reference_video_base64: Motion control reference video base64.
            reference_image_url: Motion control reference image URL.
            reference_image_base64: Motion control reference image base64.
            motion_direction: "video" or "image" for motion control.
            callback_url: Webhook URL for completion notification.

        Returns:
            KlingGenerateResponse with generation_id, status, price.
        """
        model_enum = KlingVideoModel(model) if isinstance(model, str) else model
        duration_enum = KlingVideoDuration(duration) if isinstance(duration, str) else duration
        ar_enum = KlingVideoAspectRatio(aspect_ratio) if isinstance(aspect_ratio, str) else aspect_ratio
        md_enum = KlingMotionDirection(motion_direction) if isinstance(motion_direction, str) else motion_direction

        params = KlingVideoGenerateParams(
            prompt=prompt,
            model=model_enum,
            duration=duration_enum,
            aspect_ratio=ar_enum,
            negative_prompt=negative_prompt,
            cfg_scale=cfg_scale,
            seed=seed,
            image_url=image_url,
            image_base64=image_base64,
            reference_video_url=reference_video_url,
            reference_video_base64=reference_video_base64,
            reference_image_url=reference_image_url,
            reference_image_base64=reference_image_base64,
            motion_direction=md_enum,
            callback_url=callback_url,
        )

        return await self._client.request(
            "POST",
            "/v1/kling/generate",
            response_model=KlingGenerateResponse,
            body=params.to_api_body(),
        )

    async def get_status(self, generation_id: str) -> KlingVideoStatusResponse:
        """Get video generation status.

        Args:
            generation_id: The ID returned from generate().

        Returns:
            KlingVideoStatusResponse with status, video_url on completion.
        """
        return await self._client.request(
            "GET",
            f"/v1/kling/generations/{generation_id}",
            response_model=KlingVideoStatusResponse,
        )

    async def wait_for_result(
        self,
        generation_id: str,
        *,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
        poll_interval: float = _POLL_INITIAL_DELAY,
        max_poll_interval: float = _POLL_MAX_DELAY,
        backoff_factor: float = _POLL_BACKOFF_FACTOR,
    ) -> KlingVideoStatusResponse:
        """Poll until video generation completes or fails.

        Args:
            generation_id: The ID to poll.
            timeout: Max seconds to wait (default 600).
            poll_interval: Initial delay between polls (default 2s).
            max_poll_interval: Max delay between polls (default 10s).
            backoff_factor: Multiply interval each iteration (default 1.5).

        Returns:
            KlingVideoStatusResponse with final status.

        Raises:
            GenerationTimeoutError: If timeout exceeded.
        """
        return await self._poll_video(
            generation_id, timeout=timeout, poll_interval=poll_interval,
            max_poll_interval=max_poll_interval, backoff_factor=backoff_factor,
        )

    async def generate_and_wait(
        self,
        *,
        prompt: str,
        model: KlingVideoModel | str = KlingVideoModel.KLING_2_5_TURBO,
        duration: KlingVideoDuration | str = KlingVideoDuration.FIVE,
        aspect_ratio: KlingVideoAspectRatio | str = KlingVideoAspectRatio.LANDSCAPE,
        negative_prompt: str | None = None,
        cfg_scale: float = 0.5,
        seed: int | None = None,
        image_url: str | None = None,
        image_base64: str | None = None,
        callback_url: str | None = None,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingVideoStatusResponse:
        """Generate video and wait for result.

        Returns:
            KlingVideoStatusResponse with final status and video_url.
        """
        task = await self.generate(
            prompt=prompt, model=model, duration=duration, aspect_ratio=aspect_ratio,
            negative_prompt=negative_prompt, cfg_scale=cfg_scale, seed=seed,
            image_url=image_url, image_base64=image_base64, callback_url=callback_url,
        )
        logger.info("Created video %s (price=%s), waiting...", task.generation_id, task.price)
        return await self.wait_for_result(task.generation_id, timeout=timeout)

    async def generate_o1_video(
        self,
        *,
        prompt: str,
        duration: KlingVideoDuration | str = KlingVideoDuration.FIVE,
        aspect_ratio: KlingVideoAspectRatio | str | None = None,
        keep_original_sound: bool = True,
        images_urls: list[str] | None = None,
        images_base64: list[str] | None = None,
        first_frame_image_url: str | None = None,
        first_frame_image_base64: str | None = None,
        end_frame_image_url: str | None = None,
        end_frame_image_base64: str | None = None,
        callback_url: str | None = None,
    ) -> KlingGenerateResponse:
        """Generate video with Kling O1.

        Supports text-to-video, image-to-video (up to 5 images),
        and first+end frame mode.

        Args:
            prompt: Text description (1-2500 chars).
            duration: "5" or "10" seconds.
            aspect_ratio: "16:9", "9:16", "1:1" (auto for first+end frame).
            keep_original_sound: Keep generated audio (default True).
            images_urls: Input image URLs for image-to-video (max 5).
            images_base64: Input image base64 for image-to-video (max 5).
            first_frame_image_url: First frame URL (requires end frame).
            first_frame_image_base64: First frame base64 (requires end frame).
            end_frame_image_url: End frame URL (requires first frame).
            end_frame_image_base64: End frame base64 (requires first frame).
            callback_url: Webhook URL.

        Returns:
            KlingGenerateResponse with generation_id, status, price.
        """
        duration_enum = KlingVideoDuration(duration) if isinstance(duration, str) else duration
        ar_enum = KlingVideoAspectRatio(aspect_ratio) if isinstance(aspect_ratio, str) and aspect_ratio else aspect_ratio

        params = KlingO1VideoGenerateParams(
            prompt=prompt,
            duration=duration_enum,
            aspect_ratio=ar_enum if isinstance(ar_enum, KlingVideoAspectRatio) else None,
            keep_original_sound=keep_original_sound,
            images_urls=images_urls,
            images_base64=images_base64,
            first_frame_image_url=first_frame_image_url,
            first_frame_image_base64=first_frame_image_base64,
            end_frame_image_url=end_frame_image_url,
            end_frame_image_base64=end_frame_image_base64,
            callback_url=callback_url,
        )

        return await self._client.request(
            "POST",
            "/v1/kling/o1-video/generate",
            response_model=KlingGenerateResponse,
            body=params.to_api_body(),
        )

    async def get_o1_video_status(self, generation_id: str) -> KlingVideoStatusResponse:
        """Get O1 video generation status."""
        return await self._client.request(
            "GET",
            f"/v1/kling/o1-video/generations/{generation_id}",
            response_model=KlingVideoStatusResponse,
        )

    async def wait_for_o1_video(
        self,
        generation_id: str,
        *,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingVideoStatusResponse:
        """Poll until O1 video generation completes."""
        return await self._poll_video(
            generation_id, endpoint_prefix="/v1/kling/o1-video", timeout=timeout,
        )

    async def generate_o1_video_and_wait(
        self,
        *,
        prompt: str,
        duration: KlingVideoDuration | str = KlingVideoDuration.FIVE,
        aspect_ratio: KlingVideoAspectRatio | str | None = None,
        keep_original_sound: bool = True,
        images_urls: list[str] | None = None,
        images_base64: list[str] | None = None,
        callback_url: str | None = None,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingVideoStatusResponse:
        """Generate O1 video and wait for result."""
        task = await self.generate_o1_video(
            prompt=prompt, duration=duration, aspect_ratio=aspect_ratio,
            keep_original_sound=keep_original_sound, images_urls=images_urls,
            images_base64=images_base64, callback_url=callback_url,
        )
        logger.info("Created O1 video %s (price=%s), waiting...", task.generation_id, task.price)
        return await self.wait_for_o1_video(task.generation_id, timeout=timeout)

    async def generate_o3_video(
        self,
        *,
        prompt: str,
        model: KlingO3VideoModel | str = KlingO3VideoModel.STANDARD,
        duration: KlingO3VideoDuration | str = KlingO3VideoDuration.FIVE,
        aspect_ratio: KlingVideoAspectRatio | str = KlingVideoAspectRatio.LANDSCAPE,
        keep_original_sound: bool = True,
        images_url: list[str] | None = None,
        images_base64: list[str] | None = None,
        first_frame_url: str | None = None,
        first_frame_base64: str | None = None,
        end_frame_url: str | None = None,
        end_frame_base64: str | None = None,
        customize_multi_shots: bool = False,
        prefer_multi_shots: bool = False,
        callback_url: str | None = None,
    ) -> KlingGenerateResponse:
        """Generate video with Kling O3.

        Supports 15s duration and multi-shot generation.

        Args:
            prompt: Text description (1-2500 chars).
            model: "kling_o3" or "kling_o3_pro".
            duration: "5", "10", or "15" seconds.
            aspect_ratio: "16:9", "9:16", or "1:1".
            keep_original_sound: Keep generated audio (default True).
            images_url: Input image URLs (max 5).
            images_base64: Input image base64 (max 5).
            first_frame_url: First frame URL (requires end frame).
            first_frame_base64: First frame base64 (requires end frame).
            end_frame_url: End frame URL (requires first frame).
            end_frame_base64: End frame base64 (requires first frame).
            customize_multi_shots: Enable custom multi-shot.
            prefer_multi_shots: Prefer multi-shot output.
            callback_url: Webhook URL.

        Returns:
            KlingGenerateResponse with generation_id, status, price.
        """
        model_enum = KlingO3VideoModel(model) if isinstance(model, str) else model
        duration_enum = KlingO3VideoDuration(duration) if isinstance(duration, str) else duration
        ar_enum = KlingVideoAspectRatio(aspect_ratio) if isinstance(aspect_ratio, str) else aspect_ratio

        params = KlingO3VideoGenerateParams(
            prompt=prompt, model=model_enum, duration=duration_enum, aspect_ratio=ar_enum,
            keep_original_sound=keep_original_sound, images_url=images_url,
            images_base64=images_base64, first_frame_url=first_frame_url,
            first_frame_base64=first_frame_base64, end_frame_url=end_frame_url,
            end_frame_base64=end_frame_base64, customize_multi_shots=customize_multi_shots,
            prefer_multi_shots=prefer_multi_shots, callback_url=callback_url,
        )

        return await self._client.request(
            "POST",
            "/v1/kling/o3-video/generate",
            response_model=KlingGenerateResponse,
            body=params.to_api_body(),
        )

    async def get_o3_video_status(self, generation_id: str) -> KlingVideoStatusResponse:
        """Get O3 video generation status."""
        return await self._client.request(
            "GET",
            f"/v1/kling/o3-video/generations/{generation_id}",
            response_model=KlingVideoStatusResponse,
        )

    async def wait_for_o3_video(
        self,
        generation_id: str,
        *,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingVideoStatusResponse:
        """Poll until O3 video generation completes."""
        return await self._poll_video(
            generation_id, endpoint_prefix="/v1/kling/o3-video", timeout=timeout,
        )

    async def generate_o3_video_and_wait(
        self,
        *,
        prompt: str,
        model: KlingO3VideoModel | str = KlingO3VideoModel.STANDARD,
        duration: KlingO3VideoDuration | str = KlingO3VideoDuration.FIVE,
        aspect_ratio: KlingVideoAspectRatio | str = KlingVideoAspectRatio.LANDSCAPE,
        keep_original_sound: bool = True,
        callback_url: str | None = None,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingVideoStatusResponse:
        """Generate O3 video and wait for result."""
        task = await self.generate_o3_video(
            prompt=prompt, model=model, duration=duration, aspect_ratio=aspect_ratio,
            keep_original_sound=keep_original_sound, callback_url=callback_url,
        )
        logger.info("Created O3 video %s (price=%s), waiting...", task.generation_id, task.price)
        return await self.wait_for_o3_video(task.generation_id, timeout=timeout)

    async def generate_o1_photo(
        self,
        *,
        prompt: str,
        img_resolution: KlingPhotoResolution | str = KlingPhotoResolution.RES_2K,
        aspect_ratio: KlingPhotoAspectRatio | str = KlingPhotoAspectRatio.SQUARE,
        images_url: list[str] | None = None,
        images_base64: list[str] | None = None,
        callback_url: str | None = None,
    ) -> KlingPhotoGenerateResponse:
        """Generate image with Kling O1 Photo (Kolors).

        Args:
            prompt: Text description (1-2500 chars).
            img_resolution: "1k" or "2k" (default "2k").
            aspect_ratio: One of 8 ratios (default "1:1").
            images_url: Reference image URLs (max 10).
            images_base64: Reference image base64 (max 10).
            callback_url: Webhook URL.

        Returns:
            KlingPhotoGenerateResponse with generation_id, status, price.
        """
        res_enum = KlingPhotoResolution(img_resolution) if isinstance(img_resolution, str) else img_resolution
        ar_enum = KlingPhotoAspectRatio(aspect_ratio) if isinstance(aspect_ratio, str) else aspect_ratio

        params = KlingO1PhotoGenerateParams(
            prompt=prompt, img_resolution=res_enum, aspect_ratio=ar_enum,
            images_url=images_url, images_base64=images_base64, callback_url=callback_url,
        )

        return await self._client.request(
            "POST",
            "/v1/kling/o1-photo/generate",
            response_model=KlingPhotoGenerateResponse,
            body=params.to_api_body(),
        )

    async def get_o1_photo_status(self, generation_id: str) -> KlingPhotoStatusResponse:
        """Get O1 photo generation status."""
        return await self._client.request(
            "GET",
            f"/v1/kling/o1-photo/generations/{generation_id}",
            response_model=KlingPhotoStatusResponse,
        )

    async def wait_for_o1_photo(
        self,
        generation_id: str,
        *,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingPhotoStatusResponse:
        """Poll until O1 photo generation completes."""
        return await self._poll_photo(
            generation_id, endpoint_prefix="/v1/kling/o1-photo", timeout=timeout,
        )

    async def generate_o1_photo_and_wait(
        self,
        *,
        prompt: str,
        img_resolution: KlingPhotoResolution | str = KlingPhotoResolution.RES_2K,
        aspect_ratio: KlingPhotoAspectRatio | str = KlingPhotoAspectRatio.SQUARE,
        images_url: list[str] | None = None,
        images_base64: list[str] | None = None,
        callback_url: str | None = None,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingPhotoStatusResponse:
        """Generate O1 photo and wait for result."""
        task = await self.generate_o1_photo(
            prompt=prompt, img_resolution=img_resolution, aspect_ratio=aspect_ratio,
            images_url=images_url, images_base64=images_base64, callback_url=callback_url,
        )
        logger.info("Created O1 photo %s (price=%s), waiting...", task.generation_id, task.price)
        return await self.wait_for_o1_photo(task.generation_id, timeout=timeout)

    async def generate_o3_photo(
        self,
        *,
        prompt: str,
        img_resolution: KlingPhotoResolution | str = KlingPhotoResolution.RES_4K,
        aspect_ratio: KlingPhotoAspectRatio | str = KlingPhotoAspectRatio.SQUARE,
        prefer_multi_shots: bool = False,
        images_url: list[str] | None = None,
        images_base64: list[str] | None = None,
        callback_url: str | None = None,
    ) -> KlingPhotoGenerateResponse:
        """Generate image with Kling O3 Photo.

        Supports 4K resolution and multi-shot generation.

        Args:
            prompt: Text description (1-2500 chars).
            img_resolution: "1k", "2k", or "4k" (default "4k").
            aspect_ratio: One of 9 ratios including "auto" (default "1:1").
            prefer_multi_shots: Prefer multi-shot output.
            images_url: Reference image URLs (max 10).
            images_base64: Reference image base64 (max 10).
            callback_url: Webhook URL.

        Returns:
            KlingPhotoGenerateResponse with generation_id, status, price.
        """
        res_enum = KlingPhotoResolution(img_resolution) if isinstance(img_resolution, str) else img_resolution
        ar_enum = KlingPhotoAspectRatio(aspect_ratio) if isinstance(aspect_ratio, str) else aspect_ratio

        params = KlingO3PhotoGenerateParams(
            prompt=prompt, img_resolution=res_enum, aspect_ratio=ar_enum,
            prefer_multi_shots=prefer_multi_shots, images_url=images_url,
            images_base64=images_base64, callback_url=callback_url,
        )

        return await self._client.request(
            "POST",
            "/v1/kling/o3-photo/generate",
            response_model=KlingPhotoGenerateResponse,
            body=params.to_api_body(),
        )

    async def get_o3_photo_status(self, generation_id: str) -> KlingPhotoStatusResponse:
        """Get O3 photo generation status."""
        return await self._client.request(
            "GET",
            f"/v1/kling/o3-photo/generations/{generation_id}",
            response_model=KlingPhotoStatusResponse,
        )

    async def wait_for_o3_photo(
        self,
        generation_id: str,
        *,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingPhotoStatusResponse:
        """Poll until O3 photo generation completes."""
        return await self._poll_photo(
            generation_id, endpoint_prefix="/v1/kling/o3-photo", timeout=timeout,
        )

    async def generate_o3_photo_and_wait(
        self,
        *,
        prompt: str,
        img_resolution: KlingPhotoResolution | str = KlingPhotoResolution.RES_4K,
        aspect_ratio: KlingPhotoAspectRatio | str = KlingPhotoAspectRatio.SQUARE,
        prefer_multi_shots: bool = False,
        images_url: list[str] | None = None,
        images_base64: list[str] | None = None,
        callback_url: str | None = None,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
    ) -> KlingPhotoStatusResponse:
        """Generate O3 photo and wait for result."""
        task = await self.generate_o3_photo(
            prompt=prompt, img_resolution=img_resolution, aspect_ratio=aspect_ratio,
            prefer_multi_shots=prefer_multi_shots, images_url=images_url,
            images_base64=images_base64, callback_url=callback_url,
        )
        logger.info("Created O3 photo %s (price=%s), waiting...", task.generation_id, task.price)
        return await self.wait_for_o3_photo(task.generation_id, timeout=timeout)

    async def generate_batch(
        self,
        prompts: list[str],
        *,
        model: KlingVideoModel | str = KlingVideoModel.KLING_2_5_TURBO,
        duration: KlingVideoDuration | str = KlingVideoDuration.FIVE,
        aspect_ratio: KlingVideoAspectRatio | str = KlingVideoAspectRatio.LANDSCAPE,
        concurrency: int = 5,
    ) -> list[KlingGenerateResponse]:
        """Submit multiple video generations concurrently.

        Args:
            prompts: List of text prompts.
            model: Model for all generations.
            duration: Duration for all.
            aspect_ratio: Aspect ratio for all.
            concurrency: Max concurrent requests (default 5).

        Returns:
            List of KlingGenerateResponse in same order as prompts.
        """
        sem = asyncio.Semaphore(concurrency)

        async def _gen(prompt: str) -> KlingGenerateResponse:
            async with sem:
                return await self.generate(
                    prompt=prompt, model=model, duration=duration, aspect_ratio=aspect_ratio,
                )

        return list(await asyncio.gather(*[_gen(p) for p in prompts]))

    async def list(
        self,
        *,
        limit: int = 50,
        offset: int = 0,
    ) -> list[KlingVideoStatusResponse]:
        """List recent video generations.

        Args:
            limit: Max results, 1-100 (default 50).
            offset: Skip N results for pagination (default 0).

        Returns:
            List of KlingVideoStatusResponse objects.
        """
        response = await self._client.request_raw(
            "GET",
            "/v1/kling/generations",
            params={"limit": limit, "offset": offset},
        )
        items = response.json()
        return [KlingVideoStatusResponse.model_validate(item) for item in items]

    async def _poll_video(
        self,
        generation_id: str,
        *,
        endpoint_prefix: str = "/v1/kling",
        timeout: float = _POLL_DEFAULT_TIMEOUT,
        poll_interval: float = _POLL_INITIAL_DELAY,
        max_poll_interval: float = _POLL_MAX_DELAY,
        backoff_factor: float = _POLL_BACKOFF_FACTOR,
    ) -> KlingVideoStatusResponse:
        deadline = asyncio.get_event_loop().time() + timeout
        delay = poll_interval

        while True:
            status = await self._client.request(
                "GET",
                f"{endpoint_prefix}/generations/{generation_id}",
                response_model=KlingVideoStatusResponse,
            )

            if status.is_terminal:
                logger.info("Video %s finished: %s", generation_id, status.status.value)
                return status

            remaining = deadline - asyncio.get_event_loop().time()
            if remaining <= 0:
                raise GenerationTimeoutError(generation_id, timeout)

            actual_delay = min(delay, remaining)
            logger.debug("Video %s status=%s, polling in %.1fs", generation_id, status.status.value, actual_delay)
            await asyncio.sleep(actual_delay)
            delay = min(delay * backoff_factor, max_poll_interval)

    async def _poll_photo(
        self,
        generation_id: str,
        *,
        endpoint_prefix: str,
        timeout: float = _POLL_DEFAULT_TIMEOUT,
        poll_interval: float = _POLL_INITIAL_DELAY,
        max_poll_interval: float = _POLL_MAX_DELAY,
        backoff_factor: float = _POLL_BACKOFF_FACTOR,
    ) -> KlingPhotoStatusResponse:
        deadline = asyncio.get_event_loop().time() + timeout
        delay = poll_interval

        while True:
            status = await self._client.request(
                "GET",
                f"{endpoint_prefix}/generations/{generation_id}",
                response_model=KlingPhotoStatusResponse,
            )

            if status.is_terminal:
                logger.info("Photo %s finished: %s", generation_id, status.status.value)
                return status

            remaining = deadline - asyncio.get_event_loop().time()
            if remaining <= 0:
                raise GenerationTimeoutError(generation_id, timeout)

            actual_delay = min(delay, remaining)
            logger.debug("Photo %s status=%s, polling in %.1fs", generation_id, status.status.value, actual_delay)
            await asyncio.sleep(actual_delay)
            delay = min(delay * backoff_factor, max_poll_interval)
