#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
offline_bg.quality — ④ 把 AI 背景「降级」到与摄像头一致（色彩 → 解模糊 → 加噪）。

🔴 顺序铁律：色彩迁移必须在解模糊**之前**（它改对比度 → 改 Laplacian 方差 →
   会把刚解好的模糊系数冲掉；搞反过一次，误差 67.2%）。这条由 validated
   实现内部保证，本模块只负责"在正确的分辨率上、用正确的参考帧"调用它。

🔴 分辨率铁律：模糊 σ 是分辨率相关量。必须先把背景放大到**成片分辨率**
   （= 摄像头帧尺寸）再做匹配，不能在 IC-Light 的 768×448 上解 σ 再放大。

实测：AI 背景锐度 269.0 vs 摄像头 16.2（软 16.6 倍），解出 σ=1.244，误差 1.4%。
"""
from typing import Dict, Optional, Tuple

import cv2
import numpy as np

from .config import DEFAULT, OfflineBGConfig
from .lighting import CameraProfile
from .validated import color_transfer, laplacian_sharpness


def textured_sharpness(img: np.ndarray, mask=None, block: int = 64, pct: float = 80.0) -> float:
    """按块算 Laplacian 方差，取高分位作为「有纹理处的锐度」。

    🔴 为什么不用全图 Laplacian 方差：它衡量的是"画面里有多少高频"，
       和**内容量**强耦合。摄像头背景若是一面平坦白墙，全图值极低（实测影棚布 2.59），
       那反映的是"没东西可拍"，不是"镜头软"。拿它当目标会把有内容的 AI 背景
       过度模糊成一团雾（实测 A 用例成品就是这样）。
       改成按块取高分位，比较的是"两边各自有纹理的地方有多锐"，物理上才可比。
    """
    g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32)
    l = cv2.Laplacian(g, cv2.CV_32F)
    h, w = l.shape
    vals = []
    for y in range(0, h - block + 1, block):
        for x in range(0, w - block + 1, block):
            if mask is not None and mask[y:y + block, x:x + block].mean() < 0.9:
                continue                       # 块里混进人物就不要，避免污染基准
            vals.append(l[y:y + block, x:x + block].var())
    if not vals:
        return float(l[mask].var() if mask is not None and mask.any() else l.var())
    return float(np.percentile(vals, pct))


def noise_laplacian(sigma: float, seed: int = 0, n: int = 256) -> float:
    """实测「纯噪声」自身贡献多少 Laplacian 方差（含 uint8 量化那一块）。
    不用解析式：理论 20×(0.669σ)² 只能对上一半，量化项还得再加（实测 4.47 vs 理论 2.77）。"""
    base = np.full((n, n, 3), 128, np.uint8)
    rng = np.random.default_rng(seed)
    o = np.clip(base.astype(np.float32) + rng.normal(0, sigma, base.shape), 0, 255).astype(np.uint8)
    return laplacian_sharpness(o)


def _cap_noise(sigma: float, budget: float, seed: int = 0) -> float:
    """把注入噪声压到「其 Laplacian 贡献不超过 budget」。两次实测足够收敛。"""
    if sigma <= 0 or budget <= 0:
        return 0.0
    nl = noise_laplacian(sigma, seed)
    if nl <= budget:
        return sigma
    for _ in range(2):                      # var_L ≈ a·σ²+b，按平方根缩一次就很接近
        sigma *= float(np.sqrt(max(budget / max(nl, 1e-6), 1e-6)))
        nl = noise_laplacian(sigma, seed)
        if nl <= budget:
            break
    return sigma


def match_quality(bg_bgr: np.ndarray, profile: CameraProfile,
                  cfg: OfflineBGConfig = DEFAULT, seed: int = 0,
                  target_size: Optional[Tuple[int, int]] = None
                  ) -> Tuple[np.ndarray, Dict]:
    """把背景降级到与摄像头画质一致。返回 (结果图, 诊断)。

    🔴 匹配一律在**摄像头原生分辨率**上完成：锐度是分辨率相关量，只有同分辨率
       比较才有意义。target_size 与摄像头分辨率不同时，最后再缩到 target，
       并在诊断里记下"锐度是在摄像头分辨率上对齐的"——不要以为缩放后还精确。

    噪声不交给 validated 内部加：profile.noise_sigma 是在全部连续帧上估的
    （比单次调用现估更稳），且 config.noise_gain 的校正只在一处生效。
    """
    if profile.ref_frame is None:
        raise ValueError("profile 缺 ref_frame，无法做画质匹配")

    th, tw = profile.ref_frame.shape[:2]
    if bg_bgr.shape[:2] != (th, tw):
        # 放大背景去对齐摄像头，绝不缩放摄像头帧——那会改变它的锐度基准
        bg_bgr = cv2.resize(bg_bgr, (tw, th), interpolation=cv2.INTER_CUBIC)

    sigma = float(profile.noise_sigma)
    # 度量函数两边必须同一个：摄像头基准与背景都用「有纹理块的高分位锐度」
    if cfg.sharp_metric == "textured":
        def sharp(im, m=None):
            return textured_sharpness(im, m, cfg.sharp_block, cfg.sharp_pct)
    else:
        sharp = laplacian_sharpness
    target = sharp(profile.ref_frame, profile.bg_mask if cfg.sharp_use_bg_mask else None)

    # 🔴 时域估出的 σ 不能直接当「白噪声」注入。摄像头噪声经过编码器空间低通后
    #    高频能量已被削掉，而白噪声全是高频：实测影棚素材 σ=0.556 时，光噪声一项的
    #    Laplacian 方差就 ≈5.4，已经超过整帧背景的 4.26 —— 目标直接变得不可达，
    #    模糊打到上限也追不回来。所以按实测给注入量封顶（不是按理论系数）。
    sigma = _cap_noise(sigma, target * cfg.noise_lap_budget, seed)
    add_noise = sigma > cfg.noise_min_sigma

    # 🔴 色彩迁移必须在解模糊之前（它改对比度→改 Laplacian 方差）。
    #    cam_mask 把人物排除在统计之外，否则背景会被染上肤色/被主体对比度带偏。
    bg_c = color_transfer(bg_bgr, profile.ref_frame, profile.bg_mask)

    # 🔴 加噪必须**在搜索回路内**，不能"先解模糊再加噪"：噪声本身会把 Laplacian
    #    方差顶上去，摄像头背景越平顶得越离谱（实测影棚白墙 target=2.52，
    #    加 σ=0.51 的噪声后成品 6.44，误差 148.9%）。
    #    也别改成"目标里扣掉噪声的解析贡献"：理论值 20×(0.669σ)² 只对上一半，
    #    uint8 量化还会再贡献一块（实测 4.47 vs 理论 2.77）。搜索包含加噪这一步，
    #    按定义就不需要任何系数。
    rng_seed = seed

    def render(bs):
        o = cv2.GaussianBlur(bg_c, (0, 0), bs) if bs > 0.05 else bg_c.copy()
        if add_noise:
            rng = np.random.default_rng(rng_seed)
            o = np.clip(o.astype(np.float32) + rng.normal(0, sigma, o.shape),
                        0, 255).astype(np.uint8)
        return o

    lo, hi = 0.0, 8.0
    for _ in range(14):
        mid = (lo + hi) / 2
        if sharp(render(mid)) > target:
            lo = mid
        else:
            hi = mid
    blur_sigma = (lo + hi) / 2
    out = render(blur_sigma)
    sharp_after_blur = sharp(
        cv2.GaussianBlur(bg_c, (0, 0), blur_sigma) if blur_sigma > 0.05 else bg_c)
    diag = {"blur_sigma": blur_sigma}

    final = sharp(out)
    target_blur = sharp_after_blur      # 诊断用：加噪前实际到达的锐度
    err = abs(final - target) / max(target, 1e-6)
    d = {
        "size": [tw, th],
        "target_sharp": round(target, 3),
        "sharp_before_noise": round(target_blur, 3),
        "blur_sigma": round(float(diag["blur_sigma"]), 4),
        "sharp_after_blur": round(sharp_after_blur, 3),
        "noise_sigma": round(sigma, 4),
        "final_sharp": round(final, 3),
        "sharp_err": round(err, 4),
        "warn": "",
    }
    if err > cfg.sharp_err_warn:
        d["warn"] = (f"锐度误差 {err*100:.1f}% 超过 {cfg.sharp_err_warn*100:.0f}%；"
                     f"目标 {target:.2f}，加噪前 {sharp_after_blur:.2f} → 成品 {final:.2f}"
                     f"（σ={sigma:.2f}，模糊 {blur_sigma:.2f}）；"
                     f"背景本身若已比摄像头更软，再模糊也追不上目标")

    if target_size and tuple(target_size) != (tw, th):
        out = cv2.resize(out, tuple(target_size), interpolation=cv2.INTER_CUBIC)
        d["resized_to"] = list(target_size)
        d["note"] = "锐度/噪声是在摄像头分辨率上对齐的，缩放到 target_size 后不再精确"
    return out, d


def quality_report(bg_before: np.ndarray, bg_after: np.ndarray,
                   profile: CameraProfile) -> Dict:
    """给人看的前后对比（落库存档用）。"""
    return {
        "bg_sharp_before": round(laplacian_sharpness(bg_before), 2),
        "bg_sharp_after": round(laplacian_sharpness(bg_after), 2),
        "camera_sharp": round(profile.sharpness, 2),
        "ratio_before": round(laplacian_sharpness(bg_before) / max(profile.sharpness, 1e-6), 2),
        "ratio_after": round(laplacian_sharpness(bg_after) / max(profile.sharpness, 1e-6), 2),
    }
