"""
Stable Diffusion 服务模块
封装对 SD WebUI API 的调用，支持 txt2img 和 img2img，支持 ReActor 换脸
含图片反 AI 检测后处理管线
"""
import requests
import base64
import io
import logging
import os
import random
import math
import struct
import zlib
from PIL import Image, ImageFilter, ImageEnhance

logger = logging.getLogger(__name__)

SD_TIMEOUT = 1800  # 图片生成可能需要较长时间

# 头像文件默认保存路径
FACE_IMAGE_PATH = os.path.join(os.path.dirname(__file__), "my_face.png")

# ==================== 多模型配置系统 ====================
# 每个模型的最优参数、prompt 增强词、负面提示词、三档预设

SD_MODEL_PROFILES = {
    # ---- majicmixRealistic: 东亚网红感，朋友圈自拍/美妆/穿搭 (SD 1.5) ----
    "majicmixRealistic": {
        "display_name": "majicmixRealistic ⭐⭐⭐⭐⭐",
        "description": "东亚网红感 | 朋友圈自拍、美妆、穿搭",
        "arch": "sd15",  # SD 1.5 架构
        # 自动追加到 prompt 前面的增强词
        "prompt_prefix": (
            "(best quality:1.4), (masterpiece:1.4), (ultra detailed:1.3), "
            "(photorealistic:1.4), (realistic:1.3), raw photo, "
            "(asian girl:1.3), (chinese:1.2), (east asian features:1.2), "
            "(delicate facial features:1.2), (fair skin:1.1), (natural skin texture:1.2), "
            "(soft lighting:1.1), (natural makeup:1.1), "
        ),
        # 自动追加到 prompt 后面的补充词
        "prompt_suffix": (
            ", film grain, shallow depth of field, "
            "instagram aesthetic, xiaohongshu style, phone camera feel"
        ),
        "negative_prompt": (
            "(nsfw:1.5), (nudity:1.5), (worst quality:2), (low quality:2), (normal quality:2), "
            "lowres, bad anatomy, bad hands, text, error, missing fingers, "
            "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, "
            "blurry, deformed, mutated, disfigured, ugly, duplicate, "
            "poorly drawn face, poorly drawn hands, extra limbs, fused fingers, "
            "too many fingers, long neck, out of frame, "
            "western face, european face, caucasian, deep-set eyes, high nose bridge, "
            "blonde hair, red hair, blue eyes, green eyes, freckles, thick body hair, "
            "painting, cartoon, anime, sketch, illustration, 3d render"
        ),
        "presets": {
            "快速 (约30秒)": {
                "steps": 20,
                "cfg_scale": 7.0,
                "width": 512,
                "height": 768,
                "sampler_name": "Euler a",
                "scheduler": "Normal",
                "batch_size": 2,
            },
            "标准 (约1分钟)": {
                "steps": 30,
                "cfg_scale": 7.0,
                "width": 512,
                "height": 768,
                "sampler_name": "DPM++ 2M",
                "scheduler": "Karras",
                "batch_size": 2,
            },
            "精细 (约2-3分钟)": {
                "steps": 40,
                "cfg_scale": 7.5,
                "width": 576,
                "height": 864,
                "sampler_name": "DPM++ SDE",
                "scheduler": "Karras",
                "batch_size": 2,
            },
        },
    },

    # ---- Realistic Vision: 写实摄影感，纪实摄影/街拍/真实质感 (SD 1.5) ----
    "realisticVision": {
        "display_name": "Realistic Vision ⭐⭐⭐⭐",
        "description": "写实摄影感 | 纪实摄影、街拍、真实质感",
        "arch": "sd15",
        "prompt_prefix": (
            "RAW photo, (best quality:1.4), (masterpiece:1.3), (realistic:1.4), "
            "(photorealistic:1.4), 8k uhd, DSLR, high quality, "
            "(asian:1.2), (chinese girl:1.2), (east asian features:1.1), "
            "(natural skin:1.2), (skin pores:1.1), (detailed skin texture:1.2), "
        ),
        "prompt_suffix": (
            ", shot on Canon EOS R5, 85mm lens, f/1.8, "
            "natural lighting, documentary style, street photography, "
            "film color grading, depth of field"
        ),
        "negative_prompt": (
            "(nsfw:1.5), (nudity:1.5), (worst quality:2), (low quality:2), (normal quality:2), "
            "lowres, bad anatomy, bad hands, text, error, missing fingers, "
            "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, "
            "blurry, deformed, mutated, disfigured, ugly, duplicate, "
            "poorly drawn face, extra limbs, fused fingers, long neck, "
            "western face, european face, caucasian, deep-set eyes, "
            "blonde hair, blue eyes, green eyes, freckles, "
            "painting, cartoon, anime, sketch, illustration, 3d render, "
            "over-sharpened, over-saturated, plastic skin, airbrushed, "
            "smooth skin, doll-like, HDR, overprocessed"
        ),
        "presets": {
            "快速 (约30秒)": {
                "steps": 20,
                "cfg_scale": 7.0,
                "width": 512,
                "height": 768,
                "sampler_name": "Euler a",
                "scheduler": "Normal",
                "batch_size": 2,
            },
            "标准 (约1分钟)": {
                "steps": 28,
                "cfg_scale": 7.0,
                "width": 512,
                "height": 768,
                "sampler_name": "DPM++ 2M",
                "scheduler": "Karras",
                "batch_size": 2,
            },
            "精细 (约2-3分钟)": {
                "steps": 40,
                "cfg_scale": 7.5,
                "width": 576,
                "height": 864,
                "sampler_name": "DPM++ SDE",
                "scheduler": "Karras",
                "batch_size": 2,
            },
        },
    },

    # ---- Juggernaut XL: 电影大片感，高画质/商业摄影/复杂背景 (SDXL) ----
    "juggernautXL": {
        "display_name": "Juggernaut XL ⭐⭐⭐⭐",
        "description": "电影大片感 | 高画质、商业摄影、复杂背景",
        "arch": "sdxl",  # SDXL 架构
        "prompt_prefix": (
            "masterpiece, best quality, ultra detailed, 8k uhd, high resolution, "
            "photorealistic, cinematic lighting, cinematic composition, "
            "asian girl, chinese, east asian features, black hair, dark brown eyes, "
            "delicate facial features, fair skin, slim figure, "
        ),
        "prompt_suffix": (
            ", cinematic color grading, anamorphic lens, bokeh, "
            "volumetric lighting, ray tracing, global illumination, "
            "commercial photography, editorial style, vogue aesthetic"
        ),
        "negative_prompt": (
            "nsfw, nudity, lowres, bad anatomy, bad hands, text, error, missing fingers, "
            "extra digit, fewer digits, cropped, worst quality, low quality, normal quality, "
            "jpeg artifacts, signature, watermark, blurry, deformed, mutated, disfigured, "
            "ugly, duplicate, morbid, mutilated, poorly drawn face, poorly drawn hands, "
            "extra limbs, fused fingers, too many fingers, long neck, username, "
            "out of frame, distorted, oversaturated, underexposed, overexposed, "
            "western face, european face, caucasian, deep-set eyes, high nose bridge, "
            "blonde hair, red hair, blue eyes, green eyes, freckles, thick body hair"
        ),
        "presets": {
            "快速 (约30秒)": {
                "steps": 12,
                "cfg_scale": 5.0,
                "width": 768,
                "height": 1024,
                "sampler_name": "Euler a",
                "scheduler": "Normal",
                "batch_size": 2,
            },
            "标准 (约1分钟)": {
                "steps": 20,
                "cfg_scale": 5.5,
                "width": 832,
                "height": 1216,
                "sampler_name": "DPM++ 2M",
                "scheduler": "Karras",
                "batch_size": 2,
            },
            "精细 (约2-3分钟)": {
                "steps": 35,
                "cfg_scale": 6.0,
                "width": 832,
                "height": 1216,
                "sampler_name": "DPM++ 2M SDE",
                "scheduler": "Karras",
                "batch_size": 2,
            },
        },
    },
}

# ==================== 人设 SD 视觉配置 ====================
# 每个人设对应一组 SD prompt 增强词 + 视觉风格 + LLM 绘图指导
# key 匹配规则: persona 文本中包含 key 即命中

PERSONA_SD_PROFILES = {
    # ---- 赛博AI虚拟博主 ----
    "赛博AI虚拟博主": {
        # 追加到 SD prompt 前面的人设特有增强词
        "prompt_boost": (
            "(perfect face:1.3), (extremely detailed face:1.3), (beautiful detailed eyes:1.3), "
            "(flawless skin:1.2), (ultra high resolution face:1.2), (glossy lips:1.1), "
        ),
        # 追加到 SD prompt 后面的风格词
        "prompt_style": (
            ", cinematic portrait, fashion editorial, vibrant colors, "
            "dramatic lighting, creative background, fantasy environment, "
            "instagram influencer, trending on artstation"
        ),
        # 额外追加到负面提示词
        "negative_extra": "",
        # LLM 绘图指导 (追加到 sd_prompt_guide)
        "llm_guide": (
            "\n\n【人设视觉风格 - 赛博AI虚拟博主】\n"
            "你在为一个大方承认自己是AI的虚拟博主生成图片，主打高颜值+场景奇观：\n"
            "- 五官极致精致：重点描述 detailed eyes, long eyelashes, glossy lips, perfect skin\n"
            "- 场景大胆奇幻：巴黎埃菲尔铁塔前、东京霓虹街头、赛博朋克城市、外太空空间站、水下宫殿、樱花隧道等\n"
            "- 光效华丽：neon lights, cyberpunk glow, holographic, lens flare, volumetric fog\n"
            "- 穿搭多变：可大胆使用 futuristic outfit, holographic dress, cyberpunk jacket, maid outfit, school uniform, wedding dress 等\n"
            "- 构图要有视觉冲击力：close-up portrait, dynamic angle, full body shot, looking at viewer\n"
            "- 整体风格：超现实+高颜值，不需要追求真实感，要追求视觉震撼\n"
        ),
    },
    # ---- 性感福利主播 ----
    "性感福利主播": {
        "prompt_boost": (
            "(beautiful detailed face:1.3), (seductive eyes:1.2), (glossy lips:1.2), "
            "(perfect body proportions:1.2), (slim waist:1.2), (long legs:1.2), "
            "(glamour photography:1.3), "
        ),
        "prompt_style": (
            ", soft glamour lighting, beauty retouching, "
            "intimate atmosphere, warm golden tones, shallow depth of field, "
            "boudoir style, sensual but tasteful, fashion model pose"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 性感福利主播】\n"
            "为一个身材曼妙的时尚博主生成图片，主打身材美感+氛围感：\n"
            "- 身材描述：slim waist, long legs, perfect figure, hourglass body, graceful pose\n"
            "- 穿搭关键：lingerie, bikini, bodycon dress, off-shoulder, backless dress, lace, sheer fabric\n"
            "- 光线氛围：soft warm lighting, golden hour, window light, candle light, intimate mood\n"
            "- 场景选择：bedroom, luxury hotel, swimming pool, beach sunset, mirror selfie\n"
            "- 构图要点：full body or three-quarter shot, emphasize curves, elegant pose, looking at viewer\n"
            "- 整体风格：glamour photography，魅力但有品位，不要低俗\n"
        ),
    },
    # ---- 身材管理健身美女 ----
    "身材管理健身美女": {
        "prompt_boost": (
            "(fit body:1.3), (athletic physique:1.2), (toned muscles:1.2), "
            "(healthy glow:1.2), (confident pose:1.2), "
        ),
        "prompt_style": (
            ", fitness photography, gym environment, athletic wear, "
            "dynamic pose, energetic mood, healthy lifestyle, "
            "motivational, natural sweat, workout aesthetic"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 身材管理健身美女】\n"
            "为一个健身达人生成图片，主打健康美+运动感：\n"
            "- 身材描述：fit body, toned abs, lean muscles, athletic build, healthy skin\n"
            "- 穿搭关键：sports bra, yoga pants, running outfit, gym wear, crop top\n"
            "- 场景选择：gym, yoga studio, outdoor running, home workout, mirror selfie at gym\n"
            "- 动作姿势：workout pose, stretching, running, yoga pose, flexing, plank\n"
            "- 光线：gym lighting, natural daylight, morning sun, energetic bright tones\n"
            "- 整体风格：充满活力的运动健身风，展现自律和力量美\n"
        ),
    },
    # ---- 温柔知性的时尚博主 ----
    "温柔知性时尚博主": {
        "prompt_boost": (
            "(elegant:1.2), (gentle expression:1.2), (sophisticated:1.2), "
            "(fashion forward:1.2), (graceful:1.1), "
        ),
        "prompt_style": (
            ", fashion editorial, street style photography, "
            "chic outfit, elegant pose, soft natural tones, "
            "magazine cover quality, lifestyle photography"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 温柔知性时尚博主】\n"
            "为一个知性优雅的时尚博主生成图片，主打高级感穿搭：\n"
            "- 气质描述：elegant, gentle smile, sophisticated, poised, graceful\n"
            "- 穿搭关键：french style, minimalist outfit, trench coat, silk blouse, midi skirt, neutral colors\n"
            "- 场景选择：cafe, art gallery, tree-lined street, bookstore, european architecture\n"
            "- 构图：three-quarter shot, walking pose, looking away candidly, holding coffee\n"
            "- 色调：warm neutral tones, soft creamy, muted colors, film aesthetic\n"
            "- 整体风格：法式优雅+知性温柔，像时尚杂志的生活方式大片\n"
        ),
    },
    # ---- 文艺青年摄影师 ----
    "文艺青年摄影师": {
        "prompt_boost": (
            "(artistic:1.2), (moody atmosphere:1.2), (film grain:1.2), "
            "(vintage tones:1.1), "
        ),
        "prompt_style": (
            ", film photography, 35mm film, kodak portra 400, "
            "vintage color grading, indie aesthetic, dreamy atmosphere, "
            "golden hour, nostalgic mood"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 文艺青年摄影师】\n"
            "为文艺摄影师生成图片，主打胶片感+故事性：\n"
            "- 风格关键：film grain, vintage tones, kodak portra, dreamy, nostalgic\n"
            "- 场景选择：old streets, abandoned places, cafe corner, train station, seaside, flower field\n"
            "- 光线：golden hour, window light, overcast soft light, dappled sunlight\n"
            "- 构图：off-center composition, back view, silhouette, reflection\n"
            "- 色调：warm vintage, faded colors, low contrast, film color grading\n"
            "- 整体风格：独立电影画面感，有故事性的文艺氛围\n"
        ),
    },
    # ---- 二次元coser ----
    "二次元coser": {
        "prompt_boost": (
            "(cosplay:1.3), (detailed costume:1.2), (colorful:1.2), "
            "(anime inspired:1.1), (vibrant:1.2), "
        ),
        "prompt_style": (
            ", cosplay photography, anime convention, colorful costume, "
            "dynamic pose, vibrant colors, fantasy setting, "
            "dramatic lighting, character portrayal"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 二次元coser】\n"
            "为cos博主生成图片，主打二次元还原+视觉冲击：\n"
            "- 风格关键：cosplay, detailed costume, colorful wig, contact lenses, anime style\n"
            "- 场景选择：anime convention, fantasy landscape, school rooftop, cherry blossoms, studio backdrop\n"
            "- 动作姿势：character pose, dynamic action, cute pose, peace sign, holding prop\n"
            "- 光效：colored lighting, rim light, sparkle effects, dramatic shadows\n"
            "- 色调：vibrant saturated, anime color palette, high contrast\n"
            "- 整体风格：真人cos感，兼具二次元的鲜艳感和真实摄影的质感\n"
        ),
    },
    # ---- 汉服爱好者 ----
    "汉服爱好者": {
        "prompt_boost": (
            "(traditional chinese dress:1.3), (hanfu:1.3), (chinese aesthetic:1.2), "
            "(elegant traditional:1.2), (delicate accessories:1.1), "
        ),
        "prompt_style": (
            ", traditional chinese photography, han dynasty style, "
            "ink painting aesthetic, bamboo forest, ancient architecture, "
            "flowing silk fabric, classical beauty, ethereal atmosphere"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 汉服爱好者】\n"
            "为国风汉服博主生成图片，主打古典美+中国风：\n"
            "- 服饰描述：hanfu, flowing silk robes, wide sleeves, hair accessories, jade earrings, fan\n"
            "- 场景选择：bamboo forest, ancient temple, moon gate, lotus pond, plum blossom, mountain mist\n"
            "- 光线：soft diffused light, misty atmosphere, morning fog, moonlight, lantern glow\n"
            "- 构图：full body flowing fabric, profile view, looking down gently, holding umbrella\n"
            "- 色调：muted earth tones, ink wash style, red and white contrast, jade green\n"
            "- 整体风格：仙气飘飘的古风摄影，有水墨画的意境\n"
        ),
    },
    # ---- 独居女孩 ----
    "独居女孩": {
        "prompt_boost": (
            "(cozy atmosphere:1.3), (warm lighting:1.2), (homey:1.2), "
            "(casual style:1.1), (relaxed:1.1), "
        ),
        "prompt_style": (
            ", cozy home photography, warm ambient light, "
            "casual indoor style, hygge aesthetic, "
            "soft blanket, candle light, peaceful morning"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 独居女孩】\n"
            "为独居生活博主生成图片，主打温馨氛围感+仪式感：\n"
            "- 氛围关键：cozy, warm, hygge, peaceful, intimate, ambient candlelight\n"
            "- 场景选择：small apartment, kitchen cooking, bathtub, reading by window, balcony garden\n"
            "- 穿搭关键：oversized sweater, pajamas, casual homewear, messy bun\n"
            "- 光线：warm lamp light, candle glow, morning window light, fairy lights\n"
            "- 道具：coffee mug, book, cat, houseplant, scented candle, blanket\n"
            "- 整体风格：温暖治愈的独居日常，有仪式感的精致生活\n"
        ),
    },
    # ---- 资深美妆博主 ----
    "资深美妆博主": {
        "prompt_boost": (
            "(flawless makeup:1.3), (detailed eye makeup:1.3), (beauty close-up:1.2), "
            "(perfect skin:1.2), (beauty lighting:1.2), "
        ),
        "prompt_style": (
            ", beauty photography, ring light, macro lens, "
            "studio beauty lighting, makeup tutorial style, "
            "dewy skin, perfect complexion, vibrant lip color"
        ),
        "negative_extra": "",
        "llm_guide": (
            "\n\n【人设视觉风格 - 资深美妆博主】\n"
            "为美妆博主生成图片，主打妆容特写+产品展示：\n"
            "- 妆容描述：detailed eye shadow, winged eyeliner, glossy lips, dewy foundation, blush\n"
            "- 构图要点：face close-up, half face, eye close-up, lip close-up, before and after\n"
            "- 场景选择：vanity desk, bathroom mirror, ring light studio, flat lay of products\n"
            "- 光线：ring light, beauty dish, soft diffused studio light, bright even lighting\n"
            "- 色调：clean bright, pink tones, neutral with pops of color\n"
            "- 整体风格：专业美妆教程感，妆容细节清晰可见\n"
        ),
    },
}


def get_persona_sd_profile(persona_text: str) -> dict | None:
    """根据人设文本匹配 SD 视觉配置，返回 profile dict 或 None"""
    if not persona_text:
        return None
    for key, profile in PERSONA_SD_PROFILES.items():
        if key in persona_text:
            return profile
    return None


# 默认配置 profile key
DEFAULT_MODEL_PROFILE = "juggernautXL"


def detect_model_profile(model_name: str) -> str:
    """根据 SD 模型名称自动识别对应的 profile key"""
    name_lower = model_name.lower() if model_name else ""
    if "majicmix" in name_lower or "majic" in name_lower:
        return "majicmixRealistic"
    elif "realistic" in name_lower and "vision" in name_lower:
        return "realisticVision"
    elif "rv" in name_lower and ("v5" in name_lower or "v6" in name_lower or "v4" in name_lower):
        return "realisticVision"  # RV v5.1 等简写
    elif "juggernaut" in name_lower or "jugger" in name_lower:
        return "juggernautXL"
    # 根据架构猜测
    elif "xl" in name_lower or "sdxl" in name_lower:
        return "juggernautXL"  # SDXL 架构默认用 Juggernaut 参数
    else:
        return DEFAULT_MODEL_PROFILE  # 无法识别时默认


def get_model_profile(model_name: str = None) -> dict:
    """获取模型配置 profile"""
    key = detect_model_profile(model_name) if model_name else DEFAULT_MODEL_PROFILE
    return SD_MODEL_PROFILES.get(key, SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE])


def get_model_profile_info(model_name: str = None) -> str:
    """获取当前模型的显示信息 (Markdown 格式)"""
    profile = get_model_profile(model_name)
    key = detect_model_profile(model_name) if model_name else DEFAULT_MODEL_PROFILE
    is_default = key == DEFAULT_MODEL_PROFILE and model_name and detect_model_profile(model_name) == DEFAULT_MODEL_PROFILE
    # 如果检测结果是默认回退的, 说明是未知模型
    actual_key = detect_model_profile(model_name) if model_name else None
    presets = profile["presets"]
    first_preset = list(presets.values())[0]
    res = f"{first_preset.get('width', '?')}×{first_preset.get('height', '?')}"
    lines = [
        f"**🎨 {profile['display_name']}** | `{profile['arch'].upper()}` | {res}",
        f"> {profile['description']}",
    ]
    if model_name and not any(k in (model_name or "").lower() for k in ["majicmix", "realistic", "juggernaut"]):
        lines.append(f"> ⚠️ 未识别的模型，使用默认档案 ({profile['display_name']})")
    return "\n".join(lines)


# ==================== 兼容旧接口 ====================
# 默认预设和反向提示词 (使用 Juggernaut XL 作为默认)

SD_PRESETS = SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE]["presets"]
SD_PRESET_NAMES = list(SD_PRESETS.keys())


def get_sd_preset(name: str, model_name: str = None) -> dict:
    """获取生成预设参数，自动适配模型"""
    profile = get_model_profile(model_name)
    presets = profile.get("presets", SD_PRESETS)
    return presets.get(name, presets.get("标准 (约1分钟)", list(presets.values())[0]))


# 默认反向提示词（Juggernaut XL）
DEFAULT_NEGATIVE = SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE]["negative_prompt"]


# ==================== 图片反 AI 检测管线 ====================

def anti_detect_postprocess(img: Image.Image) -> Image.Image:
    """对 AI 生成的图片进行后处理，模拟手机拍摄/加工特征，降低 AI 检测率

    处理流程:
    1. 剥离所有元数据 (EXIF/SD参数/PNG chunks)
    2. 微小随机裁剪 (模拟手机截图不完美)
    3. 微小旋转+校正 (破坏像素完美对齐)
    4. 色彩微扰 (模拟手机屏幕色差)
    5. 不均匀高斯噪声 (模拟传感器噪声)
    6. 微小锐化 (模拟手机锐化算法)
    7. JPEG 压缩回环 (最关键: 引入真实压缩伪影)
    """
    if img.mode != "RGB":
        img = img.convert("RGB")

    w, h = img.size

    # Step 1: 微小随机裁剪 (1-3 像素, 破坏边界对齐)
    crop_l = random.randint(0, 3)
    crop_t = random.randint(0, 3)
    crop_r = random.randint(0, 3)
    crop_b = random.randint(0, 3)
    if crop_l + crop_r < w and crop_t + crop_b < h:
        img = img.crop((crop_l, crop_t, w - crop_r, h - crop_b))

    # Step 2: 极微旋转 (0.1°-0.5°, 破坏完美像素排列)
    if random.random() < 0.6:
        angle = random.uniform(-0.5, 0.5)
        img = img.rotate(angle, resample=Image.BICUBIC, expand=False,
                         fillcolor=(
                             random.randint(240, 255),
                             random.randint(240, 255),
                             random.randint(240, 255),
                         ))

    # Step 3: 色彩微扰 (模拟手机屏幕/相机色差)
    # 亮度微调
    brightness_factor = random.uniform(0.97, 1.03)
    img = ImageEnhance.Brightness(img).enhance(brightness_factor)
    # 对比度微调
    contrast_factor = random.uniform(0.97, 1.03)
    img = ImageEnhance.Contrast(img).enhance(contrast_factor)
    # 饱和度微调
    saturation_factor = random.uniform(0.96, 1.04)
    img = ImageEnhance.Color(img).enhance(saturation_factor)

    # Step 4: 不均匀传感器噪声 (比均匀噪声更像真实相机)
    try:
        import numpy as np
        arr = np.array(img, dtype=np.float32)
        # 生成不均匀噪声: 中心弱边缘强 (模拟暗角)
        h_arr, w_arr = arr.shape[:2]
        y_grid, x_grid = np.mgrid[0:h_arr, 0:w_arr]
        center_y, center_x = h_arr / 2, w_arr / 2
        dist = np.sqrt((y_grid - center_y) ** 2 + (x_grid - center_x) ** 2)
        max_dist = np.sqrt(center_y ** 2 + center_x ** 2)
        # 噪声强度: 中心 1.0, 边缘 2.5
        noise_strength = 1.0 + 1.5 * (dist / max_dist)
        noise_strength = noise_strength[:, :, np.newaxis]
        # 高斯噪声
        noise = np.random.normal(0, random.uniform(1.5, 3.0), arr.shape) * noise_strength
        arr = np.clip(arr + noise, 0, 255).astype(np.uint8)
        img = Image.fromarray(arr)
    except ImportError:
        # numpy 不可用时用 PIL 的简单模糊代替
        pass

    # Step 5: 轻微锐化 (模拟手机后处理)
    if random.random() < 0.5:
        img = img.filter(ImageFilter.SHARPEN)
        # 再做一次轻微模糊中和, 避免过度锐化
        img = img.filter(ImageFilter.GaussianBlur(radius=0.3))

    # Step 6: JPEG 压缩回环 (最关键! 引入真实压缩伪影)
    # 模拟: 手机保存 → 社交平台压缩 → 重新上传
    quality = random.randint(85, 93)  # 质量略低于完美, 像手机存储
    buf = io.BytesIO()
    img.save(buf, format="JPEG", quality=quality, subsampling=0)
    buf.seek(0)
    img = Image.open(buf).copy()  # 重新加载, 已包含 JPEG 伪影

    # Step 7: resize 回原始尺寸附近 (模拟平台缩放)
    # 微小缩放 ±2%
    scale = random.uniform(0.98, 1.02)
    new_w = int(img.width * scale)
    new_h = int(img.height * scale)
    if new_w > 100 and new_h > 100:
        img = img.resize((new_w, new_h), Image.LANCZOS)

    logger.info("🛡️ 图片反检测后处理完成: crop=%dx%d→%dx%d, jpeg_q=%d, scale=%.2f",
                w, h, img.width, img.height, quality, scale)
    return img


def strip_metadata(img: Image.Image) -> Image.Image:
    """彻底剥离图片所有元数据 (EXIF, SD参数, PNG text chunks)"""
    clean = Image.new(img.mode, img.size)
    clean.putdata(list(img.getdata()))
    return clean


class SDService:
    """Stable Diffusion WebUI API 封装"""

    def __init__(self, sd_url: str = "http://127.0.0.1:7860"):
        self.sd_url = sd_url.rstrip("/")

    # ---------- 工具方法 ----------

    @staticmethod
    def _image_to_base64(img: Image.Image) -> str:
        """PIL Image → base64 字符串"""
        buf = io.BytesIO()
        img.save(buf, format="PNG")
        return base64.b64encode(buf.getvalue()).decode("utf-8")

    @staticmethod
    def load_face_image(path: str = None) -> Image.Image | None:
        """加载头像图片，不存在则返回 None"""
        path = path or FACE_IMAGE_PATH
        if path and os.path.isfile(path):
            try:
                return Image.open(path).convert("RGB")
            except Exception as e:
                logger.warning("头像加载失败: %s", e)
        return None

    @staticmethod
    def save_face_image(img: Image.Image, path: str = None) -> str:
        """保存头像图片，返回保存路径"""
        path = path or FACE_IMAGE_PATH
        img = img.convert("RGB")
        img.save(path, format="PNG")
        logger.info("头像已保存: %s", path)
        return path

    def _build_reactor_args(self, face_image: Image.Image) -> dict:
        """构建 ReActor 换脸参数（alwayson_scripts 格式）
        
        参数索引对照 (reactor script-info):
        0: source_image (base64)     1: enable       2: source_faces
        3: target_faces              4: model        5: restore_face
        6: restore_visibility        7: restore_first 8: upscaler
        9: scale                    10: upscaler_vis 11: swap_in_source
        12: swap_in_generated       13: log_level    14: gender_source
        15: gender_target           16: save_original 17: codeformer_weight
        18: source_hash_check       19: target_hash_check 20: exec_provider
        21: face_mask_correction    22: select_source 23: face_model
        24: source_folder           25: multiple_sources 26: random_image
        27: force_upscale           28: threshold     29: max_faces
        30: tab_single
        """
        face_b64 = self._image_to_base64(face_image)
        return {
            "reactor": {
                "args": [
                    face_b64,       # 0: source image (base64)
                    True,           # 1: enable ReActor
                    "0",            # 2: source face index
                    "0",            # 3: target face index
                    "inswapper_128.onnx",  # 4: swap model
                    "CodeFormer",   # 5: restore face method
                    1,              # 6: restore face visibility
                    True,           # 7: restore face first, then upscale
                    "None",         # 8: upscaler
                    1,              # 9: scale
                    1,              # 10: upscaler visibility
                    False,          # 11: swap in source
                    True,           # 12: swap in generated
                    1,              # 13: console log level (0=min, 1=med, 2=max)
                    0,              # 14: gender detection source (0=No)
                    0,              # 15: gender detection target (0=No)
                    False,          # 16: save original
                    0.8,            # 17: CodeFormer weight (0=max effect, 1=min)
                    False,          # 18: source hash check
                    False,          # 19: target hash check
                    "CUDA",         # 20: execution provider
                    True,           # 21: face mask correction
                    0,              # 22: select source (0=Image, 1=FaceModel, 2=Folder)
                    "",             # 23: face model filename (when #22=1)
                    "",             # 24: source folder path (when #22=2)
                    None,           # 25: skip for API
                    False,          # 26: random image
                    False,          # 27: force upscale
                    0.6,            # 28: face detection threshold
                    2,              # 29: max faces to detect (0=unlimited)
                ],
            }
        }

    def has_reactor(self) -> bool:
        """检查 SD WebUI 是否安装了 ReActor 扩展"""
        try:
            resp = requests.get(f"{self.sd_url}/sdapi/v1/scripts", timeout=5)
            scripts = resp.json()
            all_scripts = scripts.get("txt2img", []) + scripts.get("img2img", [])
            return any("reactor" in s.lower() for s in all_scripts)
        except Exception:
            return False

    def check_connection(self) -> tuple[bool, str]:
        """检查 SD 服务是否可用"""
        try:
            resp = requests.get(f"{self.sd_url}/sdapi/v1/sd-models", timeout=5)
            if resp.status_code == 200:
                count = len(resp.json())
                return True, f"SD 已连接，{count} 个模型可用"
            return False, f"SD 返回异常状态: {resp.status_code}"
        except requests.exceptions.ConnectionError:
            return False, "SD WebUI 未启动或端口错误"
        except Exception as e:
            return False, f"SD 连接失败: {e}"

    def get_models(self) -> list[str]:
        """获取 SD 模型列表"""
        resp = requests.get(f"{self.sd_url}/sdapi/v1/sd-models", timeout=5)
        resp.raise_for_status()
        return [m["title"] for m in resp.json()]

    def switch_model(self, model_name: str):
        """切换 SD 模型"""
        try:
            requests.post(
                f"{self.sd_url}/sdapi/v1/options",
                json={"sd_model_checkpoint": model_name},
                timeout=60,
            )
        except Exception as e:
            logger.warning("模型切换失败: %s", e)

    def txt2img(
        self,
        prompt: str,
        negative_prompt: str = None,
        model: str = None,
        steps: int = None,
        cfg_scale: float = None,
        width: int = None,
        height: int = None,
        batch_size: int = None,
        seed: int = -1,
        sampler_name: str = None,
        scheduler: str = None,
        face_image: Image.Image = None,
        quality_mode: str = None,
        persona: str = None,
    ) -> list[Image.Image]:
        """文生图（自动适配当前 SD 模型 + 人设的最优参数）
        
        Args:
            model: SD 模型名，自动识别并应用对应配置
            face_image: 头像 PIL Image，传入后自动启用 ReActor 换脸
            quality_mode: 预设模式名
            persona: 博主人设文本，自动注入人设视觉增强词
        """
        if model:
            self.switch_model(model)

        # 自动识别模型配置
        profile = get_model_profile(model)
        profile_key = detect_model_profile(model)
        logger.info("🎯 SD 模型识别: %s → %s (%s)",
                    model or "默认", profile_key, profile["description"])

        # 加载模型专属预设参数
        preset = get_sd_preset(quality_mode, model) if quality_mode else get_sd_preset("标准 (约1分钟)", model)

        # 自动增强 prompt: 人设增强 + 模型前缀 + 原始 prompt + 模型后缀 + 人设风格
        persona_sd = get_persona_sd_profile(persona)
        persona_boost = persona_sd.get("prompt_boost", "") if persona_sd else ""
        persona_style = persona_sd.get("prompt_style", "") if persona_sd else ""
        enhanced_prompt = persona_boost + profile.get("prompt_prefix", "") + prompt + profile.get("prompt_suffix", "") + persona_style

        if persona_sd:
            logger.info("🎭 人设视觉增强已注入: +%d boost词 +%d style词",
                        len(persona_boost), len(persona_style))

        # 使用模型专属反向提示词 + 人设额外负面词
        final_negative = negative_prompt if negative_prompt is not None else profile.get("negative_prompt", DEFAULT_NEGATIVE)
        if persona_sd and persona_sd.get("negative_extra"):
            final_negative = final_negative + ", " + persona_sd["negative_extra"]

        payload = {
            "prompt": enhanced_prompt,
            "negative_prompt": final_negative,
            "steps": steps if steps is not None else preset["steps"],
            "cfg_scale": cfg_scale if cfg_scale is not None else preset["cfg_scale"],
            "width": width if width is not None else preset["width"],
            "height": height if height is not None else preset["height"],
            "batch_size": batch_size if batch_size is not None else preset["batch_size"],
            "seed": seed,
            "sampler_name": sampler_name if sampler_name is not None else preset["sampler_name"],
            "scheduler": scheduler if scheduler is not None else preset["scheduler"],
        }
        logger.info("SD 生成参数 [%s]: steps=%s, cfg=%.1f, %dx%d, sampler=%s",
                    profile_key, payload['steps'], payload['cfg_scale'],
                    payload['width'], payload['height'], payload['sampler_name'])

        # 如果提供了头像，通过 ReActor 换脸
        if face_image is not None:
            payload["alwayson_scripts"] = self._build_reactor_args(face_image)
            logger.info("🎭 ReActor 换脸已启用")

        resp = requests.post(
            f"{self.sd_url}/sdapi/v1/txt2img",
            json=payload,
            timeout=SD_TIMEOUT,
        )
        resp.raise_for_status()

        images = []
        for img_b64 in resp.json().get("images", []):
            img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
            # 反 AI 检测后处理: 剥离元数据 + 模拟手机拍摄特征
            img = anti_detect_postprocess(img)
            images.append(img)
        return images

    def img2img(
        self,
        init_image: Image.Image,
        prompt: str,
        negative_prompt: str = None,
        denoising_strength: float = 0.5,
        steps: int = 30,
        cfg_scale: float = None,
        sampler_name: str = None,
        scheduler: str = None,
        model: str = None,
    ) -> list[Image.Image]:
        """图生图（自动适配模型参数）"""
        profile = get_model_profile(model)
        preset = get_sd_preset("标准 (约1分钟)", model)

        # 将 PIL Image 转为 base64
        buf = io.BytesIO()
        init_image.save(buf, format="PNG")
        init_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")

        enhanced_prompt = profile.get("prompt_prefix", "") + prompt + profile.get("prompt_suffix", "")
        final_negative = negative_prompt if negative_prompt is not None else profile.get("negative_prompt", DEFAULT_NEGATIVE)

        payload = {
            "init_images": [init_b64],
            "prompt": enhanced_prompt,
            "negative_prompt": final_negative,
            "denoising_strength": denoising_strength,
            "steps": steps,
            "cfg_scale": cfg_scale if cfg_scale is not None else preset["cfg_scale"],
            "width": init_image.width,
            "height": init_image.height,
            "sampler_name": sampler_name if sampler_name is not None else preset["sampler_name"],
            "scheduler": scheduler if scheduler is not None else preset["scheduler"],
        }

        resp = requests.post(
            f"{self.sd_url}/sdapi/v1/img2img",
            json=payload,
            timeout=SD_TIMEOUT,
        )
        resp.raise_for_status()

        images = []
        for img_b64 in resp.json().get("images", []):
            img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
            # 反 AI 检测后处理
            img = anti_detect_postprocess(img)
            images.append(img)
        return images

    def get_lora_models(self) -> list[str]:
        """获取可用的 LoRA 模型列表"""
        try:
            resp = requests.get(f"{self.sd_url}/sdapi/v1/loras", timeout=5)
            resp.raise_for_status()
            return [lora["name"] for lora in resp.json()]
        except Exception:
            return []
