"""BooruGacha Roll — weighted random tag selection by category."""

import json
import random


class BooruGachaRoll:
    """Randomly rolls Danbooru tags from selected categories with
    popularity-weighted selection."""

    DESCRIPTION = (
        "Randomly generates Danbooru tags from configured categories. "
        "Add categories via the node UI and configure how many tags to "
        "roll from each. Use popularity_bias to control how much post "
        "count influences selection."
    )

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "seed": (
                    "INT",
                    {
                        "default": 0,
                        "min": 0,
                        "max": 0xFFFFFFFF,
                        "tooltip": "Random seed for reproducible rolls.",
                    },
                ),
                "popularity_bias": (
                    "FLOAT",
                    {
                        "default": 0.5,
                        "min": 0.0,
                        "max": 1.0,
                        "step": 0.05,
                        "tooltip": (
                            "How much post count influences selection. "
                            "0 = uniform random, 0.5 = balanced, "
                            "1.0 = proportional to popularity."
                        ),
                    },
                ),
                "slots": (
                    "STRING",
                    {
                        "default": "[]",
                        "tooltip": "JSON category config (managed by the node UI).",
                    },
                ),
                "content_filter": (
                    ["all", "sfw", "sfw+nudity", "sfw+nudity+sexual"],
                    {
                        "default": "all",
                        "tooltip": (
                            "Filter rolled tags by content rating. "
                            "'sfw' excludes nudity/sexual/gore tags, etc."
                        ),
                    },
                ),
            },
            "optional": {
                "ban_list": (
                    "BANLIST",
                    {"tooltip": "Ban list from Booru Gacha Ban List node."},
                ),
            },
        }

    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("tags", "debug_info")
    OUTPUT_TOOLTIPS = (
        "Comma-separated Danbooru tags",
        "Per-category roll details",
    )
    FUNCTION = "roll"
    CATEGORY = "BooruGacha"

    def roll(
        self,
        seed: int,
        popularity_bias: float,
        slots: str,
        content_filter: str = "all",
        ban_list: set | None = None,
    ):
        from ..search import CONTENT_FILTER_SETS, get_engine

        engine = get_engine()
        registry = engine.registry
        ban = ban_list or set()
        allowed = CONTENT_FILTER_SETS.get(content_filter)

        try:
            slot_configs = json.loads(slots)
        except (json.JSONDecodeError, TypeError):
            return ("", "Error: invalid slots JSON")

        if not isinstance(slot_configs, list):
            return ("", "Error: slots must be a JSON array")

        rng = random.Random(seed)
        all_tags: list[str] = []
        debug_lines: list[str] = []

        for slot in slot_configs:
            if not isinstance(slot, dict):
                continue
            if not slot.get("enabled", True):
                continue

            category = slot.get("category", "")
            count = max(1, int(slot.get("count", 1)))

            # Look up tags in this category
            indices = registry.by_category.get(category, [])
            if not indices:
                debug_lines.append(f"[{category}] no tags found")
                continue

            candidates = [
                registry.tags[i]
                for i in indices
                if registry.tags[i]["name"] not in ban
                and registry.tags[i]["danbooru_name"] not in ban
                and (allowed is None or registry.tags[i]["filtering"] in allowed)
            ]

            # Weighted random without replacement
            weights = [
                max(c["post_count"], 1) ** popularity_bias for c in candidates
            ]
            selected = _weighted_sample(rng, candidates, weights, count)

            weight = slot.get("weight", 1.0)

            for tag in selected:
                from ..data import format_weighted_tag

                out = format_weighted_tag(tag["danbooru_name"], weight)
                all_tags.append(out)
                debug_lines.append(
                    f"[{category}] {out}"
                    f" (posts: {tag['post_count']:,})"
                )

        return (", ".join(all_tags), "\n".join(debug_lines))


def _weighted_sample(
    rng: random.Random, items: list, weights: list[float], k: int
) -> list:
    """Sample *k* items without replacement using the given weights."""
    k = min(k, len(items))
    remaining = list(range(len(items)))
    remaining_w = list(weights)
    selected = []

    for _ in range(k):
        total = sum(remaining_w)
        if total <= 0:
            break
        r = rng.random() * total
        cumulative = 0.0
        chosen = 0
        for j, w in enumerate(remaining_w):
            cumulative += w
            if cumulative >= r:
                chosen = j
                break
        selected.append(items[remaining[chosen]])
        remaining.pop(chosen)
        remaining_w.pop(chosen)

    return selected
