#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
offline_bg.library — ⑤ 用户背景库。

目录布局（每个用户一个 manifest，实时管线只读 final.png）：
    <root>/<user_id>/manifest.json
    <root>/<user_id>/<bg_id>/original.png   原始背景（未处理）
    <root>/<user_id>/<bg_id>/relit.png      IC-Light 重打光后（未画质匹配）
    <root>/<user_id>/<bg_id>/final.png      ★ 成品，实时合成直接用这张
    <root>/<user_id>/<bg_id>/record.json    完整加工记录（画像/校验分数/诊断/耗时）

manifest 只放检索需要的字段，细节在各条目的 record.json 里，避免 manifest 膨胀。
写 manifest 走 临时文件 + os.replace（原子），避免写一半崩掉留下坏 json。
"""
import json
import os
import shutil
import threading
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional

import cv2
import numpy as np

from .config import DEFAULT, OfflineBGConfig
from .lighting import CameraProfile

_LOCK = threading.Lock()
MANIFEST_VERSION = 1


def _now() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


# 🔴 cv2.imwrite/imread 在 Windows 上遇到非 ASCII 路径会静默失败（或写出乱码文件名）。
#    库路径里会出现 user_id，用户名/目录含中文就中招 → 一律走 imencode + 字节读写。
def imwrite(path: str, img: np.ndarray, ext: str = ".png") -> None:
    ok, buf = cv2.imencode(ext, img)
    if not ok:
        raise IOError(f"编码失败: {path}")
    with open(path, "wb") as f:
        f.write(buf.tobytes())


def imread(path: str) -> Optional[np.ndarray]:
    if not os.path.exists(path):
        return None
    return cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)


class BackgroundLibrary:
    def __init__(self, root: Optional[str] = None, cfg: OfflineBGConfig = DEFAULT):
        self.root = Path(root or cfg.library_root)
        self.cfg = cfg
        self.root.mkdir(parents=True, exist_ok=True)

    # ---------------- 内部 ----------------
    def _user_dir(self, user_id: str) -> Path:
        d = self.root / str(user_id)
        d.mkdir(parents=True, exist_ok=True)
        return d

    def _manifest_path(self, user_id: str) -> Path:
        return self._user_dir(user_id) / "manifest.json"

    def _load(self, user_id: str) -> Dict:
        p = self._manifest_path(user_id)
        if not p.exists():
            return {"version": MANIFEST_VERSION, "user_id": str(user_id), "items": []}
        try:
            return json.loads(p.read_text("utf-8"))
        except json.JSONDecodeError:              # 坏文件不静默吞：改名留证据再重建
            p.rename(p.with_suffix(".json.broken"))
            return {"version": MANIFEST_VERSION, "user_id": str(user_id), "items": []}

    def _save(self, user_id: str, man: Dict) -> None:
        p = self._manifest_path(user_id)
        tmp = p.with_suffix(".json.tmp")
        tmp.write_text(json.dumps(man, ensure_ascii=False, indent=2), "utf-8")
        os.replace(tmp, p)                        # 原子替换

    # ---------------- 对外 ----------------
    def add(self, user_id: str, images: Dict[str, np.ndarray], record: Dict,
            name: Optional[str] = None, bg_id: Optional[str] = None) -> Dict:
        """落库。images 至少要有 'final'；'original'/'relit' 按 cfg.keep_intermediate 存。"""
        if "final" not in images:
            raise ValueError("images 必须包含 'final'")
        bg_id = bg_id or uuid.uuid4().hex[:12]
        d = self._user_dir(user_id) / bg_id
        d.mkdir(parents=True, exist_ok=True)

        files = {}
        for key, im in images.items():
            if key != "final" and not self.cfg.keep_intermediate:
                continue
            fp = d / f"{key}.png"
            imwrite(str(fp), im)
            files[key] = str(fp.relative_to(self.root)).replace("\\", "/")

        record = dict(record)
        record["bg_id"] = bg_id
        record["files"] = files
        record["created_at"] = _now()
        (d / "record.json").write_text(json.dumps(record, ensure_ascii=False, indent=2), "utf-8")

        light = record.get("target_light", {}) or {}
        entry = {
            "bg_id": bg_id,
            "name": name or bg_id,
            "created_at": record["created_at"],
            "cls": light.get("cls", "front"),
            "dx": light.get("dx", 0.0),
            "dy": light.get("dy", 0.0),
            "relit": bool(record.get("relit", False)),
            "validated": bool(record.get("validated", False)),
            "signed_delta": (record.get("direction_score") or {}).get("signed_delta"),
            "sharp_err": (record.get("quality") or {}).get("sharp_err"),
            "final": files["final"],
            # 🔴 files 必须回到 entry 里：add() 内部对 record 做了 dict() 复制，
            #    调用方手上的 record 拿不到 files，只能从 entry 取中间产物路径。
            "files": files,
        }
        with _LOCK:
            man = self._load(user_id)
            man["items"] = [i for i in man["items"] if i["bg_id"] != bg_id] + [entry]
            self._save(user_id, man)
        return entry

    def list(self, user_id: str, cls: Optional[str] = None,
             validated_only: bool = False) -> List[Dict]:
        items = self._load(user_id)["items"]
        if cls:
            items = [i for i in items if i["cls"] == cls]
        if validated_only:
            items = [i for i in items if i.get("validated")]
        return sorted(items, key=lambda i: i["created_at"], reverse=True)

    def get(self, user_id: str, bg_id: str) -> Optional[Dict]:
        p = self._user_dir(user_id) / bg_id / "record.json"
        return json.loads(p.read_text("utf-8")) if p.exists() else None

    def abspath(self, rel: str) -> str:
        return str(self.root / rel)

    def load_final(self, user_id: str, bg_id: str) -> Optional[np.ndarray]:
        e = next((i for i in self._load(user_id)["items"] if i["bg_id"] == bg_id), None)
        return imread(self.abspath(e["final"])) if e else None

    def pick(self, user_id: str, profile: CameraProfile,
             prefer_validated: bool = True) -> Optional[Dict]:
        """会话开始时按当前摄像头光照挑一张最匹配的背景。

        排序键：同类优先 → 已通过校验优先 → (dx,dy) 欧氏距离近的优先。
        """
        items = self._load(user_id)["items"]
        if not items:
            return None

        def key(i):
            same_cls = 0 if i["cls"] == profile.cls else 1
            unval = 0 if (i.get("validated") or not prefer_validated) else 1
            dist = float(np.hypot(i.get("dx", 0.0) - profile.dx, i.get("dy", 0.0) - profile.dy))
            return (same_cls, unval, dist)

        return sorted(items, key=key)[0]

    def remove(self, user_id: str, bg_id: str) -> bool:
        d = self._user_dir(user_id) / bg_id
        with _LOCK:
            man = self._load(user_id)
            before = len(man["items"])
            man["items"] = [i for i in man["items"] if i["bg_id"] != bg_id]
            self._save(user_id, man)
        if d.exists():
            shutil.rmtree(d, ignore_errors=True)
        return len(man["items"]) < before
