import os
import re
import random
import logging
import yaml

logger = logging.getLogger(__name__)

GENERIC_TAGS = {"1girl", "solo", "girl", "1boy", "boy", "1other", "androgynous", "1girls"}


def parse_character_line(line):
    """-> (nombre_lora|None, [tags]). Acepta el tag <lora:...> al principio o al final."""
    m = re.search(r"<lora:([^:>]+):[^>]*>", line)
    lora = m.group(1).strip() if m else None
    rest = re.sub(r"<lora:[^:>]+:[^>]*>", " ", line)
    tags = [t.strip() for t in rest.split(",") if t.strip()]
    return lora, tags


def compute_character_keys(rows):
    """-> [(clave, linea, lora)] con una clave unica por personaje.

    La clave es el nombre del LoRA, salvo que varias lineas compartan ese LoRA
    (pack multi-personaje): ahi se usa el primer tag que difiere del nombre del
    LoRA, que normalmente es el triggerword. Los tags genericos (1girl, solo...)
    se descartan siempre. Ninguna clave se inventa: sale de un tag de la linea.
    """
    lora_count = {}
    for line in rows:
        lora, _ = parse_character_line(line)
        if lora:
            lora_count[lora] = lora_count.get(lora, 0) + 1

    out = []
    used = {}
    for line in rows:
        lora, tags = parse_character_line(line)
        ambiguous = lora_count.get(lora, 0) > 1
        cands = [t for t in tags
                 if t.lower() not in GENERIC_TAGS
                 and (not lora or t.lower() != lora.lower())]
        if ambiguous:
            key = cands[0] if cands else (lora or line[:30])
        else:
            key = lora or (cands[0] if cands else line[:30])

        if key in used:
            used[key] += 1
            key = f"{key}#{used[key]}"
        else:
            used[key] = 1

        out.append((key, line, lora))
    return out


class PromptBuilder:
    def __init__(self, base_dir):
        self.base_dir = base_dir
        self.config_dir = os.path.join(base_dir, "Config")
        self.archivos_dir = os.path.join(base_dir, "Archivos")

        self.master_prompt = ""
        self.negative_prompt = ""
        self.exceptions = {}
        self.characters = {}
        self.wildcards = {}

        self._load_all()

    def _load_all(self):
        self._load_master_prompt()
        self._load_exceptions()
        self._load_characters()
        self._load_wildcards()

    def _load_master_prompt(self):
        path = os.path.join(self.config_dir, "master_prompt.yaml")
        if not os.path.exists(path):
            logger.error(f"No existe master_prompt.yaml en {path}")
            return

        with open(path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)

        self.master_prompt = data.get("master_prompt", "").strip().rstrip(",")
        self.negative_prompt = data.get("negative_prompt", "").strip().rstrip(",")
        logger.info(f"Master prompt cargado: {len(self.master_prompt)} chars")

    def _load_exceptions(self):
        path = os.path.join(self.config_dir, "excepciones_maestras.yaml")
        if not os.path.exists(path):
            logger.warning("No existe excepciones_maestras.yaml")
            return

        with open(path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)

        self.exceptions = data or {}
        globales = self.exceptions.get("globales") or {}
        personajes = self.exceptions.get("personajes") or {}
        logger.info(f"Excepciones cargadas: {len(globales)} globales, {len(personajes)} personajes")

    def _load_characters(self):
        base_file = os.path.join(self.archivos_dir, "personajes.txt")
        outfit_file = os.path.join(self.archivos_dir, "personajes_outfit.txt")

        rows = []
        if os.path.exists(base_file):
            with open(base_file, "r", encoding="utf-8-sig") as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith("#"):
                        continue
                    rows.append(line)

        for key, line, lora in compute_character_keys(rows):
            self.characters[key] = {"base": line, "outfit": "", "lora": lora}

        if os.path.exists(outfit_file):
            with open(outfit_file, "r", encoding="utf-8-sig") as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith("#"):
                        continue

                    lora, _ = parse_character_line(line)
                    for char in self.characters.values():
                        if char["lora"] and char["lora"] == lora:
                            char["outfit"] = line
                            break

        logger.info(f"Personajes cargados: {len(self.characters)}")

    def _load_wildcards(self):
        wc_dir = os.path.join(self.archivos_dir, "wildcards")
        if not os.path.exists(wc_dir):
            logger.warning(f"No existe carpeta wildcards: {wc_dir}")
            return

        for filename in ["pose.txt", "scene.txt", "details.txt", "outfit.txt"]:
            filepath = os.path.join(wc_dir, filename)
            if os.path.exists(filepath):
                key = filename.replace(".txt", "")
                with open(filepath, "r", encoding="utf-8") as f:
                    lines = [l.strip() for l in f if l.strip() and not l.startswith("#")]
                self.wildcards[key] = lines
                logger.info(f"Wildcard '{key}': {len(lines)} opciones")

    def get_character_names(self):
        return sorted(self.characters.keys())

    def get_character(self, name):
        return self.characters.get(name)

    def get_wildcard_options(self, wc_type):
        return self.wildcards.get(wc_type, [])

    def random_wildcard(self, wc_type):
        options = self.wildcards.get(wc_type, [])
        if options:
            return random.choice(options)
        return ""

    def apply_exceptions(self, character_name, outfit="", scene=""):
        pos_extra = ""
        neg_extra = ""

        globales = self.exceptions.get("globales", {})

        bloqueo_reflejos = globales.get("bloqueo_reflejos", {})
        if bloqueo_reflejos:
            disparadores = bloqueo_reflejos.get("disparadores", [])
            for d in disparadores:
                if d.lower() in scene.lower():
                    neg_extra += f", {bloqueo_reflejos.get('inyectar_negativo', '')}"
                    break

        bloqueo_oceano = globales.get("bloqueo_oceano", {})
        if bloqueo_oceano:
            disparadores = bloqueo_oceano.get("disparadores", [])
            for d in disparadores:
                if d.lower() in outfit.lower():
                    neg_extra += f", {bloqueo_oceano.get('inyectar_negativo', '')}"
                    break

        personajes_exc = self.exceptions.get("personajes") or {}
        if character_name in personajes_exc:
            p_exc = personajes_exc[character_name]

            outfit_exc = p_exc.get("outfits", {})
            if outfit in outfit_exc:
                pos_extra += f", {outfit_exc[outfit].get('positivo_extra', '')}"
                neg_extra += f", {outfit_exc[outfit].get('negativo_extra', '')}"

            escenario_exc = p_exc.get("escenarios", {})
            for key in escenario_exc:
                if key.lower() in scene.lower():
                    neg_extra += f", {escenario_exc[key].get('negativo_extra', '')}"
                    break

        return pos_extra.strip(", "), neg_extra.strip(", ")

    def build_prompt(self, character_name, outfit=None, scene=None,
                     pose=None, details=None, extra_tags="",
                     always_age_up=True):
        char = self.characters.get(character_name)
        if not char:
            raise ValueError(f"Personaje '{character_name}' no encontrado en personajes.txt")

        char_tags = char["base"]

        if outfit is None:
            outfit_tags = char.get("outfit", "")
        elif outfit in self.wildcards.get("outfit", []):
            outfit_tags = outfit
        else:
            outfit_tags = outfit

        scene_tags = scene if scene else self.random_wildcard("scene")
        pose_tags = pose if pose else self.random_wildcard("pose")
        details_tags = details if details else self.random_wildcard("details")

        age_up = "age up, mature female" if always_age_up else ""

        pos_extra, neg_extra = self.apply_exceptions(character_name, outfit_tags, scene_tags)

        parts = [
            self.master_prompt,
            char_tags,
            age_up,
            outfit_tags,
            pose_tags,
            scene_tags,
            details_tags,
            extra_tags,
            pos_extra,
        ]
        positive = ", ".join([p for p in parts if p])

        neg_parts = [self.negative_prompt, neg_extra]
        negative = ", ".join([n for n in neg_parts if n])

        logger.info(f"Prompt construido para '{character_name}': {len(positive)} chars")
        return positive, negative

    def build_random(self, character_name=None, spice_level="suggestive"):
        if character_name is None:
            character_name = random.choice(list(self.characters.keys()))

        outfit_options = self.wildcards.get("outfit", [])
        if spice_level == "suggestive":
            outfit = random.choice(outfit_options[:20]) if len(outfit_options) >= 20 else random.choice(outfit_options)
        elif spice_level == "explicit":
            outfit = random.choice(outfit_options) if outfit_options else ""
        else:
            outfit = random.choice(outfit_options) if outfit_options else ""

        scene = self.random_wildcard("scene")
        pose = self.random_wildcard("pose")
        details = self.random_wildcard("details")

        return self.build_prompt(
            character_name=character_name,
            outfit=outfit,
            scene=scene,
            pose=pose,
            details=details
        )