﻿"""
PLUMLOOM ENGINE - Batch Progression Generator v3
Estructura con BREAK separators para mejor adherencia.
"""
import requests
import base64
import random
import time
import re
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
ALLOW_ANY_LORA = False
FORGE_URL = "http://127.0.0.1:7860"
OUTPUT_DIR = BASE_DIR / "Output" / "batch_progression_v3"
WILDCARD_DIR = BASE_DIR / "Archivos" / "progression_wildcards"
CHAR_FILE = BASE_DIR / "Archivos" / "personajes.txt"

MASTER = (
    "masterpiece, best quality, absurdres, highres, score_9, score_8_up, "
    "<lora:IFL_v1.0_IL:0.7> <lora:add-detail-xl:0.5> "
    "<lora:blackwhiplash(bwl)-guy90-Illust-Lorav3:0.6> blaash, "
    "<lora:scara2b_ill_v1.00:0.7> scara2b-style, "
    "<lora:Add_color_ILXLv1:3>,"
)

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"
)

# Tags explícitos directos para que SD los reconozca
SEX_TAGS = "sex, penetration, dick, pussy, cock, cum, naked sex"

NEG = (
    "lowres, bad anatomy, bad hands, error, missing fingers, extra digit, "
    "fewer digits, cropped, worst quality, low quality, normal quality, "
    "jpeg artifacts, signature, watermark, username, blurry, artist name, "
    "bad feet, multiple views, splits, 2girls, extra people, "
    "duplicate_character, cloned_character, ugly, deformed, disfigured, "
    "mutation, mutated, extra_limbs, fused_fingers, long_neck, cross-eyed, "
    "censored, mosaic censoring, bar censor, "
    "mirror, reflection, mirrored, glass_reflection, reflective_surface"
)

STEP_NAMES = ["01_outfit", "02_undress", "03_underwear", "04_presenting", "05_sexual", "06_explicit", "07_climax"]
STEP_LABELS = ["OUTFIT", "UNDRESS", "UNDERWEAR", "PRESENTING", "SEXUAL", "EXPLICIT", "CLIMAX"]


def load_wildcards():
    wildcards = {}
    for filename in [
        "step1_outfit.txt", "step2_undress.txt", "step3_underwear.txt",
        "extra_poses.txt", "step4_presenting.txt", "step5_sexual.txt",
        "step6_explicit.txt", "step7_climax.txt", "step_state.txt",
        "cameras.txt", "scenes.txt",
    ]:
        filepath = WILDCARD_DIR / filename
        if filepath.exists():
            with open(filepath, "r", encoding="utf-8") as f:
                lines = [l.strip() for l in f if l.strip() and not l.startswith("#")]
            wildcards[filename.replace(".txt", "")] = lines
    # Las poses extra sirven a los pasos 1, 2 y 3. Antes solo llegaban al 3:
    # el codigo pedia la clave 'step2_extra_poses' y el fichero se llamaba
    # step3_extra_poses.txt, asi que los pasos 1 y 2 salian sin pose.
    if "extra_poses" in wildcards:
        for step in ("step1_extra_poses", "step2_extra_poses", "step3_extra_poses"):
            wildcards[step] = wildcards["extra_poses"]
    return wildcards


def load_characters(count=None):
    """Solo personajes cuyo .safetensors exista en disco, salvo --any-lora.

    Sin este filtro el script puede elegir un personaje cuyo LoRA no esta
    instalado: el prompt sale igual pero la imagen sale con el aspecto por
    defecto del checkpoint y parece que 'el personaje no funciona'.
    """
    with open(CHAR_FILE, "r", encoding="utf-8") as f:
        rows = [l.strip() for l in f if l.strip()]

    # compute_character_keys da una clave UNICA por personaje (misma logica que
    # PromptBuilder). extract_char_name() antes usaba el trigger mas cercano al
    # '>' y devolvia 'unknown' o un tag de pelo, con lo que dos personajes podian
    # acabar con el mismo nombre y pisarse los ficheros.
    import sys as _sys
    _sys.path.insert(0, str(Path(__file__).resolve().parent))
    from prompt_builder import compute_character_keys
    keyed = compute_character_keys(rows)

    if not ALLOW_ANY_LORA:
        from character_keys import lora_stems
        available = lora_stems()
        if available:
            before = len(keyed)
            keyed = [t for t in keyed if t[2] and t[2].lower() in available]
            sin_tag = sum(1 for line in rows if not re.search(r"<lora:", line))
            print(f"[!] {before - len(keyed)} personajes descartados "
                  f"({before - len(keyed) - sin_tag} con LoRA ausente en disco, "
                  f"{sin_tag} que van por defecto del checkpoint). "
                  f"Usa --any-lora para incluirlos.")

    if count:
        return random.sample(keyed, min(count, len(keyed)))
    return keyed


def pick(wildcards, key):
    lst = wildcards.get(key, [])
    return random.choice(lst) if lst else ""


def build_step_prompt(step_num, char_line, wildcards, camera, scene):
    outfit = pick(wildcards, "step1_outfit")
    undress = pick(wildcards, "step2_undress")
    pose_undress = pick(wildcards, "step2_extra_poses")
    underwear = pick(wildcards, "step3_underwear")
    pose_underwear = pick(wildcards, "step3_extra_poses")
    presenting = pick(wildcards, "step4_presenting")
    sexual = pick(wildcards, "step5_sexual")
    explicit = pick(wildcards, "step6_explicit")
    climax = pick(wildcards, "step7_climax")
    state = pick(wildcards, "step_state")

    if step_num == 1:
        # MASTER + 1girl, solo + BREAK + char + age up + OUTFIT + POSE + BREAK + CAMERA + BREAK + SCENE
        prompt = (
            f"{MASTER} 1girl, solo, "
            f"BREAK "
            f"{char_line}, age up, mature female, "
            f"{outfit}, {pose_undress}, "
            f"BREAK "
            f"{camera}, "
            f"BREAK "
            f"{scene}"
        )

    elif step_num == 2:
        # MASTER + 1girl, solo + BREAK + char + age up + OUTFIT + UNDRESS + POSE + BREAK + CAMERA + BREAK + SCENE
        prompt = (
            f"{MASTER} 1girl, solo, "
            f"BREAK "
            f"{char_line}, age up, mature female, "
            f"{outfit}, {undress}, {pose_undress}, "
            f"BREAK "
            f"{camera}, "
            f"BREAK "
            f"{scene}"
        )

    elif step_num == 3:
        # MASTER + 1girl, solo + BREAK + char + age up + UNDERWEAR + POSE + BREAK + CAMERA + BREAK + SCENE
        prompt = (
            f"{MASTER} 1girl, solo, "
            f"BREAK "
            f"{char_line}, age up, mature female, "
            f"{underwear}, {pose_underwear}, "
            f"BREAK "
            f"{camera}, "
            f"BREAK "
            f"{scene}"
        )

    elif step_num == 4:
        # MASTER + 1girl, solo + BREAK + char + age up + nude + PRESENTING + BREAK + CAMERA + BREAK + SCENE
        prompt = (
            f"{MASTER} 1girl, solo, "
            f"BREAK "
            f"{char_line}, age up, mature female, nude, naked, nipples, pussy, "
            f"{presenting}, "
            f"BREAK "
            f"{camera}, "
            f"BREAK "
            f"{scene}"
        )

    elif step_num == 5:
        # MASTER + 1girl, solo + BREAK + char + age up + nude + SEXUAL + BREAK + CAMERA + BREAK + SCENE
        prompt = (
            f"{MASTER} 1girl, solo, "
            f"BREAK "
            f"{char_line}, age up, mature female, nude, naked, nipples, pussy, "
            f"{sexual}, {state}, "
            f"BREAK "
            f"{camera}, "
            f"BREAK "
            f"{scene}"
        )

    elif step_num == 6:
        # MASTER + 1girl + BREAK + char + age up + EXPLICIT + BREAK + SEX_TAGS + MALE + BREAK + CAMERA + BREAK + SCENE
        prompt = (
            f"{MASTER} 1girl, "
            f"BREAK "
            f"{char_line}, age up, mature female, "
            f"{explicit}, {state}, "
            f"BREAK "
            f"{SEX_TAGS}, {MALE_TAGS}, "
            f"BREAK "
            f"{camera}, "
            f"BREAK "
            f"{scene}"
        )

    elif step_num == 7:
        # MASTER + 1girl + BREAK + char + age up + nude + CLIMAX + POSE_STEP6 + BREAK + SEX_TAGS + MALE + BREAK + CAMERA + BREAK + SCENE
        prompt = (
            f"{MASTER} 1girl, "
            f"BREAK "
            f"{char_line}, age up, mature female, nude, naked, nipples, pussy, "
            f"{climax}, {explicit}, {state}, "
            f"BREAK "
            f"{SEX_TAGS}, {MALE_TAGS}, "
            f"BREAK "
            f"{camera}, "
            f"BREAK "
            f"{scene}"
        )

    return prompt


def generate_image(prompt, outpath):
    payload = {
        "prompt": prompt,
        "negative_prompt": NEG,
        "width": 832,
        "height": 1216,
        "steps": 28,
        "cfg_scale": 5,
        "sampler_name": "DPM++ 2M Karras",
        "batch_size": 1,
        "n_iter": 1,
        "seed": -1,
        "clip_skip": 2,
    }
    try:
        resp = requests.post(f"{FORGE_URL}/sdapi/v1/txt2img", json=payload, timeout=300)
        if resp.status_code == 200:
            img = base64.b64decode(resp.json()["images"][0])
            with open(outpath, "wb") as f:
                f.write(img)
            return True, len(img) // 1024
        return False, resp.status_code
    except Exception as e:
        return False, str(e)


def run_batch(char_count=2, dry_run=False):
    wildcards = load_wildcards()
    characters = load_characters(char_count)
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

    print("=" * 55)
    print(f"  PROGRESSION BATCH v3 - {len(characters)} chars x 7 steps")
    print("=" * 55)

    total = len(characters) * 7
    done = 0

    for ci, (char_name, char_line, _lora) in enumerate(characters):
        char_name = re.sub(r"[^a-zA-Z0-9]", "_", char_name)[:24]
        scene = pick(wildcards, "scenes")
        print(f"\n--- {char_name} ---")

        for si in range(7):
            step_num = si + 1
            camera = pick(wildcards, "cameras")
            prompt = build_step_prompt(step_num, char_line, wildcards, camera, scene)
            filename = f"{char_name}_{STEP_NAMES[si]}.png"
            outpath = OUTPUT_DIR / filename
            done += 1

            print(f"  [{done}/{total}] {STEP_LABELS[si]}")

            if dry_run:
                print(f"    [DRY] {filename}")
                print(f"    {prompt[:150]}...")
            else:
                ok, result = generate_image(prompt, outpath)
                if ok:
                    print(f"    [OK] {result}KB")
                else:
                    print(f"    [FAIL] {result}")
                time.sleep(2)

    print("\n" + "=" * 55)
    print(f"  COMPLETADO - {done} images in {OUTPUT_DIR}")
    print("=" * 55)


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Progression Batch v3")
    parser.add_argument("--chars", type=int, default=2, help="Number of characters")
    parser.add_argument("--dry-run", action="store_true", help="Print prompts only")
    parser.add_argument("--any-lora", action="store_true",
                        help="incluye personajes cuyo .safetensors no este en disco")
    args = parser.parse_args()

    ALLOW_ANY_LORA = args.any_lora
    run_batch(char_count=args.chars, dry_run=args.dry_run)
