"""
Test de transplanto: aplica tags cosechados del WD14 a personajes tuyos.

El objetivo NO es comprobar los LoRA, sino si las poses/escenas/ropas que salio
de Archivos/wildcards_harvest/ funcionan cuando se cambian por un personaje.

    python Engine/test_harvest_tags.py --dry-run
    python Engine/test_harvest_tags.py
    python Engine/test_harvest_tags.py --set nsfw
"""
import argparse
import base64
import json
import random
import re
import sys
import time
from datetime import datetime
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(Path(__file__).resolve().parent))
from prompt_builder import PromptBuilder  # noqa: E402

HARVEST = BASE_DIR / "Archivos" / "wildcards_harvest"
OUT = BASE_DIR / "Output" / "test_harvest"
FORGE = "http://127.0.0.1:7860"

# Poses cosechadas que NO existen en Archivos/wildcards/pose.txt actual
NSFW_POSES = [
    "cowgirl_position", "reverse_cowgirl_position", "all_fours", "missionary",
    "prone_bone", "spread_legs, on_stomach", "straddling, looking_at_viewer",
    "bent_over, from_behind", "squatting_cowgirl_position", "lying, on_back, legs_up",
    "grabbing_own_breast, arching_back", "legs, standing_on_one_leg, looking_back",
]
SFW_POSES = [
    "looking_at_viewer, hand on hip, weight on one leg",
    "lying on side, propped on elbow, one leg bent",
    "kneeling, sitting on heels, hands on thighs, looking up",
    "standing on one leg, arms_up, hand in hair",
    "crouching, arms around knees, looking up through lashes",
    "twisting body, dynamic pose, hair flowing, confident stance",
]
SCENES = {
    "beach": "beach, ocean, sunny day, bright light, blue sky",
    "poolside": "poolside, swimming pool, water, sunlight, lounge chair",
    "bedroom": "bedroom, dim lighting, silk sheets, intimate atmosphere",
    "indoors": "indoors, simple background, soft lighting",
    "balcony": "balcony, city view, sunset, golden hour",
    "shower": "shower, wet tiles, water droplets, steam, tiled bathroom",
}
SFW_OUTFITS = [
    "black bikini, string bikini, bare shoulders, sun tan lines",
    "white bikini, bikini bottom, bikini top only",
    "swimsuit, one-piece swimsuit, highleg, exposed midriff",
    "sundress, open shirt, bikini underneath, boob tube",
]
NSFW_OUTFITS = [
    "lingerie, black bra, black panties, garter belt, thighhighs",
    "black lace lingerie, bra, panties, sheer, open clothes",
    "school uniform, white shirt, black skirt, thighhighs, necktie",
    "nude, nude, wearing only thighhighs and gloves",
]
# clave -> (nombre legible, lora esperado en disco)
CHARS = {
    "Arlecchino": "Arlecchino (Chainsaw Man)",
    "Yelan": "Yelan (Genshin)",
    "rem": "Rem (Re:Zero)",
    "Boa_Hancock": "Boa Hancock (One Piece)",
    "gold_ship_illust_scarxzys": "Gold Ship (Umamusume)",
    "Raiden_shogun": "Raiden Shogun (Genshin)",
    "hu_tao_genshin_impact": "Hu Tao (Genshin)",
    "Sangonomiya_Kokomi_Kokomi": "Kokomi (Genshin)",
    "Nilou": "Nilou (Genshin)",
    "Furina": "Furina (Genshin)",
    "Skirk": "Skirk (Genshin)",
    "Hoshino": "Blue Archive",
    "Lavender": "Lavender (Blue Archive)",
    "bwc_ZXCY": "Puella Magi (cosplay)",
    "wh_takeuchi_emi": "Emi Takeuchi (Lycoris)",
    "fujiwara_chika": "Chika Fujiwara (Kaguya)",
    "kugisaki_nobara": "Nobara (JJK)",
    "shiina_mai": "Mai Sakurajima",
    "yasu": "Yasu (A2Z)",
    "Tokisaki_Kurumi": "Kurumi (Date A Live)",
    "Honjo_Nimura": "Muzan (Kimetsu)",
    "sanae": "Sanae (Touhou)",
    "konpaku_youmu": "Youmu (Touhou)",
    "Fischl": "Fischl (Genshin)",
    "Nilou_2": "Nilou dup",
    "yaoyorozu": "Yaoyorozu (Kaguya)",
    "hakari_hanazono": "Hakari (Idolmaster)",
    "hakari2": "Hahari (Idolmaster)",
    "sakamata_chloe": "Chloe (Genshin)",
    "sunday": "Sunday (HSR)",
    "sparkle": "Sparkle (HSR)",
    "luka": "Luka (HSR)",
    "aventurine": "Aventurine (HSR)",
    "firefly_honkai_star_rail": "Firefly (HSR)",
    "robin_hsr": "Robin (HSR)",
}

# batch_progression_v3.py / male_template.txt -- el hombre siempre faceless
MALE_TAGS = (
    "1boy, (faceless male:1.4), no face, blank face, featureless face, "
    "no facial features, eyes closed, black hair, short hair, "
    "muscular body, broad shoulders, large hands, masculine physique, "
    "nude male, larger male, taller dominant"
)

# Posiciones de dos personas, tal cual salieron del harvest
COUPLE_POSES = [
    "cowgirl_position",
    "reverse_cowgirl_position",
    "all_fours, from_behind, sex_from_behind",
    "missionary",
    "paizuri, standing_sex",
    "spread_legs, on_back, looking_up",
    "grabbing_another's_breast, leaning_forward",
    "head_grab, between_breasts",
]


def scale_lora(line, weight):
    """Reescala el peso del tag <lora:NAME:W>. Si no lo hay, no toca nada."""
    if weight is None:
        return line
    return re.sub(r"(<lora:[^:>]+):[^>]*>", rf"\g<1>:{weight}>", line)


def lora_on_disk(name):
    d = Path(r"D:\Data\Packages\Stable Diffusion WebUI Forge - Neo\models\Lora")
    if not d.exists():
        return None
    stems = {p.stem.lower() for p in d.rglob("*.safetensors")}
    if name.lower() in stems:
        return True
    base = name.rsplit("-", 1)[0].lower()
    return base in stems


def main():
    p = argparse.ArgumentParser(description="Transplanto de tags cosechados")
    p.add_argument("--set", choices=["nsfw", "sfw", "mixed"], default="mixed")
    p.add_argument("--n", type=int, default=12, help="cuantas imagenes")
    p.add_argument("--dry-run", action="store_true")
    p.add_argument("--seed", type=int, default=-1)
    p.add_argument("--structure", choices=["flat", "break"], default="break",
                   help="break (default, medido 0->1 en cowgirl_position) = pose tras "
                        "el 1er BREAK segun memory.md 21 | flat = orden plano, la pose se pierde")
    p.add_argument("--tag", action="append", default=None,
                   help="fuerza una pose concreta (para A/B), repetible")
    p.add_argument("--outfit", help="fuerza el outfit (si no, aleatorio del set)")
    p.add_argument("--scene", help="fuerza la escena (si no, aleatoria)")
    p.add_argument("--cam", help="fuerza la camara (si no, aleatoria)")
    p.add_argument("--char", help="fuerza un personaje")
    p.add_argument("--mode", choices=["solo", "couple"], default="solo",
                   help="couple añade MALE_TAGS y quita 'solo' del MASTER")
    p.add_argument("--lora-weight", type=float, default=None,
                   help="reescala el peso de TODOS los LoRA de la linea del personaje "
                        "(p.ej. 0.65 o 0.7) para quitar sesgo de ropa del LoRA")
    args = p.parse_args()

    if args.tag:
        forced_pose = args.tag
        poses_pool = None
    else:
        forced_pose = None
        poses_pool = None

    B = PromptBuilder(str(BASE_DIR))
    print(f"[-] {len(B.get_character_names())} personajes, CFG via config")

    import requests
    r = requests.get(f"{FORGE}/sdapi/v1/options", timeout=15)
    ckpt = r.json().get("sd_model_checkpoint", "?")
    print(f"[-] Forge OK | checkpoint: {ckpt}")

    available = {k: v for k, v in CHARS.items() if B.get_character(k)}
    missing_lora = {k: v for k, v in available.items() if lora_on_disk(k) is not True}
    usable = [k for k in available if k not in missing_lora]
    print(f"[-] personajes en el test: {len(available)} | con LoRA confirmado: {len(usable)}")
    for k, v in missing_lora.items():
        print(f"    AVISO sin LoRA en disco -> {k} ({v})")

    if not usable:
        raise SystemExit("[!] Ningun personaje con LoRA confirmado")

    combos = []
    if args.set in ("nsfw", "mixed"):
        combos += [(c, NSFW_OUTFITS, NSFW_POSES, s) for c in usable for s in SCENES]
    if args.set in ("sfw", "mixed"):
        combos += [(c, SFW_OUTFITS, SFW_POSES, s) for c in usable for s in SCENES]
    random.shuffle(combos)

    if args.char:
        if args.char not in usable:
            raise SystemExit(f"[!] {args.char} no esta en la lista usable")
        combos = [(args.char, NSFW_OUTFITS, NSFW_POSES, s) for s in SCENES]
        combos += [(args.char, SFW_OUTFITS, SFW_POSES, s) for s in SCENES]
        picked = combos[:args.n]
    else:
        # round-robin: una imagen por personaje antes de repetir ninguno
        plist = COUPLE_POSES if args.mode == "couple" else None
        olist = NSFW_OUTFITS if args.mode == "couple" else None
        pool = []
        for c in usable:
            for s in SCENES:
                if plist:
                    pool.append((c, olist, plist, s))
                else:
                    pool.append((c, NSFW_OUTFITS, NSFW_POSES, s))
                    pool.append((c, SFW_OUTFITS, SFW_POSES, s))
        random.shuffle(pool)
        picked = []
        depth = 0
        while len(picked) < args.n and depth < len(SCENES) * 2:
            for c in usable:
                for item in pool:
                    if item[0] == c and item not in picked:
                        picked.append(item)
                        break
                if len(picked) >= args.n:
                    break
            depth += 1

    OUT.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    suffix = "" if args.structure == "flat" else "_brk"
    manifest = []

    for i, (ckey, outfits, poses, sname) in enumerate(picked, 1):
        char = B.get_character(ckey)
        char_line = scale_lora(char["base"], args.lora_weight)
        if args.outfit:
            outfit = args.outfit
        else:
            outfit = random.choice(outfits)
        pose = (random.choice(forced_pose) if forced_pose else random.choice(poses))
        sname = args.scene or sname
        scene = SCENES.get(sname, sname)
        cam = args.cam or random.choice(
            ["cowboy shot", "full body", "upper body", "from above", "from side"])

        master = B.master_prompt
        if args.mode == "couple":
            # '1girl, solo' pelea con la posicion de dos personas: se quita el 'solo'
            master = master.replace("1girl, solo", "1girl")
            pose_block = f"{pose}, {MALE_TAGS}"
        else:
            pose_block = pose

        if args.structure == "break":
            pos = (
                f"{master} BREAK "
                f"{char_line}, age up, mature female, "
                f"{pose_block}, "
                f"BREAK "
                f"{outfit}, {cam}, "
                f"BREAK "
                f"{scene}"
            )
        else:
            parts = [master, char_line, "age up, mature female", outfit, pose_block, cam, scene]
            pos = ", ".join([p for p in parts if p])
        neg = B.negative_prompt
        w = f"_w{args.lora_weight}" if args.lora_weight else ""
        name = f"{i:02d}_{ckey[:16]}_{sname}{w}_{random.choice(['a','b','c'])}{suffix}.png"

        manifest.append({"file": name, "char": ckey, "pose": pose, "outfit": outfit,
                         "scene": scene, "cam": cam, "prompt": pos})
        print(f"\n[{i}/{len(picked)}] {name}")
        print(f"    char   : {ckey}")
        print(f"    pose   : {pose}")
        print(f"    outfit : {outfit}")
        print(f"    scene  : {scene}")
        print(f"    cam    : {cam}")

        if args.dry_run:
            print(f"    prompt : {pos[:110]}...")
            continue

        payload = {
            "prompt": pos, "negative_prompt": neg,
            "width": 832, "height": 1216, "steps": 28, "cfg_scale": 5.0,
            "sampler_name": "DPM++ 2M Karras", "batch_size": 1, "n_iter": 1,
            "seed": args.seed, "clip_skip": 2,
        }
        try:
            t0 = time.time()
            resp = requests.post(f"{FORGE}/sdapi/v1/txt2img", json=payload, timeout=300)
            resp.raise_for_status()
            data = resp.json()
            (OUT / name).write_bytes(base64.b64decode(data["images"][0]))
            info = data.get("info", "{}")
            try:
                seed = json.loads(info).get("seed", "?")
            except Exception:
                seed = "?"
            print(f"    [OK] {time.time()-t0:.0f}s seed={seed} -> {name}")
        except Exception as e:
            print(f"    [FAIL] {e}")

    (OUT / f"manifest_{stamp}.json").write_text(
        json.dumps(manifest, ensure_ascii=False, indent=1), encoding="utf-8")
    print(f"\n[OK] {len(manifest)} en {OUT}")
    print(f"[OK] manifest_{stamp}.json  (prompt exacto de cada imagen, para auditar)")


if __name__ == "__main__":
    main()