from __future__ import annotations

import asyncio
import os
import shutil
import tempfile
import urllib.parse
from pathlib import Path
from urllib.request import Request, urlopen

from flow_xhs.runtime.errors import InvalidPublishParameterError, PublishMediaPreparationError


def _is_url(path: str) -> bool:
    try:
        parsed = urllib.parse.urlparse(path)
        return bool(parsed.scheme and parsed.netloc)
    except Exception:
        return False


def _guess_image_extension(file_head: bytes, content_type: str = "") -> str:
    ct = (content_type or "").lower()
    if file_head.startswith(b"\xFF\xD8\xFF"):
        return ".jpg"
    if file_head.startswith(b"\x89PNG\r\n\x1a\n"):
        return ".png"
    if file_head.startswith((b"GIF87a", b"GIF89a")):
        return ".gif"
    if len(file_head) >= 12 and file_head[:4] == b"RIFF" and file_head[8:12] == b"WEBP":
        return ".webp"
    if file_head.startswith(b"BM"):
        return ".bmp"
    if file_head.startswith((b"II*\x00", b"MM\x00*")):
        return ".tiff"

    if "png" in ct:
        return ".png"
    if "jpeg" in ct or "jpg" in ct:
        return ".jpg"
    if "webp" in ct:
        return ".webp"
    if "gif" in ct:
        return ".gif"
    if "bmp" in ct:
        return ".bmp"
    if "tiff" in ct:
        return ".tiff"
    return ""


def _guess_video_extension(file_head: bytes, content_type: str = "", source_suffix: str = "") -> str:
    suffix = (source_suffix or "").strip().lower()
    if suffix in {".mp4", ".mov"}:
        return suffix

    ct = (content_type or "").lower()
    if "video/mp4" in ct:
        return ".mp4"
    if "video/quicktime" in ct:
        return ".mov"

    # MP4/MOV both belong to ISO BMFF and commonly contain `ftyp` in the first bytes.
    if len(file_head) >= 12 and file_head[4:8] == b"ftyp":
        major_brand = file_head[8:12]
        if major_brand == b"qt  ":
            return ".mov"
        return ".mp4"

    return ""


def _supported_extensions_text(extensions: set[str]) -> str:
    return ", ".join(ext.lstrip(".") for ext in sorted(extensions))


class MediaPreprocessor:
    """Prepare and validate media assets before publish flow."""

    def __init__(
        self,
        allowed_image_extensions: set[str] | None = None,
        allowed_video_extensions: set[str] | None = None,
        temp_prefix: str = "publish_upload_",
    ):
        self.allowed_image_extensions = allowed_image_extensions or {".png", ".jpg", ".jpeg", ".webp"}
        self.allowed_video_extensions = allowed_video_extensions or {".mp4", ".mov"}
        self.temp_prefix = temp_prefix
        self.temp_dir: str | None = None

    async def __aenter__(self) -> "MediaPreprocessor":
        self.temp_dir = tempfile.mkdtemp(prefix=self.temp_prefix)
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        await self.cleanup()

    async def cleanup(self) -> None:
        if self.temp_dir and os.path.exists(self.temp_dir):
            await asyncio.to_thread(shutil.rmtree, self.temp_dir, True)
            self.temp_dir = None

    async def prepare_images(self, media_list: list[str]) -> list[str]:
        normalized = self._normalize_media_list(media_list, field_name="media_list")
        final_paths: list[str] = []

        for index, item in enumerate(normalized):
            if _is_url(item):
                final_paths.append(await self._download_image(item, index))
            else:
                final_paths.append(await self._validate_local_image(item))
        return final_paths

    async def prepare_videos(self, media_list: list[str]) -> list[str]:
        normalized = self._normalize_media_list(media_list, field_name="media_list")
        final_paths: list[str] = []

        for index, item in enumerate(normalized):
            if _is_url(item):
                final_paths.append(await self._download_video(item, index))
            else:
                final_paths.append(await self._validate_local_video(item))
        return final_paths

    @staticmethod
    def _normalize_media_list(media_list: list, field_name: str) -> list[str]:
        if media_list is None:
            raise InvalidPublishParameterError(f"`{field_name}` 不能为空")
        if not isinstance(media_list, (list, tuple)):
            raise InvalidPublishParameterError(f"`{field_name}` 必须是 list 或 tuple")
        if len(media_list) == 0:
            raise InvalidPublishParameterError(f"`{field_name}` 至少包含 1 个文件")

        normalized = []
        for index, item in enumerate(media_list):
            text = "" if item is None else str(item).strip()
            if not text:
                raise InvalidPublishParameterError(f"`{field_name}` 第 {index + 1} 项不能为空")
            normalized.append(text)
        return normalized

    async def _validate_local_image(self, image_path: str) -> str:
        local_path = Path(image_path)
        if not local_path.exists():
            raise InvalidPublishParameterError(f"本地图片不存在: {image_path}")
        if not local_path.is_file():
            raise InvalidPublishParameterError(f"路径不是一个有效文件: {image_path}")

        suffix = local_path.suffix.lower()
        if suffix not in self.allowed_image_extensions:
            supported = _supported_extensions_text(self.allowed_image_extensions)
            raise InvalidPublishParameterError(f"不支持的文件格式: {image_path} (仅支持 {supported})")

        try:
            head = await asyncio.to_thread(self._read_file_head, local_path, 64)
        except Exception as e:
            raise PublishMediaPreparationError(f"读取本地图片失败: {image_path}。原因: {e}") from e

        detected = _guess_image_extension(head)
        if detected and detected not in self.allowed_image_extensions:
            supported = _supported_extensions_text(self.allowed_image_extensions)
            raise InvalidPublishParameterError(f"本地图片格式不支持: {image_path} (仅支持 {supported})")

        return str(local_path.absolute())

    async def _validate_local_video(self, video_path: str) -> str:
        local_path = Path(video_path)
        if not local_path.exists():
            raise InvalidPublishParameterError(f"本地视频不存在: {video_path}")
        if not local_path.is_file():
            raise InvalidPublishParameterError(f"路径不是一个有效文件: {video_path}")
        if local_path.stat().st_size <= 0:
            raise InvalidPublishParameterError(f"本地视频文件为空: {video_path}")

        suffix = local_path.suffix.lower()
        if suffix not in self.allowed_video_extensions:
            supported = _supported_extensions_text(self.allowed_video_extensions)
            raise InvalidPublishParameterError(f"不支持的视频格式: {video_path} (仅支持 {supported})")
        return str(local_path.absolute())

    async def _download_image(self, image_url: str, index: int) -> str:
        if not self.temp_dir:
            self.temp_dir = tempfile.mkdtemp(prefix=self.temp_prefix)
        return await asyncio.to_thread(self._download_image_sync, image_url, index)

    async def _download_video(self, video_url: str, index: int) -> str:
        if not self.temp_dir:
            self.temp_dir = tempfile.mkdtemp(prefix=self.temp_prefix)
        return await asyncio.to_thread(self._download_video_sync, video_url, index)

    def _download_image_sync(self, image_url: str, index: int) -> str:
        req = Request(image_url, method="GET")
        req.add_header(
            "User-Agent",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
        )
        parsed = urllib.parse.urlparse(image_url)
        req.add_header("Referer", f"{parsed.scheme}://{parsed.netloc}/")

        try:
            with urlopen(req, timeout=20) as resp:
                status = getattr(resp, "status", 200)
                if status != 200:
                    raise PublishMediaPreparationError(f"图片下载失败，HTTP 状态码: {status}, url: {image_url}")
                body = resp.read()
                content_type = (resp.headers.get("Content-Type") or "").lower()
        except Exception as e:
            raise PublishMediaPreparationError(f"图片下载失败: {image_url}。原因: {e}") from e

        ext = _guess_image_extension(body[:64], content_type=content_type)
        if not ext:
            raise InvalidPublishParameterError(f"无法识别图片格式: {image_url}")
        if ext not in self.allowed_image_extensions:
            supported = _supported_extensions_text(self.allowed_image_extensions)
            raise InvalidPublishParameterError(f"不支持的图片格式: {image_url} (仅支持 {supported})")

        target_path = Path(self.temp_dir) / f"download_{index}{ext}"
        try:
            with open(target_path, "wb") as output:
                output.write(body)
        except Exception as e:
            raise PublishMediaPreparationError(f"保存图片失败: {image_url}。原因: {e}") from e

        return str(target_path.absolute())

    def _download_video_sync(self, video_url: str, index: int) -> str:
        req = Request(video_url, method="GET")
        req.add_header(
            "User-Agent",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
        )
        parsed = urllib.parse.urlparse(video_url)
        req.add_header("Referer", f"{parsed.scheme}://{parsed.netloc}/")

        source_suffix = Path(urllib.parse.urlparse(video_url).path).suffix.lower()
        supported = _supported_extensions_text(self.allowed_video_extensions)

        try:
            with urlopen(req, timeout=60) as resp:
                status = getattr(resp, "status", 200)
                if status != 200:
                    raise PublishMediaPreparationError(f"视频下载失败，HTTP 状态码: {status}, url: {video_url}")
                content_type = (resp.headers.get("Content-Type") or "").lower()

                first_chunk = resp.read(64 * 1024)
                if not first_chunk:
                    raise InvalidPublishParameterError(f"视频下载为空: {video_url}")

                ext = _guess_video_extension(first_chunk[:64], content_type=content_type, source_suffix=source_suffix)
                if not ext:
                    raise InvalidPublishParameterError(f"无法识别视频格式: {video_url}")
                if ext not in self.allowed_video_extensions:
                    raise InvalidPublishParameterError(f"不支持的视频格式: {video_url} (仅支持 {supported})")

                target_path = Path(self.temp_dir) / f"download_{index}{ext}"
                with open(target_path, "wb") as output:
                    output.write(first_chunk)
                    while True:
                        chunk = resp.read(1024 * 1024)
                        if not chunk:
                            break
                        output.write(chunk)
        except InvalidPublishParameterError:
            raise
        except Exception as e:
            raise PublishMediaPreparationError(f"视频下载失败: {video_url}。原因: {e}") from e

        return str(target_path.absolute())

    @staticmethod
    def _read_file_head(path: Path, size: int) -> bytes:
        with open(path, "rb") as f:
            return f.read(size)
