#!/usr/bin/env python3
from __future__ import annotations

import json
import os
import re
import shutil
import subprocess
import sys
import venv
from datetime import datetime, timezone
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
PUBLIC_DIR = BASE_DIR / "public"
MANIFEST_PATH = PUBLIC_DIR / "manifest.json"

VARIANTS = {
    "mobile": {
        "out": PUBLIC_DIR / "mobile-image",
        "scan": [
            PUBLIC_DIR / "mobile-image",
            BASE_DIR / "mobile-image",
        ],
    },
    "desktop": {
        "out": PUBLIC_DIR / "desketop-image",
        "scan": [
            PUBLIC_DIR / "desketop-image",
            BASE_DIR / "desketop-image",
        ],
    },
}

INPUT_EXTENSIONS = {
    ".jpg",
    ".jpeg",
    ".png",
    ".bmp",
    ".gif",
    ".tif",
    ".tiff",
    ".webp",
}

GENERATED_NAME_RE = re.compile(r"^\d+\.webp$", re.IGNORECASE)
VENV_DIR = BASE_DIR / ".build-venv"


def ensure_pillow() -> None:
    try:
        from PIL import Image  # noqa: F401
        return
    except Exception:
        pass

    venv_python = VENV_DIR / "bin" / "python"
    if not venv_python.exists():
        print("Pillow not found, creating a local build venv...", file=sys.stderr)
        builder = venv.EnvBuilder(with_pip=True)
        builder.create(VENV_DIR)

    has_pillow = subprocess.run(
        [str(venv_python), "-c", "import PIL"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        check=False,
    ).returncode == 0

    if not has_pillow:
        print("Installing Pillow into the local build venv...", file=sys.stderr)
        subprocess.check_call([str(venv_python), "-m", "pip", "install", "--quiet", "Pillow"])
    os.execv(str(venv_python), [str(venv_python), str(Path(__file__).resolve()), *sys.argv[1:]])


def load_pillow():
    ensure_pillow()
    from PIL import Image, ImageOps

    return Image, ImageOps


def iter_source_files(directory: Path) -> list[Path]:
    if not directory.exists():
        return []
    files: list[Path] = []
    for path in sorted(directory.iterdir(), key=lambda item: item.name.lower()):
        if not path.is_file():
            continue
        if GENERATED_NAME_RE.match(path.name):
            continue
        if path.suffix.lower() not in INPUT_EXTENSIONS:
            continue
        files.append(path)
    return files


def select_scan_directory(scan_candidates: list[Path]) -> Path:
    """优先有原图的目录，其次有序号 webp；否则退回 scan 第一项（通常为 public下的输出目录）。"""
    for directory in scan_candidates:
        if iter_source_files(directory):
            return directory
    for directory in scan_candidates:
        if sorted_generated_webp_names(directory):
            return directory
    return scan_candidates[0]


def clear_generated_files(directory: Path) -> None:
    if not directory.exists():
        directory.mkdir(parents=True, exist_ok=True)
        return
    for path in directory.iterdir():
        if path.is_file() and GENERATED_NAME_RE.match(path.name):
            path.unlink()


def sorted_generated_webp_names(directory: Path) -> list[str]:
    """已存在的序号 webp（无原图时再跑脚本不会清空）。"""
    if not directory.exists():
        return []
    names = [
        path.name
        for path in directory.iterdir()
        if path.is_file() and GENERATED_NAME_RE.match(path.name)
    ]

    def sort_key(entry: str) -> int:
        return int(Path(entry).stem)

    names.sort(key=sort_key)
    return names


def convert_to_webp(source: Path, target: Path, image_mod, image_ops) -> None:
    with image_mod.open(source) as image:
        image = image_ops.exif_transpose(image)
        if image.mode not in {"RGB", "RGBA", "LA", "P"}:
            image = image.convert("RGBA")
        elif image.mode == "P":
            image = image.convert("RGBA")
        elif image.mode == "LA":
            image = image.convert("RGBA")
        elif image.mode == "RGB":
            pass
        image.save(target, format="WEBP", quality=92, method=6)


def copy_generated_webps(source_dir: Path, out_dir: Path, names: list[str]) -> None:
    """将已有序号 webp 同步到 public 下的部署目录。"""
    out_dir.mkdir(parents=True, exist_ok=True)
    for entry in names:
        shutil.copy2(source_dir / entry, out_dir / entry)


def build_variant(name: str, spec: dict[str, Path | list[Path]], image_mod, image_ops) -> dict:
    scan_dir = select_scan_directory(list(spec["scan"]))
    out_dir = spec["out"]
    out_dir.mkdir(parents=True, exist_ok=True)
    sources = iter_source_files(scan_dir)

    images: list[str] = []
    if sources:
        clear_generated_files(out_dir)
        for index, source in enumerate(sources, start=1):
            target = out_dir / f"{index}.webp"
            try:
                convert_to_webp(source, target, image_mod, image_ops)
                images.append(target.name)
                rel_src = os.path.relpath(source.resolve(), BASE_DIR.resolve())
                print(f"[{name}] {rel_src} -> {target.name}")
            except Exception as exc:
                print(f"[{name}] skip {source.name}: {exc}", file=sys.stderr)
    else:
        images = sorted_generated_webp_names(out_dir)
        if not images and scan_dir.resolve() != out_dir.resolve():
            legacy = sorted_generated_webp_names(scan_dir)
            if legacy:
                copy_generated_webps(scan_dir, out_dir, legacy)
                images = legacy
                print(f"[{name}] 已从 {scan_dir.relative_to(BASE_DIR)} 复制序号 webp ×{len(images)} -> public/")
        if images:
            if scan_dir.resolve() == out_dir.resolve():
                print(f"[{name}] 未检测到原图，保留 public 下序号 webp ×{len(images)}")
        else:
            print(f"[{name}] 跳过：未发现原图，且 public/ 与归档目录均无 1.webp 等序号文件")

    return {
        "folder": out_dir.name,
        "count": len(images),
        "images": images,
    }


def write_manifest(manifest: dict) -> None:
    MANIFEST_PATH.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def main() -> int:
    image_mod, image_ops = load_pillow()

    manifest = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "variants": {},
    }

    for name, variant_spec in VARIANTS.items():
        manifest["variants"][name] = build_variant(name, variant_spec, image_mod, image_ops)

    write_manifest(manifest)
    print(f"Wrote manifest: {MANIFEST_PATH}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
