"""
Quita accesorios de CUERPO de personajes.txt, dejando los de CABEZA.

POR QUE ESTE SCRIPT NO USA REGEX PARA LOS 3 PILOTOS
----------------------------------------------------
Furina (personajes.txt:139) tiene en la MISMA linea:
    hat bow, blue bow          <- pelo (se conservan)
    ascot, blue ascot, ribbon  <- cuello (se quitan)
Un personaje no es "de bow" o "de cuello": es los dos a la vez. Ningun regex
por tag puede separarlos, asi que para los 3 del piloto la lista de tags a
quitar esta HARDCODEADA tag por tag, revisada contra los previews .jpg.

Decision de los previews (07-10-2026), 'ribbon'/'bow' a secas son ambiguos:
    chisa_wuwa  ribbon = lazo ROJO del pelo     -> conservar
    Beatrice    bow    = cintas de las coletas   -> conservar
    Phoebe      ribbon = cinta AZUL del cuello   -> quitar
    Sense       bow    = lazo BLANCO del cuello -> quitar

OJO tags con ESPACIOS, no guiones bajos: 'bare shoulders', 'wrist cuffs',
'top hat', 'hat bow', 'head wreath'. Por eso se normaliza a guiones bajos
ANTES de comparar con los patrones.

    python Engine/clean_accesorios.py            # dry-run (solo informa)
    python Engine/clean_accesorios.py --apply    # escribe personajes.txt
"""
import argparse
import re
import sys
from pathlib import Path

# Consola Windows (cp1252) no soporta ciertos caracteres (p.ej. 'ō' en
# 'Magik_MARVEL_Tōkon_Fighting_Souls'): sin esto, un print con ese nombre
# lanza UnicodeEncodeError y aborta la corrida. Reconfigura a UTF-8 tolerante.
try:
    sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace")
except Exception:
    pass

BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(Path(__file__).resolve().parent))
from prompt_builder import parse_character_line  # noqa: E402

PERSO = BASE_DIR / "Archivos" / "personajes.txt"
BACKUP_DIR = BASE_DIR / "_legacy" / "Datos"
BACKUP = BACKUP_DIR / "personajes_216_antes_de_accesorios.txt"

# --- LISTA HARDCODEADA: clave = nombre del LoRA tal cual aparece en el fichero
QUITAR = {
    # VERIFICADO en Furina.jpg: el lazo azul grande esta en el SOMBRERO, no en
    # el cuello. 'hat bow' y 'blue bow' ya lo cubren, asi que los 'ribbon' a
    # secas son redundantes y se quitan. El ascot si es el cuello (negro).
    # Corazones/joyas del pecho = jewelry/brooch/gemstone.
    "Furina": [
        "ascot", "blue ascot",          # cuello
        "ribbon", "blue ribbon",        # cinta del cuello
        "jewelry", "brooch",            # pectoral
        "gemstone", "blue gemstone",    # pectoral
        "thigh strap",                  # muslo
        "black footwear", "high heels",  # pie
    ],
    # VERIFICADO en ReZero_Rem_IlluXL.jpg: la linea tiene DOS tags 'ribbon'
    # distintos y opuestos. El preview los separa:
    #   purple ribbon = lazo MORADO DEL PELO   -> se conserva
    #   black ribbon  = moño NEGRO DEL CUELLO  -> se quita
    # Ningun regex por tag puede distinguirlos; por eso van hardcodeados.
    "ReZero_Rem_IlluXL": [
        "detached collar",   # cuello (gola amplia de la maid)
        "neck ribbon",       # cuello
        "black ribbon",      # cuello (moño negro) - ver preview
        "cleavage",          # anatomia, lo pone el bloque de ropa
        "underbust",         # ropa interior
    ],
    # VERIFICADO en Hoseki_ZenlessZoneZero_Seed_IllustriousXL_v1.JPG:
    #   clover hair ornament = trébol DEL PELO -> se conserva (no va en la lista)
    #   el collar con trébol del cuello NO esta tagueado, asi que no hay nada
    #   que quitar ahi. Los 5 de abajo si son cuerpo/ropa.
    "Hoseki_ZenlessZoneZero_Seed_IllustriousXL_v1": [
        "bare shoulders",           # torso  (CON espacio, no bare_shoulders)
        "see-through cleavage",     # torso
        "stirrup legwear",          # pierna
        "armlet",                   # brazo
        "wrist cuffs",              # muneca (CON espacio)
        # ROPA horneada que se le adhiere a la piel (medido en la corrida
        # --nude del 09-10-2026: 'thigh cutout' dibujaba las lineas del traje
        # marcadas sobre el muslo). clean_outfit_tags.py no lo cubria porque su
        # patron no incluye 'cutout'.  Mismo criterio que los accesorios: la
        # ropa la pone el bloque, no la linea del personaje.
        "thigh cutout",
    ],
    # --- Corrida --nude del 16-10-2026 (Output/test_accesorios/nude_keys, seed 12345)
    # Belle salio desnuda de torso pero CONSERVO guantes, gargantilla, pulsera y
    # una media/bota + calcetin (asimétricos) + zapatillas.
    "Char-ZZZ-Belle-V2-IL": [
        "necklace",                # gargantilla verde
        "single fingerless glove", # guante (manos)
        "bracelet",                # pulsera teal
        "single black thighhigh",  # media/bota negra (pierna)
        "single orange sock",      # calcetin naranja
        "asymmetrical legwear",    # legwear
        "sneakers",                # zapatillas
    ],
    # Domina: top halter negro + brazaletes dorados en ambas munecas.
    "domina_overwatch_ilxl_goofy": [
        "jewelry",                 # brazaletes
        "bracelet",
        "ring",
    ],
    # --- Corrida --nude del 16-10-2026, 2a tanda (nude_keys, seed 12345)
    # Bowsette: pinchos en munecas/brazos. OJO: 'jewelry' se QUEDA a peticion
    # del usuario = sus aretes (azules) son identidad de cabeza.
    "P-IL-Bowsette": [
        "spiked bracelet", "spiked armlet",
    ],
    # Marika: vestido drapeado + joyas de cuerpo.
    "ER_MarikaV1-IL": [
        "necklace", "armlets", "bracelet", "single thigh_strap",
    ],
    # Keqing: gargantilla de cuello (el ornamento del pelo es cabeza).
    "keqing-gi-richy-v1_ixl": [
        "purple choker",
    ],
    # --- Corrida --nude del 16-10-2026, 3a tanda (nude_lista, seed 42)
    # AstraYao: gargantilla de perlas + colgante + pulsera + banda de muslo.
    # (white hairband y earrings = cabeza, se conservan.)
    "Hoseki_ZenlessZoneZero_AstraYao_IllustriousXL_v2": [
        "bead necklace", "pendant", "red bow choker", "bracelet", "thigh strap",
    ],
    # --- Corrida --nude del 17-10-2026, tanda 4 (nude_lista, seed 42)
    # ayase_momo: gargantilla negra (green earrings = cabeza, se conservan).
    "ayase_momo-koto-Illustrious v01": [
        "black choker",
    ],
    # Lauma: joyas de cuerpo + adornos de brazo/pie.
    "Lauma_ILv3.1": [
        "ring", "bracelet", "armlet", "anklet",
    ],
    # --- Pre-limpiado por TAGS (17-10-2026, tanda 8): ropa de cuerpo que ya se
    # lee en la linea, se quita SIN gastar un "antes" en la GPU. Se audita una
    # sola vez con --nude despues de aplicar.
    # Mitsuri: haori + uniforme de cazador (mock-up: probar tag-first).
    "mitsuri-kanroji-s3s4-illustriousxl-lora-nochekaiser": [
        "haori", "japanese clothes", "long sleeves", "partially unbuttoned",
    ],
    # Asa (Mitaka): manga larga + liston del cuello + calzado. Los 'bow / ribbon
    # / black ribbon / black bow' se dejan = CABEZA (dudosos).
    "Asa": [
        "long sleeves", "neck ribbon", "white footwear", "sneakers",
    ],
    # Himeko: guante + halterneck + brazalete + tacones. 'red/gold trim' se
    # dejan por dudosos (podrian ser de cabeza o del pelo).
    "himekoilxl": [
        "single glove", "halterneck", "gold bracelet", "black high heels",
    ],
    # Ellen Joe: brazalete de pinchos + mangas + panty. El 'maid headdress' y el
    # 'spiked hairband' son de CABEZA -> se conservan.
    "JN_Ellen_Joe_Illus": [
        "spiked_bracelet", "puffy_short_sleeves", "brown_pantyhose",
    ],
    # Evelyn Chevalier: gargantilla + traje ajustado + colgante + anillo +
    # tassel/zipper + banda de muslo + tacones + banda de brazo. Los 'hair
    # ribbon / black ribbon / long ribbon' son de CABEZA -> se conservan.
    "Hoseki_ZenlessZoneZero_EvelynChevalier_IllustriousXL_v2": [
        "black choker", "tight clothes", "see-through cleavage",
        "ring necklace", "o-ring", "tassel", "zipper", "thigh strap",
        "high heels", "arm strap",
    ],
    # --- Pre-limpiado por TAGS (17-10-2026, tanda 9)
    # aino (Genshin): mangas. El 'hair ornament'/hairclip son de CABEZA.
    "aino_Noob10": ["sleeves past wrists", "long sleeves"],
    # oninatsu: la linea trae mangas cortas Y largas; fuera ambas.
    "oninatsu_izo_IL": ["short sleeves", "long sleeves"],
    # MOMO2IL (momomo): gargantilla. 'witch' y 'earrings' se dejan (dudosos).
    "MOMO2IL": ["choker"],
    # Kara Zor-El: vaqueros.
    "KaraZorelNoobXL_byKonan": ["jeans"],
    # Nobara Kugisaki: 'gakuran' (uniforme escolar).
    "nobara-kugisaki-s1s2-illustriousxl-lora-nochekaiser": ["gakuran"],
    # --- Pre-limpiado por TAGS (17-10-2026, tanda 10)
    # Tayama: gargantilla + leggings.
    "Tayama__Smoking_Behind_the_Supermarket_with_You__Illustrious": [
        "black choker", "black leggings",
    ],
    # mirrormaiden: gola/cuello + abertura lateral + leggings. El eye mask y el
    # headdress son de CABEZA (se conservan).
    "mirrormaiden-gi-richy-v1_ixl": [
        "detached collar", "side slit", "white leggings",
    ],
    # ju fufu: shorts + banda de muslo. tiger ears/tail = identidad (se dejan).
    "ju fufu_ilv1.0_xl": [
        "shorts", "thigh strap", "black shorts",
    ],
    # chisa_wuwa: gargantilla (x2) + mangas. Los 'ribbon' rojos del pelo = CABEZA.
    "chisa_wuwa_ilxl_goofy": [
        "black choker", "choker", "long sleeves",
    ],
    # Sparkle: halter + gargantilla + obi + guante + muslo. 'mask on head' y el
    # 'hair ribbon' = CABEZA (se conservan).
    "Char-HonkaiSR-Sparkle-V4-IL": [
        "criss-cross halter", "bell choker", "obi", "black single glove",
        "thigh strap",
    ],
    # --- Pre-limpiado por TAGS (17-10-2026, tanda 11)
    # Psylocke: o-ring + faja roja + bandas de muslo. Hoop earrings = CABEZA.
    "psylocke_Marvel_Rivals_IL_r1": [
        "o-ring", "red sash", "thigh straps",
    ],
    # Paladins Io: 'outfit1' (selector de traje) + escote. Tiara/face mark/ears
    # = CABEZA (se conservan).
    "Paladins_Io-IL": [
        "outfit1", "cleavage cutout",
    ],
    # Paladins Maeve: 'outfit1' (selector de traje).
    "Paladins_Maeve-IL": [
        "outfit1",
    ],
    # sunna (ZZZ): gargantilla. Hair ornament/clip y earrings = CABEZA.
    "c_sunna_(afternoon_tea_break)_(zenless_zone_zero)_ilv1.0-xl": [
        "black choker",
    ],
    # Melony: shorts (x3) + calzado + guante. white headwear/fur hat = CABEZA.
    "MelonyPokemon": [
        "short shorts", "side slit shorts", "white shorts", "white footwear",
        "single glove",
    ],
    # --- Pre-limpiado por TAGS (17-10-2026, tanda 12)
    # velina: 'jewelry' (¿cuerpo?). white_bow/hair_ribbon/earrings = CABEZA.
    "velina airgid": ["jewelry"],
    # Brunhilde: ropa japonesa + mangas + capas. Los ornamentos de pelo y el
    # 'blue gemstone'/'brooch' se dejan (dudosos: pueden ser del pelo).
    "Brunhilde__Record_of_Ragnarok": [
        "japanese clothes", "puffy short sleeves", "layered clothes",
        "trailing clothes",
    ],
    # Yelan: hombros al aire + escote + joyas de cuerpo.
    "YelanGenshin_IXL_v2": ["bare shoulders", "cleavage", "jewelry"],
    # eris: falda + mangas + cutout de hombro. hairband/black ribbon = CABEZA.
    "eris-boreas-greyrat-s1-ponyxl-lora-nochekaiser": [
        "brown skirt", "long sleeves", "shoulder cutout", "wide sleeves",
    ],
    # --- MANUALES con LoRA confirmado en SD por el usuario (17-10-2026). El
    # <lora:...> de la linea ES correcto (SD lo resuelve); el "ausente en disco"
    # era solo el nombre del fichero distinto del stem.
    # Magik (Marvel Rivals): shorts. horns = CABEZA. OJO: el nombre REAL del
    # LoRA lleva macron (ō) y sin '_n' -> <lora:Magik_MARVEL_Tōkon_Fighting_Souls>.
    "Magik_MARVEL_Tōkon_Fighting_Souls": ["black shorts"],
    # Quetzalcoatl (FGO Babylonia): poncho + joyas de cuerpo. headdress = CABEZA.
    "quetzalcoatl-babylonia-illustriousxl-lora-nochekaiser": [
        "jewelry", "poncho", "red poncho", "underboob",
    ],
    # Hex Maniac (Pokemon): mangas largas + lazo del CUELLO (el 'ribbon' salio
    # como moño al cuello en --nude; earrings = CABEZA).
    "Hex_Maniac_A-Z": ["long sleeves", "ribbon"],
    # Peni Parker (Spider-Verse): plugsuit (traje completo).
    "PeniMR": ["skin tight", "pilot suit", "plugsuit"],
    # Nahida (Genshin): joyas de cuerpo + top. hair accessory/flower = CABEZA.
    "oo": ["jewelry", "sleeveless"],
    # Diona (Genshin): shorts + mangas sueltas + lazo de la COLA (accesorio de
    # cuerpo). hat/paw print = CABEZA; la cola se conserva (identidad).
    "COMMISSION_Genshin_Impact_-_Diona_-_illustrious": [
        "black shorts", "puffy detached sleeves", "tail ribbon",
    ],
    # Jue (Arcane): capelet + ropa/mangas asimetricas + calzado + dobladillos.
    # head wings/feather hair/braid/hair ornaments/red ribbon/headgear = CABEZA.
    "JueV1-Nuclear1811-IL": [
        "capelet", "asymmetrical clothes", "asymmetrical sleeves",
        "single detached sleeve", "single white arm warmer", "sneakers",
        "black footwear", "split hem", "black hem",
    ],
    # NM_AG_butcha-u (LoRA de estilo del artista Danbooru 'butcha-u'): joyas +
    # hombros al aire + prenda. El nombre <lora:...> aun esta por confirmar por
    # el usuario; los tags de cuerpo se limpian igual.
    "NM_AG_butcha-u": [
        "jewelry", "necklace", "bare shoulders", "sleeveless", "brown belt",
        "brown footwear",
    ],
}

# --- LINEAS SIN <lora:> (17-10-2026): reze/rias/akeno NO tienen LoRA, se
# identifican por tags dentro del checkpoint (no ahorran peso). Se localizan
# por su PRIMER tag (nombre del personaje) en vez de por LoRA. OJO: los tags
# van con las barras de escape tal cual aparecen en personajes.txt.
QUITAR_POR_NOMBRE = {
    # reze (Chainsaw Man): gargantilla + cinta de cuello + shorts.
    r"reze \(chainsaw man\)": [
        "black choker", "black neck ribbon", "black high-waist shorts",
    ],
    # rias gremory (Highschool DxD): capelina + cinta de cuello + mangas + botones.
    "rias gremory": [
        "black capelet", "neck ribbon", "black ribbon", "long sleeves", "buttons",
    ],
    # himejima akeno (Highschool DxD): capelina.
    "himejima akeno": [
        "black capelet",
    ],
}

# Tags de ROPA que clean_outfit_tags.py dejo pasar y que SI se ven en la
# corrida --nude. Mismo criterio de los accesorios: fuera de la linea.
# OJO con 'sleeveless' (es lo CONTRARIO de manga: quitarlo haria aparecer
# mangas donde no las hay) y con 'headdress' (que es CABEZA). No van aqui.
ROPA_HORNEADA = {
    "Furina": [
        "long sleeves",     # las mangas largas del traje
        "shorts", "short shorts", "grey shorts",   # el short del traje
    ],
    "ReZero_Rem_IlluXL": [
        # MEDIDO en la corrida --nude: salen las mangas negras en los brazos
        # aunque no haya ningun tag de ropa. Viene de 'black sleeves'.
        "black sleeves",
    ],
    "Hoseki_ZenlessZoneZero_Seed_IllustriousXL_v1": [],  # ya en QUITAR arriba
    # --- Corrida --nude del 16-10-2026 (Output/test_accesorios/nude_keys)
    # Prendas que el --nude NO quito porque estan HORNEADAS en el LoRA. Mismo
    # criterio: la ropa la pone el bloque, no la linea.
    "mina-ashido-epilogue-illustriousxl-lora-nochekaiser": [
        "unitard", "two-tone unitard",  # el mono verde/morado completo
    ],
    "Delia_Ketchum_IL": [
        "layered clothes", "pink button",  # no aparecian, pero son ropa
    ],
    "VentiIL": [
        "femboy hooters", "orange shorts", "clothing writing",  # top + shorts
    ],
    "domina_overwatch_ilxl_goofy": [
        "cleavage cutout", "clothing cutout",  # dibujan el top sobre el cuerpo
    ],
    # --- Corrida --nude del 16-10-2026, 2a tanda (nude_keys, seed 12345)
    "zhao_1": [
        "chinese clothes",  # el cordon/ajustador del cuello chino
    ],
    "ER_MarikaV1-IL": [
        "revealing clothes", "side slit",  # el drapeado
    ],
    "keqing-gi-richy-v1_ixl": [
        "neck tassel", "detached sleeves",  # borla del cuello + mangas sueltas
    ],
    # --- Corrida --nude del 16-10-2026, 3a tanda (Output/test_accesorios/nude_lista, seed 42)
    # PowerGirl: capa roja con broche dorado (el cutout dibuja el escote).
    "DC_PowerGirl3D-IL": [
        "shoulder cape", "red cape", "cleavage cutout",
    ],
    # Agnes Tachyon: serafuku (marinera) horneada = top + falda + lazo naval.
    # OJO: horse ears/tail, ahoge y jewelry (sus aretes) se conservan = identidad.
    "agnes-tachyon-nekoforge": [
        "puffy short sleeves", "puffy sleeves",  # mangas del serafuku
        "zettai ryouiki",                          # media-falda + thighhighs
        "brown footwear",                          # zapatos
        "serafuku",                                # el uniforme completo
    ],
    # Sigrid (training_8576657): armadura de caballo (gorget, pauldron, glove) +
    # paneles del traje.
    "training_8576657-20260726000628564": [
        "metal gorget", "single pauldron", "single glove",  # armadura
        "detached sleeves", "black platform heels",          # mangas + tacones
        "black side panels", "black chest panel",            # paneles del traje
    ],
    # AstraYao: shrug rojo + mangas largas + cinta del vestido (cuerpo).
    "Hoseki_ZenlessZoneZero_AstraYao_IllustriousXL_v2": [
        "red shrug", "long sleeves", "dress ribbon",
    ],
    # Elinalise: mangas sueltas (guantes) + shorts.
    "elinalise-dragonroad-s2-illustriousxl-lora-nochekaiser": [
        "detached sleeves", "shorts",
    ],
    # --- Corrida --nude del 17-10-2026, tanda 4 (Output/test_accesorios/nude_lista, seed 42)
    # JN_Reina: chaqueta atada a la cintura.
    "JN_Reina_Illus": [
        "clothes around waist",
    ],
    # Odette (training_565609): guante + gauntlet + cuello + paneles.
    "training_565609-20260702044914529": [
        "detached collar", "gold trim", "side cutout",  # cuello + paneles
        "single blue glove", "white bridal gauntlet",    # manos/brazos
        "high heels",                                     # pie
    ],
    # Lauma: el vestido (top + cortina pelvica) horneado.
    "Lauma_ILv3.1": [
        "cleavage cutout", "pelvic curtain",  # top + cortina
        "high heels", "sandals", "strappy heels",  # calzado
    ],
    # --- Corrida --nude del 17-10-2026, tanda 5 (Output/test_accesorios/nude_lista, seed 42)
    # Sense: capelet + lazo de cuello (el 'bow' era el del cuello, decision
    # 07-10-2026) + zapatos. 'ahoge' se conserva.
    "Sense illu": [
        "capelet", "white bow", "bow", "black footwear",
    ],
    # Ada Wong: collar/colgante de cuello + banda de muslo + mangas.
    "NM_AW_re4remake": [
        "jewelry", "necklace", "thigh strap", "long sleeves",
    ],
    # Odette (Genshin, training_4293681): cuello/oro + manguito + tacones.
    "training_4293681-20260708121102976": [
        "odette def clothes", "high heels", "golden high heels",
        "jewelry", "gold trim", "white arm warmer", "finger loop",
    ],
    # ReneeGraves: cardigan + collar (la braguita verde no tiene tag: baked).
    "ReneeGravesNoobXL_byKonan": [
        "necklace", "long sleeves",
    ],
    # --- Corrida --nude del 17-10-2026, tanda 6 (Output/test_accesorios/nude_lista, seed 42)
    # Beatrice: medias (bow/ribbon/hair_ribbon = cabeza, se conservan).
    "se_beatrice_re_zero": [
        "tights",
    ],
    # Angewomon: guantes + banda de muslo + cinta-sash (el traje va horneado).
    "AngewomonDigimonIXL": [
        "single glove", "thigh strap", "pink ribbon", "ribbon",
    ],
    # blackCatRivals: gargantilla + ribete (domino mask = cabeza, se conserva).
    "blackCatRivals_illustrious_byJaneB": [
        "choker", "purple trim",
    ],
    # --- Corrida --nude del 17-10-2026, tanda 7 (Output/test_accesorios/nude_lista, seed 42)
    # Lily (Uraraka): brazaletes + headgear. El 'headgear' (blanco/rosa) es
    # parte del uniforme de heroe, NO identidad del personaje -> se quita
    # (peticion usuario 17-10-2026).
    "Lily": [
        "pink armbands", "headgear",
    ],
    # Mavis: calzado rojo.
    "Mavis_DraculaIL": [
        "red footwear",
    ],
    # mizushima_reiko: pañuelo rosa del cuello + mangas cortas (gafas = cabeza).
    "SSO_mizushima_reikoXLIllustrious001": [
        "pink neckerchief", "short_sleeves",
    ],
    # yunli: joyas + mangas + munequeras + tobillera + traje chino.
    "Robertlu1021_yunli_honkai_star_rail_v2.0": [
        "jewelry", "detached sleeves", "wrist_guards", "anklet",
        "chinese clothes",
    ],
    # Tifa: tirantes + calentadores de brazo negros (pendientes = cabeza).
    "fftifa-illu-nvwls-v1": [
        "black suspenders", "black arm warmers",
    ],
}

# --- CABEZA: para el aviso de 'quedan sin acessorio'. Solo informativo.
CABECAZA = re.compile(
    r"hat|bow|wreath|crown|tiara|headdress|veil|hood|headband|hairband|"
    r"headphone|hair_ornament|hair[ _]pin|hair[ _]clip|hair[ _]ribbon|"
    r"hair[ _]braid|ahoge|headwear|ornament|ribbon",
    re.I,
)


def norm(t):
    return t.lower().replace(" ", "_")


def load():
    raw = PERSO.read_bytes()
    bom = raw.startswith(b"\xef\xbb\xbf")
    text = raw.decode("utf-8-sig")
    crlf = "\r\n" in text
    return text, bom, crlf


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--apply", action="store_true",
                    help="escribe de verdad (sin esto solo informa)")
    args = ap.parse_args()

    text, bom, crlf = load()
    lineas = text.splitlines()
    trailing = text.endswith("\n")

    # localizador: nombre de LoRA -> indice de linea, y (para lineas SIN lora)
    # primer tag (nombre del personaje) -> indice de linea
    idx_lora = {}
    idx_nombre = {}
    for i, line in enumerate(lineas):
        if not line.strip() or line.strip().startswith("#"):
            continue
        lora, tags = parse_character_line(line)
        if lora:
            idx_lora.setdefault(lora, i)
        elif tags:
            idx_nombre.setdefault(tags[0].strip(), i)

    # se fusiona QUITAR (accesorios) + ROPA_HORNEADA (ropa) en un solo plan
    plan_quitar = {k: list(v) for k, v in QUITAR.items()}
    for lora, extra in ROPA_HORNEADA.items():
        plan_quitar.setdefault(lora, [])
        for t in extra:
            if t not in plan_quitar[lora]:
                plan_quitar[lora].append(t)

    faltan = [k for k in plan_quitar if k not in idx_lora]
    faltan += [k for k in QUITAR_POR_NOMBRE if k not in idx_nombre]
    if faltan:
        print("[ABORTO] estos personajes no existen en personajes.txt:")
        for k in faltan:
            print("   -", k)
        print("  No se toca nada. Revisa el nombre: debe ser EXACTO.")
        return 1

    # mapa unificado clave -> indice (LoRA o nombre)
    idx_all = dict(idx_lora)
    idx_all.update(idx_nombre)
    plan_quitar.update(
        {k: list(v) for k, v in QUITAR_POR_NOMBRE.items()})

    plan = []
    for lora, drop in plan_quitar.items():
        i = idx_all[lora]
        line = lineas[i]
        _l, tags = parse_character_line(line)
        # IDEMPOTENTE: los accesorios ya se quitaron en una corrida anterior, asi
        # que solo se aplica lo que AUN este en la linea. Si un tag no esta, se
        # avisa pero no se aborta (el abort se reserva para LoRAs inexistentes).
        presentes = [d for d in drop if d in tags]
        ausentes = [d for d in drop if d not in tags]
        if ausentes:
            print("  [ok] %s: ya limpios antes -> %s"
                  % (lora, ", ".join(ausentes)))
        if not presentes:
            continue
        keep = [t for t in tags if t not in presentes]
        plan.append((i, lora, line, tags, presentes, keep))

    print("=" * 78)
    print("LIMPIEZA DE ACCESORIOS DE CUERPO  (piloto: %d personajes)" % len(plan))
    print("=" * 78)
    for i, lora, line, tags, quitados, keep in plan:
        cab = [t for t in keep if CABECAZA.search(norm(t))]
        etiqueta = ("<lora:%s>" % lora) if "<lora:" in line else ("[sin lora] %s" % lora)
        print("\n  linea %d  %s" % (i + 1, etiqueta))
        print("    QUITA (%d): %s" % (len(quitados), ", ".join(quitados)))
        print("    CABEZA conservada (%d): %s"
              % (len(cab), ", ".join(cab) if cab else "*** NINGUNA ***"))
        if not cab:
            print("    AVISO: se queda sin ningun acessorio de cabeza.")

    if not args.apply:
        print("\n" + "=" * 78)
        print("DRY-RUN. No se ha escrito nada. Usa --apply.")
        print("Backup ya existente: %s" % BACKUP.relative_to(BASE_DIR))
        return 0

    # --- escritura -----------------------------------------------------------
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    if not BACKUP.exists():
        BACKUP.write_bytes(PERSO.read_bytes())
        print("\n[OK] backup creado -> %s" % BACKUP.relative_to(BASE_DIR))
    else:
        print("\n[OK] backup reutilizado -> %s" % BACKUP.relative_to(BASE_DIR))

    nuevas = list(lineas)
    for i, lora, line, tags, quitados, keep in plan:
        m = re.search(r"<lora:[^:>]+:[^>]*>", line)
        pre = m.group(0) if m else ""
        # '1girl solo' viene como un solo campo: se preserva tal cual
        cuerpo = ", ".join(keep)
        nuevas[i] = ("%s %s" % (pre, cuerpo)).strip() if pre else cuerpo

    sep = "\r\n" if crlf else "\n"
    out = sep.join(nuevas) + (sep if trailing else "")
    data = out.encode("utf-8")
    if bom:
        data = b"\xef\xbb\xbf" + data
    PERSO.write_bytes(data)

    total = sum(len(item[4]) for item in plan)
    print("[OK] %d lineas modificadas, %d tags fuera." % (len(plan), total))
    print("[OK] codificacion: utf-8%s, saltos: %s"
          % ("-sig/BOM" if bom else "", "CRLF" if crlf else "LF"))
    return 0


if __name__ == "__main__":
    sys.exit(main())