"""
Test de CREATIVIDAD: un personaje curado recorriendo 5 escenas que suben de
intensidad (SFW -> sugerente -> undressing -> nude -> explicito), 2 semillas
por escena = 10 imagenes.

Mantiene el MASTER con los 5 LoRAs de estilo (via PromptBuilder) y usa el
ensamblador REAL (test_blocks.assemble_prompt), asi que la estructura es la
vigente: MASTER+LoRAs / 1girl+personaje[+nude] / BREAK / [hombre] / BREAK /
bloque. Para la escena explicita el hombre es faceless (convencion del
proyecto) y el ensamblador quita 'solo' del MASTER al detectar 2 personas.

    python Engine/test_creatividad.py --dry-run
    python Engine/test_creatividad.py                       # 2 semillas random
    python Engine/test_creatividad.py --char Furina --seeds 12345,77777
    python Engine/test_creatividad.py --only 1,5            # solo algunas escenas

Salida: Output/test_creatividad/ + manifest_*.json con el prompt exacto de cada
imagen (para auditar) y el personaje/escena/semilla.
"""
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
from test_blocks import assemble_prompt  # noqa: E402

FORGE = "http://127.0.0.1:7860"
OUT = BASE_DIR / "Output" / "test_creatividad"
DEFAULT_CHAR = "Furina"

# Accesorios/ropa que el LoRA de Furina trae HORNEADA (sesgo del LoRA, no del
# prompt). Solo se usa en las escenas donde sale desnuda (4-7): en las
# vestidas el outfit lo decide el bloque y este negative sobraria.
FURINA_BAKED = ("thigh strap, thigh band, garter, garter belt, jewelry, "
                "collar, choker, necklace, gemstone, brooch")

# 5 escenas. 'two_people' se detecta solo: assemble_prompt mira si el bloque
# trae '1boy' o 'hetero'. 'nude' = 'auto' deja que el ensamblador decida: si el
# bloque no lleva ropa (DROP_OUTFIT) inyecta NUDE_TAGS; si lleva, no.
SCENES = [
    {
        "id": 1,
        "slug": "01_sfw_biblioteca",
        "name": "SFW - biblioteca (outfit completo)",
        "block": (
            "standing, tiptoe, reaching up, holding a book, arm up, "
            "looking up, library, tall bookshelf, books, "
            "wooden ladder, large window, wooden floor, dust motes, "
            "smile, closed mouth, blush, beige cardigan, brown pleated "
            "skirt, brown loafers, long sleeves, warm sunlight, sunbeams, "
            "golden hour, backlighting, cinematic lighting, depth of field"
        ),
        "neg_extra": "",
    },
    {
        "id": 2,
        "slug": "02_sugerente_azotea",
        "name": "Sugerente - azotea (menos ropa)",
        "block": (
            "leaning forward, leaning on railing, arched back, looking back, "
            "looking at viewer, hip focus, parted lips, half-closed eyes, "
            "white sundress, spaghetti straps, short dress, bare shoulders, "
            "rooftop, railing, potted plants, city skyline, dusk, orange sky, "
            "wind, hair blowing, sunset, rim light, cinematic lighting, "
            "depth of field"
        ),
        "neg_extra": "",
    },
    {
        "id": 3,
        "slug": "03_undressing_granja",
        "name": "Undressing - granja (ropa abierta + interior visible)",
        "block": (
            "kneeling on bed, one knee up, unbuttoning shirt, open shirt, "
            "open clothes, shirt slipping off shoulder, bra, lace panties, "
            "underwear visible, visible panties, blush, looking down, "
            "parted lips, farmhouse, rustic bedroom, wooden wall, small "
            "window, bedsheet, quilted blanket, lace curtains, morning "
            "light, sunlight, warm lighting, soft light, depth of field"
        ),
        "neg_extra": "",
    },
    {
        "id": 4,
        "slug": "04_nude_invernadero",
        "name": "Nude - invernadero (pose dinamica)",
        "block": (
            "dynamic pose, spinning, hair flip, mid-turn, arms up, standing, "
            "tiptoe, greenhouse, glass ceiling, potted plants, "
            "tropical plants, hanging vines, flowers, sunbeams, morning "
            "light, sunlight, backlighting, lens flare, petals, barefoot, "
            "sweat, blush, parted lips, looking at viewer, cinematic "
            "lighting, depth of field, nude, naked"
        ),
        "neg_extra": FURINA_BAKED,
    },
    {
        "id": 5,
        "slug": "05_explicito_suspended",
        "name": "Explicito - suspended congress (1boy faceless)",
        "block": (
            "1boy, hetero, sex, suspended congress, lifting person, "
            "carrying, arms around neck, legs wrapped around waist, "
            "standing, large hands, holding thigh, breast grab, vaginal, "
            "penetration, penis, uncensored, blush, open mouth, tongue out, "
            "sweat, looking at viewer, bedroom, large window, night, moon, "
            "curtains, warm lamp light, cinematic lighting, nude, naked"
        ),
        "neg_extra": FURINA_BAKED,
    },
    {
        "id": 6,
        "slug": "06_explicito_fullnelson",
        "name": "Explicito - full nelson (1boy faceless)",
        "block": (
            "1boy, hetero, sex, full nelson, headlock, arms trapped behind "
            "head, looking at camera, elbows out, lifting person, carrying, "
            "standing, legs up, spread legs, large hands, holding thighs "
            "apart, vaginal, penetration, penis, testicles, uncensored, "
            "blush, open mouth, tongue out, sweat, living room, couch, "
            "fireplace, warm fire light, evening, cinematic lighting, "
            "nude, naked"
        ),
        "neg_extra": FURINA_BAKED,
    },
    {
        "id": 7,
        "slug": "07_explicito_upright",
        "name": "Explicito - upright straddle (1boy faceless)",
        "block": (
            "1boy, hetero, sex, upright straddle, facing away, standing, "
            "against wall, lifting person, carrying, arms around neck, "
            "legs wrapped around waist, large hands, holding butt, "
            "vaginal, penetration, penis, uncensored, blush, open mouth, "
            "tongue out, sweat, looking back, shower, tile wall, steam, "
            "wet, bathroom, night, warm light, cinematic lighting, "
            "nude, naked"
        ),
        "neg_extra": FURINA_BAKED,
    },
]


GENERIC = {"1girl", "solo", "girl", "1boy", "boy", "androgynous", "1girls"}
LORA_RE = re.compile(r"<lora:[^>]+>")


def normalize_char_line(char_line):
    """Separa el tag <lora:> como token propio y tira los genericos (1girl,
    solo...). El MASTER ya aporta '1girl, solo', asi que dejarlos en la linea
    los duplica (y con la estructura del lora van PEGADOS: '<lora:Furina:1>
    1girl', que el dedupe de assemble_prompt no reconoce)."""
    m = LORA_RE.search(char_line)
    lora_tag = m.group(0) if m else ""
    rest = LORA_RE.sub(" ", char_line)
    tags = [t.strip() for t in rest.split(",") if t.strip()]
    tags = [t for t in tags if t.lower() not in GENERIC]
    parts = ([lora_tag] if lora_tag else []) + tags
    return ", ".join(parts)


def build_negative(base_neg, extra):
    if not extra:
        return base_neg
    base = base_neg.strip().rstrip(",")
    return "%s, %s" % (base, extra) if base else extra


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--char", default=DEFAULT_CHAR,
                    help="clave del personaje en personajes.txt (def: Furina)")
    ap.add_argument("--seeds",
                    help="dos semillas separadas por coma; si no, 2 al azar")
    ap.add_argument("--only", help="ids de escena a generar, ej: 1,5")
    ap.add_argument("--lora-weight", type=float, default=0.7,
                    help="peso del LoRA del personaje (def: 0.7; baja de 1.0 "
                         "para que no resucite ropa horneada). 0 = no tocar")
    ap.add_argument("--steps", type=int, default=28)
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()

    if args.seeds:
        seeds = [int(s.strip()) for s in args.seeds.split(",") if s.strip()]
    else:
        rng = random.SystemRandom()
        seeds = [rng.randint(1, 2147483647) for _ in range(2)]

    only = ({int(x) for x in args.only.split(",")} if args.only else None)
    scenes = [s for s in SCENES if not only or s["id"] in only]

    B = PromptBuilder(str(BASE_DIR))
    char = B.get_character(args.char)
    if not char:
        raise SystemExit("[!] '%s' no esta en personajes.txt" % args.char)
    char_line = normalize_char_line(char["base"])
    if args.lora_weight and args.lora_weight > 0:
        char_line = re.sub(r"(<lora:[^:>]+):[^>]*>",
                           r"\g<1>:%s>" % args.lora_weight, char_line)
    master = B.master_prompt.strip().rstrip(",")
    neg = B.negative_prompt.strip().rstrip(",")

    import requests
    try:
        ck = requests.get(f"{FORGE}/sdapi/v1/options", timeout=15).json()
        print("[-] checkpoint: %s" % ck.get("sd_model_checkpoint", "?"))
    except Exception as e:
        raise SystemExit("[!] Forge no responde: %s" % e)

    print("[-] personaje: %s" % args.char)
    print("[-] semillas : %s" % ", ".join(str(s) for s in seeds))
    print("[-] escenas  : %s" % ", ".join(s["slug"] for s in scenes))

    OUT.mkdir(parents=True, exist_ok=True)
    manifest = []

    for scene in scenes:
        block = scene["block"]
        two_people = ("1boy" in block or "hetero" in block)
        cl = char_line
        pos, segs = assemble_prompt(master, cl, block, force_nude="auto")
        neg_scene = build_negative(neg, scene["neg_extra"])

        print("\n" + "=" * 78)
        print("%s  [%s]" % (scene["name"], scene["slug"]))
        print("=" * 78)
        for i, s in enumerate(segs, 1):
            print("  [%d] %s" % (i, s))
        if scene["neg_extra"]:
            print("  NEG+ : %s" % scene["neg_extra"])

        if args.dry_run:
            continue

        for seed in seeds:
            payload = {
                "prompt": pos, "negative_prompt": neg_scene,
                "width": 832, "height": 1216, "steps": args.steps,
                "cfg_scale": 5.0, "sampler_name": "DPM++ 2M Karras",
                "batch_size": 1, "n_iter": 1, "seed": seed, "clip_skip": 2,
            }
            t0 = time.time()
            r = requests.post(f"{FORGE}/sdapi/v1/txt2img", json=payload,
                              timeout=300)
            r.raise_for_status()
            data = r.json()
            try:
                got = json.loads(data.get("info", "{}")).get("seed", seed)
            except Exception:
                got = seed
            name = "%s_s%d.png" % (scene["slug"], seed)
            out = OUT / name
            out.write_bytes(base64.b64decode(data["images"][0]))
            print("  [OK] %4.0fs seed=%s -> %s"
                  % (time.time() - t0, got, name))
            manifest.append({
                "file": name, "scene": scene["id"],
                "scene_name": scene["name"], "slug": scene["slug"],
                "char": args.char, "seed": got,
                "two_people": two_people,
                "prompt": pos, "negative_prompt": neg_scene,
                "char_line": cl, "block": block,
            })

    if args.dry_run:
        print("\n[dry-run] nada generado. Quita --dry-run para generar.")
        return

    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    man = OUT / ("manifest_%s.json" % stamp)
    man.write_text(json.dumps(manifest, ensure_ascii=False, indent=1),
                   encoding="utf-8")
    print("\n[OK] %d imagenes -> %s" % (len(manifest), OUT))
    print("[OK] manifiesto: %s" % man.name)


if __name__ == "__main__":
    main()
