# -*- coding: utf-8 -*-
"""缩略图与文件元信息提取。

- 图片缩略图用 Pillow 生成，带 EXIF 方向修正、尺寸限制、磁盘缓存。
- 音频读取时长用标准库（解析 mp3/wav 头），不依赖 ffmpeg。
- 所有函数都是「失败静默降级」：解析失败返回 None 或默认值，不阻断导入。
"""
from __future__ import annotations

import math
import os
import wave
from pathlib import Path


THUMB_DIR_NAME = "thumbnails"
THUMB_MAX = 320           # 缩略图长边像素
THUMB_QUALITY = 82        # JPEG 质量
TEXT_PREVIEW_LEN = 180    # 文本预览截断长度


def thumb_dir(root: Path) -> Path:
    d = root / THUMB_DIR_NAME
    d.mkdir(parents=True, exist_ok=True)
    return d


def thumb_path(asset_id: int, root: Path) -> Path:
    return thumb_dir(root) / f"asset_{asset_id}.jpg"


def is_image_file(p: Path) -> bool:
    return p.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif"}


def is_audio_file(p: Path) -> bool:
    return p.suffix.lower() in {".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac"}


# ---------------------------------------------------------------------------
# 图片缩略图
# ---------------------------------------------------------------------------
def make_thumb(src: str | os.PathLike, asset_id: int, root: Path) -> str | None:
    """生成缩略图，返回缓存路径；失败返回 None。

    命中缓存（src mtime 未变）时直接返回，避免重复编码。
    """
    src_p = Path(src)
    if not src_p.is_file() or not is_image_file(src_p):
        return None
    dest = thumb_path(asset_id, root)
    try:
        if dest.exists() and dest.stat().st_mtime >= src_p.stat().st_mtime:
            return str(dest)
    except OSError:
        pass

    try:
        from PIL import Image, ImageOps, ImageFile
        ImageFile.LOAD_TRUNCATED_IMAGES = True

        with Image.open(src_p) as im:
            im = ImageOps.exif_transpose(im)
            if im.mode in ("RGBA", "P", "LA"):
                # 透明通道铺白底，避免缩略图发黑
                bg = Image.new("RGB", im.size, (255, 255, 255))
                im = Image.alpha_composite(bg.convert("RGBA"), im.convert("RGBA")).convert("RGB")
            elif im.mode != "RGB":
                im = im.convert("RGB")

            im.thumbnail((THUMB_MAX, THUMB_MAX), Image.Resampling.LANCZOS)
            im.save(dest, "JPEG", quality=THUMB_QUALITY, optimize=True, progressive=True)
        return str(dest)
    except Exception:
        # 损坏图片 / 格式不支持 —— 静默降级
        return None


def image_size(src: str | os.PathLike) -> tuple[int, int] | None:
    """返回图片原始宽高，失败返回 None。"""
    try:
        from PIL import Image
        with Image.open(src) as im:
            return im.size
    except Exception:
        return None


# ---------------------------------------------------------------------------
# 音频时长
# ---------------------------------------------------------------------------
def audio_duration(p: str | os.PathLike) -> float | None:
    """秒。支持 WAV（标准库）与 MP3（逐帧粗算），其余返回 None。"""
    p = Path(p)
    if not p.is_file():
        return None
    try:
        suffix = p.suffix.lower()
        if suffix == ".wav":
            with wave.open(str(p), "rb") as w:
                return w.getnframes() / float(w.getframerate() or 1)
        if suffix == ".mp3":
            return _mp3_duration(p)
    except Exception:
        pass
    return None


def _mp3_duration(p: Path) -> float | None:
    """解析 ID3v2 + MPEG 帧头估算时长；失败返回 None。"""
    try:
        size = p.stat().st_size
        with open(p, "rb") as f:
            head = f.read(10)
            # 跳过 ID3v2 头（若有）
            id3_size = 0
            if head[:3] == b"ID3":
                id3_size = _syncsafe(f.read(6))
            f.seek(id3_size)
            # 定位第一个帧同步
            buf = f.read(size)

        pos = 0
        sample_rate = None
        bitrate = None
        frames = 0
        while pos + 4 <= len(buf):
            sync = buf[pos:pos + 2]
            if sync == b"\xff\xfb" or sync == b"\xff\xe2" or sync == b"\xff\xf2":
                info = buf[pos + 2]
                br_idx = (info >> 1) & 0x0F
                sr_idx = (buf[pos + 3] >> 6) & 0x03
                if 1 <= br_idx <= 14 and 0 <= sr_idx <= 2:
                    bitrate = _MP3_BITRATES[br_idx] if br_idx <= 14 else None
                    sample_rate = _MP3_RATES[sr_idx]
                    br_pos, br_sr, br_bt = pos, sample_rate, bitrate
                    break
            pos += 1

        if bitrate is None or sample_rate is None:
            return None

        pos = br_pos
        while pos + 4 <= len(buf):
            if buf[pos:pos + 2] != b"\xff\xfb" and buf[pos:pos + 2] not in (b"\xff\xe2", b"\xff\xf2"):
                pos += 1
                continue
            size_bits = (buf[pos + 1] & 0x03) << 11 | buf[pos + 2] << 3 | buf[pos + 3] >> 5
            frame_size = math.floor(size_bits * bitrate / (8 * sample_rate)) * 4
            if frame_size <= 0:
                break
            frames += 1
            pos += frame_size
            if frames > 1_000_000:
                break
        if frames == 0:
            return None
        return frames * 1152 / sample_rate
    except Exception:
        return None


_MP3_BITRATES = [0, 32000, 40000, 48000, 56000, 64000, 80000, 96000,
                 112000, 128000, 160000, 192000, 224000, 256000, 320000]
_MP3_RATES = [44100, 48000, 32000]


def _syncsafe(b: bytes) -> int:
    return (b[0] << 21) | (b[1] << 14) | (b[2] << 7) | b[3]


# ---------------------------------------------------------------------------
# 文件元信息汇总
# ---------------------------------------------------------------------------
def describe_file(p: str | os.PathLike) -> dict[str, str]:
    """返回 {大小, 尺寸/时长} 描述，用于资产详情面板。"""
    p = Path(p)
    if not p.exists():
        return {"status": "文件不存在"}
    out: dict[str, str] = {}
    out["size"] = _human_size(p.stat().st_size)
    if is_image_file(p):
        sz = image_size(p)
        if sz:
            out["dimensions"] = f"{sz[0]} × {sz[1]}"
    elif is_audio_file(p):
        d = audio_duration(p)
        if d is not None:
            out["duration"] = _format_duration(d)
    return out


def text_preview(content: str, limit: int = TEXT_PREVIEW_LEN) -> str:
    text = (content or "").strip()
    if len(text) <= limit:
        return text
    return text[:limit].rstrip() + "…"


def _human_size(n: int) -> str:
    units = ["B", "KB", "MB", "GB"]
    i = 0
    f = float(n)
    while f >= 1024 and i < len(units) - 1:
        f /= 1024
        i += 1
    return f"{f:.1f} {units[i]}" if i else f"{int(f)} B"


def _format_duration(sec: float) -> str:
    s = int(round(sec))
    m, sec = divmod(s, 60)
    h, m = divmod(m, 60)
    return f"{h}:{m:02d}:{sec:02d}" if h else f"{m:02d}:{sec:02d}"
