from __future__ import annotations

import base64
import binascii
import mimetypes
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path


class DocumentPreviewAssets:
    PDF_RENDERER_ID = "pdf-raster-cjk-safe-v3"
    PDF_PREVIEW_MEDIA_TYPE = "image/png"
    PDF_PREVIEW_SUFFIX = ".png"
    PDF_UNUSABLE_PREVIEW_ERRORS = (
        "Missing language pack",
        "Unknown font tag",
        "No font in show",
    )
    POPPLER_DATA_DIR_CANDIDATES = (
        "/usr/share/poppler",
        "/usr/local/share/poppler",
        "/opt/homebrew/share/poppler",
    )

    @staticmethod
    def decode_data_url(payload: str) -> tuple[str, bytes] | None:
        normalized = str(payload or "").strip()
        matched = re.match(
            r"^data:(?P<media>[\w.+-]+/[\w.+-]+);base64,(?P<body>.+)$",
            normalized,
            flags=re.DOTALL,
        )
        if not matched:
            return None
        try:
            content = base64.b64decode(matched.group("body"), validate=True)
        except (binascii.Error, ValueError):
            return None
        return matched.group("media"), content

    @classmethod
    def renderer_id_for_source(cls, media_type: str | None) -> str:
        return cls.PDF_RENDERER_ID if str(media_type or "").strip() == "application/pdf" else ""

    @classmethod
    def write_data_url_preview(
        cls,
        *,
        preview_dir: Path,
        preview_name_stem: str,
        preview_data_url: str,
    ) -> tuple[Path, str, str] | None:
        decoded = cls.decode_data_url(preview_data_url)
        if decoded is None:
            return None

        preview_media_type, preview_content = decoded
        suffix = mimetypes.guess_extension(preview_media_type) or ".bin"
        preview_name = f"{Path(preview_name_stem).stem}{suffix}"
        preview_path = preview_dir / preview_name
        preview_path.write_bytes(preview_content)
        return preview_path, preview_media_type, preview_name

    @classmethod
    def render_pdf_first_page(
        cls,
        *,
        pdf_path: Path,
        preview_path: Path,
        timeout_seconds: int | float,
    ) -> Path:
        preview_path.parent.mkdir(parents=True, exist_ok=True)
        with tempfile.TemporaryDirectory(prefix=".pdf-preview-", dir=str(preview_path.parent)) as temp_dir:
            pages = cls.render_pdf_pages(
                pdf_path=pdf_path,
                output_dir=Path(temp_dir),
                timeout_seconds=timeout_seconds,
            )
            shutil.copyfile(pages[0], preview_path)
        return preview_path

    @classmethod
    def render_pdf_pages(
        cls,
        *,
        pdf_path: Path,
        output_dir: Path,
        timeout_seconds: int | float,
        resolution: int = 160,
    ) -> list[Path]:
        output_dir.mkdir(parents=True, exist_ok=True)
        errors: list[str] = []
        for renderer_name, command in cls._pdf_render_commands(
            pdf_path=pdf_path,
            output_dir=output_dir,
            resolution=resolution,
        ):
            executable = shutil.which(renderer_name)
            if not executable:
                errors.append(f"{renderer_name}: executable not found")
                continue

            cls._clear_rendered_pdf_pages(output_dir)
            command[0] = executable
            completed = subprocess.run(
                command,
                capture_output=True,
                text=True,
                timeout=timeout_seconds,
                check=False,
                env=cls._pdf_render_env(),
            )
            detail = (completed.stderr or completed.stdout or "").strip()
            if completed.returncode != 0:
                errors.append(f"{renderer_name}: {detail or 'renderer returned non-zero status'}")
                continue
            if cls.render_output_indicates_unusable_pdf_preview(detail):
                errors.append(f"{renderer_name}: {detail or 'renderer produced unusable output'}")
                continue

            pages = sorted(output_dir.glob("page-*.png"), key=cls._extract_pdf_page_sort_key)
            if pages:
                return pages
            errors.append(f"{renderer_name}: renderer did not generate PNG pages")

        cls._clear_rendered_pdf_pages(output_dir)
        detail = "；".join(errors[-3:])
        raise RuntimeError(detail or "no PDF renderer generated usable PNG pages")

    @classmethod
    def render_output_indicates_unusable_pdf_preview(cls, output: str) -> bool:
        return any(token in str(output or "") for token in cls.PDF_UNUSABLE_PREVIEW_ERRORS)

    @classmethod
    def _pdf_render_commands(
        cls,
        *,
        pdf_path: Path,
        output_dir: Path,
        resolution: int,
    ) -> list[tuple[str, list[str]]]:
        prefix = output_dir / "page"
        page_pattern = output_dir / "page-%d.png"
        return [
            (
                "pdftoppm",
                ["pdftoppm", "-png", "-r", str(resolution), str(pdf_path), str(prefix)],
            ),
            (
                "mutool",
                ["mutool", "draw", "-r", str(resolution), "-o", str(page_pattern), str(pdf_path)],
            ),
            (
                "gs",
                [
                    "gs",
                    "-dSAFER",
                    "-dBATCH",
                    "-dNOPAUSE",
                    "-sDEVICE=png16m",
                    f"-r{resolution}",
                    f"-sOutputFile={page_pattern}",
                    str(pdf_path),
                ],
            ),
            (
                "pdftocairo",
                ["pdftocairo", "-png", "-r", str(resolution), str(pdf_path), str(prefix)],
            ),
        ]

    @classmethod
    def _pdf_render_env(cls) -> dict[str, str]:
        env = os.environ.copy()
        for candidate in cls.POPPLER_DATA_DIR_CANDIDATES:
            if (Path(candidate) / "cMap").exists():
                env.setdefault("POPPLER_DATADIR", candidate)
                break
        return env

    @staticmethod
    def _clear_rendered_pdf_pages(output_dir: Path) -> None:
        for page in output_dir.glob("page-*.png"):
            page.unlink(missing_ok=True)

    @staticmethod
    def _extract_pdf_page_sort_key(path: Path) -> tuple[int, str]:
        suffix = path.stem.rsplit("-", 1)[-1]
        try:
            return int(suffix), path.name
        except ValueError:
            return 0, path.name
