from __future__ import annotations

import mimetypes
from pathlib import Path
from time import monotonic, sleep
from urllib.parse import urlparse

from pydantic import BaseModel

from ebay_client.core.http.transport import ApiTransport
from ebay_client.generated.media.models import (
    CreateDocumentFromUrlRequest,
    CreateDocumentRequest,
    CreateDocumentResponse,
    CreateImageFromUrlRequest,
    CreateVideoRequest,
    DocumentResponse,
    ImageResponse,
    Video,
)

MEDIA_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory"


class CreatedMediaResource(BaseModel):
    location: str | None = None
    resource_id: str | None = None


class VideoWorkflowResult(BaseModel):
    created: CreatedMediaResource
    video: Video
    video_id: str | None = None


class DocumentWorkflowResult(BaseModel):
    created: CreateDocumentResponse
    uploaded: DocumentResponse | None = None
    document: DocumentResponse
    document_id: str | None = None


def extract_resource_id(location: str | None) -> str | None:
    if not location:
        return None

    path = urlparse(location).path.rstrip("/")
    if not path:
        return None

    _, _, resource_id = path.rpartition("/")
    return resource_id or None


def guess_media_content_type(file_name: str, *, default: str = "application/octet-stream") -> str:
    guessed_type, _ = mimetypes.guess_type(file_name)
    return guessed_type or default


class MediaClient:
    def __init__(self, transport: ApiTransport) -> None:
        self.transport = transport

    def create_image_from_file(
        self,
        *,
        file_name: str,
        content: bytes,
        content_type: str = "application/octet-stream",
    ) -> ImageResponse:
        return self.transport.request_model(
            ImageResponse,
            "POST",
            "/commerce/media/v1_beta/image/create_image_from_file",
            scopes=[MEDIA_SCOPE],
            files={"image": (file_name, content, content_type)},
        )

    def create_image_from_path(self, image_path: str | Path) -> ImageResponse:
        path = Path(image_path)
        return self.create_image_from_file(
            file_name=path.name,
            content=path.read_bytes(),
            content_type=guess_media_content_type(path.name),
        )

    def create_image_from_url(self, payload: CreateImageFromUrlRequest) -> ImageResponse:
        return self.transport.request_model(
            ImageResponse,
            "POST",
            "/commerce/media/v1_beta/image/create_image_from_url",
            scopes=[MEDIA_SCOPE],
            headers={"Content-Type": "application/json"},
            json_body=payload.model_dump(by_alias=True, exclude_none=True),
        )

    def get_image(self, image_id: str) -> ImageResponse:
        return self.transport.request_model(
            ImageResponse,
            "GET",
            f"/commerce/media/v1_beta/image/{image_id}",
            scopes=[MEDIA_SCOPE],
        )

    def create_video(self, payload: CreateVideoRequest) -> CreatedMediaResource:
        response = self.transport.request(
            "POST",
            "/commerce/media/v1_beta/video",
            scopes=[MEDIA_SCOPE],
            headers={"Content-Type": "application/json"},
            json_body=payload.model_dump(by_alias=True, exclude_none=True),
        )
        location = response.headers.get("Location")
        return CreatedMediaResource(location=location, resource_id=extract_resource_id(location))

    def create_upload_and_wait_video(
        self,
        payload: CreateVideoRequest,
        *,
        content: bytes,
        content_length: int | None = None,
        content_range: str | None = None,
        timeout_seconds: float = 30.0,
        poll_interval_seconds: float = 1.0,
    ) -> VideoWorkflowResult:
        created = self.create_video(payload)
        video_id = self._require_resource_id(created.resource_id, "video resource ID")
        self.upload_video(
            video_id,
            content=content,
            content_length=content_length if content_length is not None else len(content),
            content_range=content_range,
        )
        video = self.wait_for_video(
            video_id,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
        )
        return VideoWorkflowResult(created=created, video=video, video_id=video_id)

    def create_upload_and_wait_video_from_path(
        self,
        video_path: str | Path,
        *,
        title: str | None = None,
        classification: list[str] | None = None,
        description: str | None = None,
        timeout_seconds: float = 30.0,
        poll_interval_seconds: float = 1.0,
    ) -> VideoWorkflowResult:
        path = Path(video_path)
        content = path.read_bytes()
        payload = CreateVideoRequest(
            title=title or path.stem,
            size=len(content),
            classification=classification or ["ITEM"],
            description=description,
        )
        return self.create_upload_and_wait_video(
            payload,
            content=content,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
        )

    def get_video(self, video_id: str) -> Video:
        return self.transport.request_model(
            Video,
            "GET",
            f"/commerce/media/v1_beta/video/{video_id}",
            scopes=[MEDIA_SCOPE],
        )

    def upload_video(
        self,
        video_id: str,
        *,
        content: bytes,
        content_length: int | None = None,
        content_range: str | None = None,
    ) -> None:
        headers = {"Content-Type": "application/octet-stream"}
        if content_length is not None:
            headers["Content-Length"] = str(content_length)
        if content_range is not None:
            headers["Content-Range"] = content_range

        self.transport.request_json(
            "POST",
            f"/commerce/media/v1_beta/video/{video_id}/upload",
            scopes=[MEDIA_SCOPE],
            headers=headers,
            content=content,
        )

    def create_document(self, payload: CreateDocumentRequest) -> CreateDocumentResponse:
        return self.transport.request_model(
            CreateDocumentResponse,
            "POST",
            "/commerce/media/v1_beta/document",
            scopes=[MEDIA_SCOPE],
            headers={"Content-Type": "application/json"},
            json_body=payload.model_dump(by_alias=True, exclude_none=True),
        )

    def create_upload_and_wait_document(
        self,
        payload: CreateDocumentRequest,
        *,
        file_name: str,
        content: bytes,
        content_type: str = "application/octet-stream",
        timeout_seconds: float = 30.0,
        poll_interval_seconds: float = 1.0,
    ) -> DocumentWorkflowResult:
        created = self.create_document(payload)
        document_id = self._require_resource_id(created.documentId, "documentId")
        uploaded = self.upload_document(
            document_id,
            file_name=file_name,
            content=content,
            content_type=content_type,
        )
        document = self.wait_for_document(
            document_id,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
        )
        return DocumentWorkflowResult(
            created=created,
            uploaded=uploaded,
            document=document,
            document_id=document_id,
        )

    def create_document_from_url(self, payload: CreateDocumentFromUrlRequest) -> CreateDocumentResponse:
        return self.transport.request_model(
            CreateDocumentResponse,
            "POST",
            "/commerce/media/v1_beta/document/create_document_from_url",
            scopes=[MEDIA_SCOPE],
            headers={"Content-Type": "application/json"},
            json_body=payload.model_dump(by_alias=True, exclude_none=True),
        )

    def create_document_from_url_and_wait(
        self,
        payload: CreateDocumentFromUrlRequest,
        *,
        timeout_seconds: float = 30.0,
        poll_interval_seconds: float = 1.0,
    ) -> DocumentWorkflowResult:
        created = self.create_document_from_url(payload)
        document_id = self._require_resource_id(created.documentId, "documentId")
        document = self.wait_for_document(
            document_id,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
        )
        return DocumentWorkflowResult(
            created=created,
            uploaded=None,
            document=document,
            document_id=document_id,
        )

    def get_document(self, document_id: str) -> DocumentResponse:
        return self.transport.request_model(
            DocumentResponse,
            "GET",
            f"/commerce/media/v1_beta/document/{document_id}",
            scopes=[MEDIA_SCOPE],
        )

    def upload_document(
        self,
        document_id: str,
        *,
        file_name: str,
        content: bytes,
        content_type: str = "application/octet-stream",
    ) -> DocumentResponse:
        return self.transport.request_model(
            DocumentResponse,
            "POST",
            f"/commerce/media/v1_beta/document/{document_id}/upload",
            scopes=[MEDIA_SCOPE],
            files={"file": (file_name, content, content_type)},
        )

    def upload_document_from_path(self, document_id: str, document_path: str | Path) -> DocumentResponse:
        path = Path(document_path)
        return self.upload_document(
            document_id,
            file_name=path.name,
            content=path.read_bytes(),
            content_type=guess_media_content_type(path.name),
        )

    def create_upload_and_wait_document_from_path(
        self,
        payload: CreateDocumentRequest,
        document_path: str | Path,
        *,
        timeout_seconds: float = 30.0,
        poll_interval_seconds: float = 1.0,
    ) -> DocumentWorkflowResult:
        path = Path(document_path)
        return self.create_upload_and_wait_document(
            payload,
            file_name=path.name,
            content=path.read_bytes(),
            content_type=guess_media_content_type(path.name),
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
        )

    def wait_for_video(
        self,
        video_id: str,
        *,
        success_statuses: set[str] | None = None,
        failure_statuses: set[str] | None = None,
        timeout_seconds: float = 30.0,
        poll_interval_seconds: float = 1.0,
    ) -> Video:
        desired_statuses = success_statuses or {"LIVE"}
        terminal_failures = failure_statuses or {"BLOCKED", "PROCESSING_FAILED"}
        return self._wait_for_media_state(
            fetch=lambda: self.get_video(video_id),
            get_status=lambda payload: payload.status,
            desired_statuses=desired_statuses,
            terminal_failures=terminal_failures,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
            resource_label=f"video {video_id}",
        )

    def wait_for_document(
        self,
        document_id: str,
        *,
        success_statuses: set[str] | None = None,
        failure_statuses: set[str] | None = None,
        timeout_seconds: float = 30.0,
        poll_interval_seconds: float = 1.0,
    ) -> DocumentResponse:
        desired_statuses = success_statuses or {"ACCEPTED"}
        terminal_failures = failure_statuses or {"REJECTED"}
        return self._wait_for_media_state(
            fetch=lambda: self.get_document(document_id),
            get_status=lambda payload: payload.documentStatus,
            desired_statuses=desired_statuses,
            terminal_failures=terminal_failures,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
            resource_label=f"document {document_id}",
        )

    def _wait_for_media_state(
        self,
        *,
        fetch,
        get_status,
        desired_statuses: set[str],
        terminal_failures: set[str],
        timeout_seconds: float,
        poll_interval_seconds: float,
        resource_label: str,
    ):
        deadline = monotonic() + timeout_seconds
        while True:
            payload = fetch()
            status = get_status(payload)
            if status in desired_statuses:
                return payload
            if status in terminal_failures:
                raise ValueError(f"{resource_label} reached terminal failure status: {status}")
            if monotonic() >= deadline:
                raise TimeoutError(f"Timed out while waiting for {resource_label}; last status was {status!r}")
            sleep(poll_interval_seconds)

    @staticmethod
    def _require_resource_id(value: str | None, field_name: str) -> str:
        if not value:
            raise RuntimeError(f"eBay did not return a required {field_name}")
        return value