from __future__ import annotations

import json
import random

try:
    from .category_cast_config import (
        build_cast_config_json,
        build_category_config_json,
        cast_preset_choices,
        category_preset_choices,
    )
    from .prompt_builder import (
        subcategory_choices,
    )
    from .seed_config import configured_seed_from_axes
    from .location_config import (
        build_composition_pool_json,
        build_location_pool_json,
        build_thematic_location_json,
        composition_pool_preset_choices,
        location_pool_preset_choices,
        location_theme_choices,
    )
    from .style_config import (
        build_style_config_json,
        style_combine_mode_choices,
        style_pool_preset_choices,
    )
except ImportError:  # Allows local smoke tests from the repository root.
    from category_cast_config import (
        build_cast_config_json,
        build_category_config_json,
        cast_preset_choices,
        category_preset_choices,
    )
    from prompt_builder import (
        subcategory_choices,
    )
    from seed_config import configured_seed_from_axes
    from location_config import (
        build_composition_pool_json,
        build_location_pool_json,
        build_thematic_location_json,
        composition_pool_preset_choices,
        location_pool_preset_choices,
        location_theme_choices,
    )
    from style_config import (
        build_style_config_json,
        style_combine_mode_choices,
        style_pool_preset_choices,
    )


SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG"
SXCP_LOCATION_CONFIG = "SXCP_LOCATION_CONFIG"
SXCP_COMPOSITION_CONFIG = "SXCP_COMPOSITION_CONFIG"
SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG"
SXCP_SEED_CONFIG = "SXCP_SEED_CONFIG"
SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG"


class SxCPCategoryPreset:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "preset": (category_preset_choices(), {"default": "auto_weighted"}),
                "subcategory": (subcategory_choices(), {"default": "random"}),
            }
        }

    RETURN_TYPES = (SXCP_CATEGORY_CONFIG, "STRING", "STRING")
    RETURN_NAMES = ("category_config", "category", "subcategory")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(self, preset, subcategory):
        config = build_category_config_json(preset=preset, subcategory=subcategory)
        parsed = json.loads(config)
        return config, parsed["category"], parsed["subcategory"]


class SxCPLocationPool:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "enabled": ("BOOLEAN", {"default": True}),
                "combine_mode": (["replace", "add"], {"default": "replace"}),
                "preset": (location_pool_preset_choices(), {"default": "custom_only"}),
                "custom_locations": ("STRING", {"default": "", "multiline": True}),
            },
            "optional": {
                "location_config": (SXCP_LOCATION_CONFIG,),
            },
        }

    RETURN_TYPES = (SXCP_LOCATION_CONFIG, "STRING")
    RETURN_NAMES = ("location_config", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(self, enabled, combine_mode, preset, custom_locations, location_config=""):
        config = build_location_pool_json(
            enabled=enabled,
            combine_mode=combine_mode,
            preset=preset,
            custom_locations=custom_locations or "",
            location_config=location_config or "",
        )
        parsed = json.loads(config)
        return config, parsed.get("summary", "")


class SxCPCompositionPool:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "enabled": ("BOOLEAN", {"default": True}),
                "combine_mode": (["replace", "add"], {"default": "replace"}),
                "preset": (composition_pool_preset_choices(), {"default": "no_outfit_check"}),
                "custom_compositions": ("STRING", {"default": "", "multiline": True}),
            },
            "optional": {
                "composition_config": (SXCP_COMPOSITION_CONFIG,),
            },
        }

    RETURN_TYPES = (SXCP_COMPOSITION_CONFIG, "STRING")
    RETURN_NAMES = ("composition_config", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(self, enabled, combine_mode, preset, custom_compositions, composition_config=""):
        config = build_composition_pool_json(
            enabled=enabled,
            combine_mode=combine_mode,
            preset=preset,
            custom_compositions=custom_compositions or "",
            composition_config=composition_config or "",
        )
        parsed = json.loads(config)
        return config, parsed.get("summary", "")


class SxCPLocationTheme:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "enabled": ("BOOLEAN", {"default": True}),
                "combine_mode": (["replace", "add"], {"default": "replace"}),
                "theme": (location_theme_choices(), {"default": "semi_public_affair"}),
                "custom_locations": ("STRING", {"default": "", "multiline": True}),
                "custom_compositions": ("STRING", {"default": "", "multiline": True}),
            },
            "optional": {
                "location_config": (SXCP_LOCATION_CONFIG,),
                "composition_config": (SXCP_COMPOSITION_CONFIG,),
            },
        }

    RETURN_TYPES = (SXCP_LOCATION_CONFIG, SXCP_COMPOSITION_CONFIG, "STRING")
    RETURN_NAMES = ("location_config", "composition_config", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(
        self,
        enabled,
        combine_mode,
        theme,
        custom_locations,
        custom_compositions,
        location_config="",
        composition_config="",
    ):
        return build_thematic_location_json(
            enabled=enabled,
            combine_mode=combine_mode,
            theme=theme,
            custom_locations=custom_locations or "",
            custom_compositions=custom_compositions or "",
            location_config=location_config or "",
            composition_config=composition_config or "",
        )


class SxCPStylePool:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "enabled": ("BOOLEAN", {"default": True}),
                "combine_mode": (style_combine_mode_choices(), {"default": "replace"}),
                "preset": (style_pool_preset_choices(), {"default": "realistic_photo"}),
                "custom_style": ("STRING", {"default": "", "multiline": True}),
                "custom_positive_suffix": ("STRING", {"default": "", "multiline": True}),
                "custom_negative": ("STRING", {"default": "", "multiline": True}),
            },
            "optional": {
                "style_config": (SXCP_STYLE_CONFIG,),
            },
        }

    RETURN_TYPES = (SXCP_STYLE_CONFIG, "STRING")
    RETURN_NAMES = ("style_config", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(
        self,
        enabled,
        combine_mode,
        preset,
        custom_style,
        custom_positive_suffix,
        custom_negative,
        style_config="",
    ):
        config = build_style_config_json(
            enabled=enabled,
            combine_mode=combine_mode,
            preset=preset,
            custom_style=custom_style or "",
            custom_positive_suffix=custom_positive_suffix or "",
            custom_negative=custom_negative or "",
            style_config=style_config or "",
        )
        parsed = json.loads(config)
        return config, parsed.get("summary", "")


class SxCPCastControl:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "cast_mode": (cast_preset_choices(), {"default": "mixed_couple"}),
                "women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
                "men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
            }
        }

    RETURN_TYPES = (SXCP_CAST_CONFIG, "INT", "INT", "STRING")
    RETURN_NAMES = ("cast_config", "women_count", "men_count", "cast_summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(self, cast_mode, women_count, men_count):
        config = build_cast_config_json(cast_mode=cast_mode, women_count=women_count, men_count=men_count)
        parsed = json.loads(config)
        summary = f"{parsed['women_count']} women, {parsed['men_count']} men"
        return config, parsed["women_count"], parsed["men_count"], summary


class SxCPCastBias:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}),
                "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}),
                "women_weights": ("STRING", {"default": "0.60,0.25,0.10,0.05"}),
                "women_start_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
                "men_weights": ("STRING", {"default": "0.45,0.40,0.10,0.05"}),
                "men_start_count": ("INT", {"default": 0, "min": 0, "max": 12, "step": 1}),
                "empty_behavior": (["force_one_woman", "force_one_man", "allow_empty"], {"default": "force_one_woman"}),
            },
            "optional": {
                "seed_config": (SXCP_SEED_CONFIG,),
            },
        }

    RETURN_TYPES = (SXCP_CAST_CONFIG, "INT", "INT", "STRING")
    RETURN_NAMES = ("cast_config", "women_count", "men_count", "cast_summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    @staticmethod
    def _configured_cast_seed(seed_config):
        return configured_seed_from_axes(
            seed_config,
            ("category", "content", "role"),
            extra_keys=("seed", "global_seed"),
        )

    @staticmethod
    def _weight_pairs(weights_text, start_count):
        pairs = []
        start = max(0, min(12, int(start_count)))
        parts = str(weights_text or "").replace("\n", ",").split(",")
        for offset, raw in enumerate(parts):
            count = start + offset
            if count > 12:
                break
            try:
                weight = float(raw.strip())
            except (TypeError, ValueError):
                continue
            if weight > 0:
                pairs.append((count, weight))
        return pairs or [(start, 1.0)]

    @staticmethod
    def _weighted_count(rng, pairs):
        total = sum(weight for _count, weight in pairs)
        point = rng.random() * total
        upto = 0.0
        for count, weight in pairs:
            upto += weight
            if point <= upto:
                return int(count)
        return int(pairs[-1][0])

    @classmethod
    def IS_CHANGED(cls, *args, **kwargs):
        seed_value = kwargs.get("seed")
        if seed_value is None and args:
            seed_value = args[0]
        seed_config = kwargs.get("seed_config", "")
        if not seed_config and len(args) > 7:
            seed_config = args[7]
        try:
            seed = int(seed_value)
        except (TypeError, ValueError):
            seed = -1
        if seed < 0 and cls._configured_cast_seed(seed_config) is None:
            return random.random()
        return tuple(args), tuple(sorted(kwargs.items()))

    def build(
        self,
        seed,
        row_number,
        women_weights,
        women_start_count,
        men_weights,
        men_start_count,
        empty_behavior,
        seed_config="",
    ):
        configured_seed = self._configured_cast_seed(seed_config)
        if configured_seed is None and int(seed) < 0:
            rng = random.Random(random.getrandbits(64))
        else:
            cast_seed = configured_seed if configured_seed is not None else int(seed)
            rng = random.Random(f"sxcp_cast_bias:{cast_seed}:{int(row_number)}")
        women_pairs = self._weight_pairs(women_weights, women_start_count)
        men_pairs = self._weight_pairs(men_weights, men_start_count)
        women_count = self._weighted_count(rng, women_pairs)
        men_count = self._weighted_count(rng, men_pairs)
        if women_count + men_count == 0:
            if empty_behavior == "force_one_man":
                men_count = 1
            elif empty_behavior != "allow_empty":
                women_count = 1
        config = build_cast_config_json(cast_mode="custom_counts", women_count=women_count, men_count=men_count)
        parsed = json.loads(config)
        summary = f"weighted cast: {parsed['women_count']} women, {parsed['men_count']} men"
        return config, parsed["women_count"], parsed["men_count"], summary


NODE_CLASS_MAPPINGS = {
    "SxCPCategoryPreset": SxCPCategoryPreset,
    "SxCPLocationPool": SxCPLocationPool,
    "SxCPCompositionPool": SxCPCompositionPool,
    "SxCPLocationTheme": SxCPLocationTheme,
    "SxCPStylePool": SxCPStylePool,
    "SxCPCastControl": SxCPCastControl,
    "SxCPCastBias": SxCPCastBias,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "SxCPCategoryPreset": "SxCP Category Preset",
    "SxCPLocationPool": "SxCP Location Pool",
    "SxCPCompositionPool": "SxCP Composition Pool",
    "SxCPLocationTheme": "SxCP Location Theme",
    "SxCPStylePool": "SxCP Style Pool",
    "SxCPCastControl": "SxCP Cast Control",
    "SxCPCastBias": "SxCP Cast Bias",
}
