from __future__ import annotations

from base64 import b64decode
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal

from ..._endpoints import Images, Tasks
from ..._enums import ImageSize
from ..._internal._builders.images import (
    build_controlnet_to_img_body,
    build_estimate_body,
    build_img_to_img_body,
    build_img_to_text_body,
    build_remove_bg_body,
    build_remove_bg_options_from_meta,
    build_smart_edit_body,
    build_text_to_img_body,
    build_upscale_body,
    build_upscale_hd_fix_body,
    build_upscale_options_from_meta,
)
from ..._internal._schemas import (
    APIEnvelope,
    MimePart,
)
from ..._internal._schemas.images import (
    ArtworkDetails,
    ImageDownload,
    ImageToTextResult,
    SmartGenerationId,
    TaskMetadata,
    UploadedImage,
    UploadedImageUrl,
    Upscaler,
)
from ..._internal._schemas.payments import Estimate
from ..._internal._schemas.tasks import NewTask
from ..._utils import make_multipart
from ...exceptions import InvalidRequestError
from ...helpers import extract_task_id_from_image_url
from ...options import (
    ControlnetToImgOptions,
    EstimateOptions,
    ImgToImgOptions,
    RemoveBgOptions,
    TextToImgOptions,
    UpscaleHdFixOptions,
    UpscaleOptions,
)
from .._base import BaseResource
from .._shared.envelopes import SyncTaskEnvelope
from .history import HistoryResource

if TYPE_CHECKING:
    from ..._sync_client import SyncClient
else:
    SyncClient = Any


class ImagesResource(BaseResource[SyncClient]):
    """Provides image generation and manipulation operations (sync).

    Accessible via ``client.images``.

    Sub-resources:
        - ``history`` — image usage history
    """

    def __init__(self, client: SyncClient) -> None:
        super().__init__(client)
        self.history = HistoryResource(client)

    def _resolve_ver_no(self, model_no: str, model_ver_no: str | None) -> str:
        return (
            self._client.cache.model_ver_no.get(model_no)
            if model_ver_no is None
            else model_ver_no
        )

    def _resolve_image_size(
        self, model_no: str, ver_no: str, image_size: ImageSize
    ) -> ImageSize:
        if image_size.is_custom:
            return image_size
        gen_params = self._client.cache.gen_params.get((model_no, ver_no))
        size = gen_params.get_size(image_size.ratio)
        if size is None:
            raise ValueError(f"Aspect ratio '{image_size}' not supported by this model")
        return ImageSize.custom(*size)

    def _resolve_task_id_detail(self, task_id: str) -> ArtworkDetails:
        return self._client.cache.task_id_detail.get(task_id)

    def text_to_img(
        self,
        prompt: str,
        model_no: str,
        *,
        model_ver_no: str | None = None,
        gen_mode: Literal["standard", "quality"] = "standard",
        prompt_magic: bool | None = None,
        image_size: ImageSize = ImageSize.PORTRAIT_2_3,
        num_images: int = 1,
        free_creation: bool | None = None,
        image_url: str | None = None,
        options: TextToImgOptions | None = None,
    ) -> SyncTaskEnvelope[TaskMetadata]:
        """Submit a text-to-image generation task.

        Args:
            prompt: Text prompt describing the desired image.
            model_no: Model identifier to use for generation.
            model_ver_no: Model version identifier. Resolved automatically from
                the cache when ``None``.
            gen_mode: Generation quality mode — ``"standard"`` or ``"quality"``.
            prompt_magic: Enable AI prompt enhancement before generation.
                Uses the server default when ``None``.
            image_size: Output aspect ratio. Use a :class:`~seaart.ImageSize`
                preset or :meth:`~seaart.ImageSize.custom` for exact pixels.
                Resolved to pixel dimensions supported by the model.
            num_images: Number of images to generate in one task.
            free_creation: Use the free generation quota when ``True``.
                Uses the server default when ``None``.
            image_url: Optional reference image URL for conditioning.
            options: Advanced generation options (negative prompt, sampling
                parameters, LoRA models, etc.). Defaults are used when ``None``.

        Returns:
            ``SyncTaskEnvelope[TaskMetadata]`` — ``.value.id`` is the task ID.
            Call ``.wait()`` to block until generation completes and receive a
            :class:`~seaart.TaskItem` with ``.image_url`` / ``.image_urls``.

        Example:
            >>> envelope = client.images.text_to_img("a cat", "model_abc123")
            >>> item = envelope.wait()
            >>> print(item.image_url)
        """
        resolved_ver_no = self._resolve_ver_no(model_no, model_ver_no)
        resolved_size = self._resolve_image_size(model_no, resolved_ver_no, image_size)

        opt = options.model_copy(deep=True) if options else TextToImgOptions()
        seed_value = self._client.generate_seed(opt.sampling.seed)

        body = build_text_to_img_body(
            model_no=model_no,
            model_ver_no=resolved_ver_no,
            prompt=prompt,
            image_url=image_url,
            options=opt,
            seed_value=seed_value,
            gen_mode=gen_mode,
            prompt_magic=prompt_magic,
            image_size=resolved_size,
            num_images=num_images,
            free_creation=free_creation,
        )

        raw = self._client.request_route(
            Images.TEXT_TO_IMG,
            response_model=TaskMetadata,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[TaskMetadata].from_envelope(
            raw, tasks=self._client.tasks
        )

    def img_to_img(
        self,
        prompt: str,
        model_no: str,
        image_url: str,
        *,
        model_ver_no: str | None = None,
        strength: float = 0.35,
        prompt_magic: bool | None = None,
        image_size: ImageSize = ImageSize.PORTRAIT_2_3,
        num_images: int = 1,
        free_creation: bool | None = None,
        options: ImgToImgOptions | None = None,
    ) -> SyncTaskEnvelope[TaskMetadata]:
        """Submit an image-to-image generation task.

        Args:
            prompt: Text prompt describing the desired output.
            model_no: Model identifier to use for generation.
            image_url: URL of the input image to transform.
            model_ver_no: Model version identifier. Resolved automatically from
                the cache when ``None``.
            strength: Denoising strength (0.01-1.0). Lower values preserve more
                of the original image; higher values allow greater change.
            prompt_magic: Enable AI prompt enhancement before generation.
                Uses the server default when ``None``.
            image_size: Output aspect ratio. Use a :class:`~seaart.ImageSize`
                preset or :meth:`~seaart.ImageSize.custom` for exact pixels.
                Resolved to pixel dimensions supported by the model.
            num_images: Number of images to generate in one task.
            free_creation: Use the free generation quota when ``True``.
                Uses the server default when ``None``.
            options: Advanced generation options (negative prompt, sampling
                parameters, LoRA models, etc.). Defaults are used when ``None``.

        Returns:
            ``SyncTaskEnvelope[TaskMetadata]`` — ``.value.id`` is the task ID.
            Call ``.wait()`` to block until generation completes and receive a
            :class:`~seaart.TaskItem` with ``.image_url`` / ``.image_urls``.

        Example:
            >>> envelope = client.images.img_to_img(
            ...     "a cat in a forest", "model_abc123", "https://..."
            ... )
            >>> item = envelope.wait()
            >>> print(item.image_url)
        """
        resolved_ver_no = self._resolve_ver_no(model_no, model_ver_no)
        resolved_size = self._resolve_image_size(model_no, resolved_ver_no, image_size)

        opt = options.model_copy(deep=True) if options else ImgToImgOptions()
        seed_value = self._client.generate_seed(opt.sampling.seed)

        body = build_img_to_img_body(
            model_no=model_no,
            model_ver_no=resolved_ver_no,
            prompt=prompt,
            image_url=image_url,
            strength=strength,
            options=opt,
            seed_value=seed_value,
            prompt_magic=prompt_magic,
            image_size=resolved_size,
            num_images=num_images,
            free_creation=free_creation,
        )

        raw = self._client.request_route(
            Images.IMG_TO_IMG,
            response_model=TaskMetadata,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[TaskMetadata].from_envelope(
            raw, tasks=self._client.tasks
        )

    def img_to_text(
        self,
        image_url: str,
        ss: int | None = None,
    ) -> SyncTaskEnvelope[ImageToTextResult]:
        """Submit an image-to-text (captioning) task.

        Args:
            image_url: URL of the image to describe.
            ss: Optional extra parameter forwarded to the API.

        Returns:
            ``SyncTaskEnvelope[ImageToTextResult]`` —
            Call ``.wait()`` to block until the task completes.

        Example:
            >>> envelope = client.images.img_to_text("https://...")
            >>> item = envelope.wait()
        """
        body = build_img_to_text_body(url=image_url, ss=ss)

        raw = self._client.request_route(
            Images.IMG_TO_TEXT,
            response_model=ImageToTextResult,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[ImageToTextResult].from_envelope(
            raw, tasks=self._client.tasks
        )

    def controlnet(
        self,
        prompt: str,
        model_no: str,
        control_input_image: str,
        *,
        model_ver_no: str | None = None,
        gen_mode: Literal["standard", "quality"] = "standard",
        prompt_magic: bool | None = None,
        weight: float = 0.75,
        image_size: ImageSize = ImageSize.PORTRAIT_2_3,
        num_images: int = 1,
        free_creation: bool | None = None,
        options: ControlnetToImgOptions | None = None,
    ) -> SyncTaskEnvelope[TaskMetadata]:
        """Submit a ControlNet-guided image generation task.

        Args:
            prompt: Text prompt describing the desired image.
            model_no: Model identifier to use for generation.
            control_input_image: URL of the guidance image fed to ControlNet.
            model_ver_no: Model version identifier. Resolved automatically from
                the cache when ``None``.
            gen_mode: Generation quality mode — ``"standard"`` or ``"quality"``.
            prompt_magic: Enable AI prompt enhancement before generation.
                Uses the server default when ``None``.
            weight: ControlNet guidance weight (0.0-1.0). Higher values follow
                the guidance image more closely.
            image_size: Output aspect ratio. Use a :class:`~seaart.ImageSize`
                preset or :meth:`~seaart.ImageSize.custom` for exact pixels.
                Resolved to pixel dimensions supported by the model.
            num_images: Number of images to generate in one task.
            free_creation: Use the free generation quota when ``True``.
                Uses the server default when ``None``.
            options: Advanced generation options (negative prompt, sampling
                parameters, LoRA models, etc.). Defaults are used when ``None``.

        Returns:
            ``SyncTaskEnvelope[TaskMetadata]`` — ``.value.id`` is the task ID.
            Call ``.wait()`` to block until generation completes and receive a
            :class:`~seaart.TaskItem` with ``.image_url`` / ``.image_urls``.

        Example:
            >>> envelope = client.images.controlnet(
            ...     "a portrait", "model_abc123", "https://..."
            ... )
            >>> item = envelope.wait()
            >>> print(item.image_url)
        """
        resolved_ver_no = self._resolve_ver_no(model_no, model_ver_no)
        resolved_size = self._resolve_image_size(model_no, resolved_ver_no, image_size)

        opt = options.model_copy(deep=True) if options else ControlnetToImgOptions()
        seed_value = self._client.generate_seed(opt.sampling.seed)

        body = build_controlnet_to_img_body(
            model_no=model_no,
            model_ver_no=resolved_ver_no,
            prompt=prompt,
            control_input_image=control_input_image,
            weight=weight,
            options=opt,
            seed_value=seed_value,
            gen_mode=gen_mode,
            prompt_magic=prompt_magic,
            image_size=resolved_size,
            num_images=num_images,
            free_creation=free_creation,
        )

        raw = self._client.request_route(
            Images.CONTROL_NET_TO_IMG,
            response_model=TaskMetadata,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[TaskMetadata].from_envelope(
            raw, tasks=self._client.tasks
        )

    def smart_edit(
        self,
        prompt: str,
        image_url: str,
        *,
        apply_id: str = "d017fv5e878c738ltm1g",
        is_use_unlimited_free: bool = False,
        task_flow_version: str = "v2",
        node_type_2: str = "TextEncodeQwenImageEditPlus_lrzjason",
        ss: int | None = None,
    ) -> SyncTaskEnvelope[SmartGenerationId]:
        """Submit a smart image editing task driven by a text prompt.

        Args:
            prompt: Text prompt describing the desired edit.
            image_url: URL of the input image to edit.
            apply_id: Smart editing workflow identifier used by the API.
            is_use_unlimited_free: Use unlimited free generation mode when
                available.
            task_flow_version: Task flow version forwarded to the API.
            node_type_2: Internal node type identifier for the prompt encoder.
            ss: Optional extra parameter forwarded to the API.

        Returns:
            ``SyncTaskEnvelope[SmartGenerationId]`` — ``.value.id`` is the
            generated task ID. Call ``.wait()`` to block until the edit
            completes.

        Example:
            >>> envelope = client.images.smart_edit(
            ...     "make the background white", "https://..."
            ... )
            >>> item = envelope.wait()
            >>> print(item.image_url)
        """
        body = build_smart_edit_body(
            prompt=prompt,
            image_url=image_url,
            apply_id=apply_id,
            is_use_unlimited_free=is_use_unlimited_free,
            task_flow_version=task_flow_version,
            node_type_2=node_type_2,
            ss=ss,
        )

        raw = self._client.request_route(
            Images.SMART_EDIT,
            response_model=SmartGenerationId,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[SmartGenerationId].from_envelope(
            raw, tasks=self._client.tasks
        )

    def upscale(
        self,
        image_url: str,
        *,
        model_no: str | None = None,
        model_ver_no: str | None = None,
        prompt: str | None = None,
        options: UpscaleOptions | None = None,
    ) -> SyncTaskEnvelope[TaskMetadata]:
        """Submit an image upscaling task.

        When ``image_url`` is a SeaArt CDN URL containing an embedded task ID,
        the model, prompt, and sampling options are resolved automatically from
        the original task. For other URLs (e.g. static uploads), ``model_no``
        and ``options`` must be provided explicitly.

        Args:
            image_url: URL of the image to upscale.
            model_no: Model identifier. Required when the URL does not contain
                an embedded task ID; overrides the resolved value otherwise.
            model_ver_no: Model version identifier. Resolved from the cache
                when ``None`` and a ``model_no`` is available.
            prompt: Prompt passed to the upscaler. Resolved from the original
                task when ``None`` and the URL contains a task ID.
            options: Advanced upscaling options. Resolved from the original
                task metadata when ``None`` and the URL contains a task ID.

        Returns:
            ``SyncTaskEnvelope[TaskMetadata]`` — ``.value.id`` is the task ID.
            Call ``.wait()`` to block until upscaling completes and receive a
            :class:`~seaart.TaskItem` with ``.image_url``.

        Raises:
            InvalidRequestError: If ``image_url`` does not contain a task ID
                and ``model_no`` or ``options`` are not provided.

        Example:
            >>> envelope = client.images.upscale("https://image.cdn2.seaart.me/...")
            >>> item = envelope.wait()
            >>> print(item.image_url)
        """
        resolved_model_no: str
        resolved_model_ver_no: str
        resolved_prompt: str
        opt: UpscaleOptions
        options_values = UpscaleOptions()

        task_id = extract_task_id_from_image_url(image_url)

        if task_id is not None:
            details = self._resolve_task_id_detail(task_id)
            resolved_model_no = model_no or details.art_model_no
            resolved_model_ver_no = model_ver_no or details.art_model_ver_no
            resolved_prompt = (
                prompt if prompt is not None else (details.meta.prompt or "")
            )

            if options is None:
                opt = build_upscale_options_from_meta(
                    meta=details.meta, defaults=options_values
                )
            else:
                opt = options.model_copy(deep=True)
        else:
            if model_no is None or options is None:
                raise InvalidRequestError(
                    "Could not extract task information from the image URL. "
                    "Please provide model_no, prompt, and options explicitly."
                )
            resolved_model_no = model_no
            resolved_model_ver_no = self._resolve_ver_no(model_no, model_ver_no)
            resolved_prompt = prompt or ""
            opt = options.model_copy(deep=True)

        seed_value = self._client.generate_seed(opt.sampling.seed)

        body = build_upscale_body(
            model_no=resolved_model_no,
            model_ver_no=resolved_model_ver_no,
            prompt=resolved_prompt,
            image_url=image_url,
            options=opt,
            seed_value=seed_value,
        )

        raw = self._client.request_route(
            Images.UPSCALE,
            response_model=TaskMetadata,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[TaskMetadata].from_envelope(
            raw, tasks=self._client.tasks
        )

    def upscale_hd_fix(
        self,
        image_url: str,
        *,
        upscaler: Upscaler | str = "4x-UltraSharp",
        upscaling_resize: float = 2,
        options: UpscaleHdFixOptions | None = None,
    ) -> SyncTaskEnvelope[NewTask]:
        """Submit an HD-fix upscaling task.

        When ``image_url`` is a SeaArt CDN URL containing an embedded task ID,
        the source dimensions and task references are resolved automatically.
        For other URLs, ``options`` must be provided with the necessary fields.

        Args:
            image_url: URL of the image to upscale.
            upscaler: Upscaler algorithm to use. Known values are listed in the
                type annotation; arbitrary strings are also accepted.
            upscaling_resize: Scale factor for the output (e.g. ``2``).
            options: Advanced upscaling options including source dimensions and
                task references. Auto-resolved from the original task when
                ``None`` and the URL contains a task ID.

        Returns:
            ``SyncTaskEnvelope[NewTask]`` — ``.value.id`` is the task ID.
            Call ``.wait()`` to block until upscaling completes and receive a
            :class:`~seaart.TaskItem` with ``.image_url``.

        Example:
            >>> envelope = client.images.upscale_hd_fix(
            ...     "https://image.cdn2.seaart.me/...", upscaling_resize=4
            ... )
            >>> item = envelope.wait()
            >>> print(item.image_url)
        """
        _defaults = UpscaleHdFixOptions()

        task_id = extract_task_id_from_image_url(image_url)
        if task_id is not None:
            details = self._resolve_task_id_detail(task_id)
            if options is None:
                uri = next(
                    (u for u in (details.img_uris or []) if u.url == image_url), None
                )
                opt = UpscaleHdFixOptions(
                    width=details.meta.width,
                    height=details.meta.height,
                    pre_task_id=task_id,
                    pre_art_work_id=details.artwork_no,
                    task_uri_index=uri.index
                    if uri is not None
                    else _defaults.task_uri_index,
                )
            else:
                opt = options.model_copy(deep=True)
        else:
            opt = options.model_copy(deep=True) if options else _defaults

        body = build_upscale_hd_fix_body(
            image_url=image_url,
            upscaler_1=upscaler,
            upscaling_resize=upscaling_resize,
            options=opt,
        )

        raw = self._client.request_route(
            Tasks.CREATE,
            response_model=NewTask,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[NewTask].from_envelope(raw, tasks=self._client.tasks)

    def remove_bg(
        self,
        image_url: str,
        *,
        options: RemoveBgOptions | None = None,
    ) -> SyncTaskEnvelope[NewTask]:
        """Submit a background removal task.

        When ``image_url`` is a SeaArt CDN URL containing an embedded task ID,
        source dimensions and references are resolved automatically from the
        original task. For other URLs the defaults from :class:`RemoveBgOptions`
        are used.

        Args:
            image_url: URL of the image to process.
            options: Background removal options. Auto-resolved from the original
                task when ``None`` and the URL contains a task ID.

        Returns:
            ``SyncTaskEnvelope[NewTask]`` — ``.value.id`` is the task ID.
            Call ``.wait()`` to block until processing completes and receive a
            :class:`~seaart.TaskItem` with ``.image_url``.

        Example:
            >>> envelope = client.images.remove_bg("https://...")
            >>> item = envelope.wait()
            >>> print(item.image_url)
        """
        _defaults = RemoveBgOptions()

        task_id = extract_task_id_from_image_url(image_url)
        if task_id is not None:
            details = self._resolve_task_id_detail(task_id)
            if options is None:
                opt = build_remove_bg_options_from_meta(
                    meta=details.meta, defaults=_defaults
                )
            else:
                opt = options.model_copy(deep=True)
        else:
            opt = options.model_copy(deep=True) if options else _defaults

        body = build_remove_bg_body(
            image_url=image_url,
            options=opt,
        )

        raw = self._client.request_route(
            Tasks.CREATE,
            response_model=NewTask,
            json=body.model_dump(exclude_none=True),
        )
        return SyncTaskEnvelope[NewTask].from_envelope(raw, tasks=self._client.tasks)

    def get_download_url(
        self,
        image_url: str,
    ) -> APIEnvelope[ImageDownload]:
        """Request a direct download link for an image.

        Args:
            image_url: URL of the image to download.

        Returns:
            ``APIEnvelope[ImageDownload]`` — ``.value.url`` is the direct
            download URL; ``.value.task_id`` is the associated task ID.

        Example:
            >>> result = client.images.get_download_url("https://...")
            >>> print(result.value.url)
        """
        env = self._client.request_route(
            Images.DOWNLOAD_URL,
            json={"url": image_url},
        )
        return APIEnvelope[ImageDownload].model_validate(env)

    def upload(
        self,
        *,
        path: str | Path | None = None,
        base64_str: str | None = None,
        bytes_data: bytes | None = None,
        filename: str | None = None,
        category: int = 36,
    ) -> APIEnvelope[UploadedImage]:
        """Upload an image file to the SeaArt platform.

        Exactly one image source must be provided.

        Args:
            path: Filesystem path to the image file.
            base64_str: Base64-encoded image data. Data-URI prefixes
                (``data:image/...;base64,``) are stripped automatically.
            bytes_data: Raw image bytes.
            filename: Filename to use in the upload. Inferred from ``path``
                when not provided; defaults to ``"image.png"`` otherwise.
            category: Upload category flag forwarded to the API.

        Returns:
            ``APIEnvelope[UploadedImage]`` — ``.value.url`` is the public image
            URL; ``.value.signed`` is the signed URL.

        Raises:
            InvalidRequestError: If not exactly one of ``path``, ``base64_str``,
                or ``bytes_data`` is provided.

        Example:
            >>> result = client.images.upload(path="/tmp/photo.png")
            >>> print(result.value.url)
        """
        sources = [path is not None, base64_str is not None, bytes_data is not None]
        if sum(sources) != 1:
            raise InvalidRequestError(
                "Provide exactly one of: path, base64_str, bytes_data"
            )

        raw: bytes
        fname: str

        if path is not None:
            p = Path(path)
            raw = p.read_bytes()
            fname = filename or p.name
        elif base64_str is not None:
            if "," in base64_str:
                base64_str = base64_str.split(",", 1)[1]
            raw = b64decode(base64_str)
            fname = filename or "image.png"
        else:
            assert bytes_data is not None
            raw = bytes_data
            fname = filename or "image.png"

        ext = Path(fname).suffix.lower()
        mime = {
            ".jpg": "image/jpeg",
            ".jpeg": "image/jpeg",
            ".png": "image/png",
            ".webp": "image/webp",
        }.get(ext, "application/octet-stream")

        mp = make_multipart(
            [
                MimePart(
                    name="file",
                    filename=fname,
                    data=raw,
                    content_type=mime,
                )
            ]
        )

        try:
            env = self._client.request_route(
                Images.UPLOAD,
                data={"filename": fname, "category": str(category)},
                multipart=mp,
            )
            return APIEnvelope[UploadedImage].model_validate(env)
        finally:
            mp.close()

    def upload_by_url(
        self,
        url: str,
    ) -> APIEnvelope[UploadedImageUrl]:
        """Upload an image to the platform by providing a public URL.

        Args:
            url: Publicly accessible image URL to import.

        Returns:
            ``APIEnvelope[UploadedImageUrl]`` — ``.value.access_url`` is the
            internal URL of the imported image on the SeaArt platform.

        Example:
            >>> result = client.images.upload_by_url("https://example.com/photo.jpg")
            >>> print(result.value.access_url)
        """
        env = self._client.request_route(
            Images.UPLOAD_BY_URL,
            json={"url": url},
        )
        return APIEnvelope[UploadedImageUrl].model_validate(env)

    def estimate(
        self,
        prompt: str,
        model_no: str | None = None,
        *,
        image_url: str | None = None,
        model_ver_no: str | None = None,
        options: EstimateOptions | None = None,
    ) -> APIEnvelope[Estimate]:
        """Estimate the coin cost for a generation task without submitting it.

        Args:
            prompt: Text prompt that would be used for generation.
            model_no: Model identifier. Resolved from ``image_url`` context
                when ``None``.
            image_url: Optional reference image URL for img-to-img cost
                estimation.
            model_ver_no: Model version identifier. Resolved from the cache
                when ``None`` and a ``model_no`` is provided.
            options: Generation options used to compute the estimate. Defaults
                are used when ``None``.

        Returns:
            ``APIEnvelope[Estimate]`` — ``.value.calculate_coins`` is the
            estimated coin cost; ``.value.has_discount`` indicates whether a
            discount applies; ``.value.calculate_coins_before_discount`` is the
            undiscounted cost.

        Example:
            >>> result = client.images.estimate("a cat", "model_abc123")
            >>> print(result.value.calculate_coins)
        """
        if model_no is not None:
            resolved_ver_no = self._resolve_ver_no(model_no, model_ver_no)
        else:
            resolved_ver_no = None

        opt = options.model_copy(deep=True) if options else EstimateOptions()
        seed_value = self._client.generate_seed(opt.sampling.seed)

        body = build_estimate_body(
            model_no=model_no,
            model_ver_no=resolved_ver_no,
            prompt=prompt,
            image_url=image_url,
            options=opt,
            seed_value=seed_value,
        )

        env = self._client.request_route(
            Tasks.ESTIMATE,
            json=body.model_dump(exclude_none=True),
        )
        return APIEnvelope[Estimate].model_validate(env)

    def detail(
        self,
        id: str,
        *,
        task_flow_version: str = "v2",
    ) -> APIEnvelope[ArtworkDetails]:
        """Fetch detailed artwork information for a task.

        Thin wrapper around :meth:`~seaart.SyncTasksResource.detail`.

        Args:
            id: Task identifier to look up.
            task_flow_version: Task flow version forwarded to the API.

        Returns:
            ``APIEnvelope[ArtworkDetails]`` — ``.value.img_uris`` contains
            the generated image entries; ``.value.art_model_name`` is the model
            used; ``.value.artwork_no`` is the public artwork identifier.

        Example:
            >>> info = client.images.detail("tid1")
            >>> for uri in info.value.img_uris:
            ...     print(uri.url)
        """
        return self._client.tasks.detail(
            task_id=id,
            task_flow_version=task_flow_version,
        )
