from __future__ import annotations

import json
import math
import random

try:
    from .seed_config import (
        build_seed_config_json,
        build_seed_lock_config_json,
        configured_seed_from_axes,
        normalize_reroll_axis,
        seed_reroll_axis_choices,
        seed_mode_choices,
    )
except ImportError:  # Allows local smoke tests from the repository root.
    from seed_config import (
        build_seed_config_json,
        build_seed_lock_config_json,
        configured_seed_from_axes,
        normalize_reroll_axis,
        seed_reroll_axis_choices,
        seed_mode_choices,
    )


SXCP_SEED_CONFIG = "SXCP_SEED_CONFIG"

SDXL_BUCKET_RESOLUTIONS = [
    {"orientation": "portrait", "width": 896, "height": 1792, "aspect": 0.50, "mp": 1.61},
    {"orientation": "portrait", "width": 960, "height": 1664, "aspect": 0.58, "mp": 1.60},
    {"orientation": "portrait", "width": 1024, "height": 1600, "aspect": 0.64, "mp": 1.64},
    {"orientation": "portrait", "width": 1088, "height": 1472, "aspect": 0.74, "mp": 1.60},
    {"orientation": "portrait", "width": 1152, "height": 1408, "aspect": 0.82, "mp": 1.62},
    {"orientation": "portrait", "width": 1216, "height": 1344, "aspect": 0.90, "mp": 1.63},
    {"orientation": "square", "width": 1280, "height": 1280, "aspect": 1.00, "mp": 1.64},
    {"orientation": "landscape", "width": 1344, "height": 1216, "aspect": 1.11, "mp": 1.63},
    {"orientation": "landscape", "width": 1408, "height": 1152, "aspect": 1.22, "mp": 1.62},
    {"orientation": "landscape", "width": 1472, "height": 1088, "aspect": 1.35, "mp": 1.60},
    {"orientation": "landscape", "width": 1536, "height": 1024, "aspect": 1.50, "mp": 1.57},
]

KREA2_API_ASPECT_RATIOS = ["1:1", "4:3", "3:2", "16:9", "2.35:1", "4:5", "2:3", "9:16"]
KREA2_ASPECT_RATIOS = KREA2_API_ASPECT_RATIOS + ["8:9", "21:9", "9:21", "3:1", "1:3"]
KREA2_MEGAPIXEL_PRESETS = [
    "1.0MP",
    "1.25MP",
    "1.5MP",
    "1.75MP",
    "2.0MP",
    "2.25MP",
    "2.5MP",
    "2.75MP",
    "3.0MP",
    "3.25MP",
    "3.5MP",
    "3.75MP",
    "4.0MP",
    "max_for_aspect",
]


class SxCPSeedControl:
    SEED_AXES = (
        "category",
        "subcategory",
        "content",
        "person",
        "scene",
        "pose",
        "role",
        "expression",
        "composition",
        "clothing",
    )

    @classmethod
    def INPUT_TYPES(cls):
        seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}
        required = {}
        for axis in cls.SEED_AXES:
            required[f"{axis}_seed_mode"] = (seed_mode_choices(), {"default": "auto"})
            required[f"{axis}_seed"] = ("INT", seed_spec)
        return {"required": required}

    RETURN_TYPES = (SXCP_SEED_CONFIG, "STRING")
    RETURN_NAMES = ("seed_config", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    @classmethod
    def IS_CHANGED(cls, *args, **kwargs):
        values = list(args) + list(kwargs.values())
        if "random" in values:
            return random.random()
        return tuple(args), tuple(sorted(kwargs.items()))

    @classmethod
    def _summary(cls, config_json):
        try:
            config = json.loads(config_json)
        except (TypeError, ValueError, json.JSONDecodeError):
            return "invalid seed config"
        parts = []
        for axis in cls.SEED_AXES:
            try:
                value = int(config.get(f"{axis}_seed", -1))
            except (TypeError, ValueError):
                value = -1
            parts.append(f"{axis}={'follow_main' if value < 0 else value}")
        return "resolved seeds: " + "; ".join(parts)

    def build(
        self,
        category_seed_mode,
        category_seed,
        subcategory_seed_mode,
        subcategory_seed,
        content_seed_mode,
        content_seed,
        person_seed_mode,
        person_seed,
        scene_seed_mode,
        scene_seed,
        pose_seed_mode,
        pose_seed,
        role_seed_mode,
        role_seed,
        expression_seed_mode,
        expression_seed,
        composition_seed_mode,
        composition_seed,
        clothing_seed_mode="auto",
        clothing_seed=-1,
    ):
        config = build_seed_config_json(
            category_seed=category_seed,
            subcategory_seed=subcategory_seed,
            content_seed=content_seed,
            clothing_seed=clothing_seed,
            person_seed=person_seed,
            scene_seed=scene_seed,
            pose_seed=pose_seed,
            role_seed=role_seed,
            expression_seed=expression_seed,
            composition_seed=composition_seed,
            category_seed_mode=category_seed_mode,
            subcategory_seed_mode=subcategory_seed_mode,
            content_seed_mode=content_seed_mode,
            clothing_seed_mode=clothing_seed_mode,
            person_seed_mode=person_seed_mode,
            scene_seed_mode=scene_seed_mode,
            pose_seed_mode=pose_seed_mode,
            role_seed_mode=role_seed_mode,
            expression_seed_mode=expression_seed_mode,
            composition_seed_mode=composition_seed_mode,
        )
        return (
            config,
            self._summary(config),
        )


class SxCPGlobalSeed:
    @classmethod
    def INPUT_TYPES(cls):
        seed_spec = {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}
        return {
            "required": {
                "global_seed": ("INT", seed_spec),
            }
        }

    RETURN_TYPES = ("INT", SXCP_SEED_CONFIG, "STRING")
    RETURN_NAMES = ("seed", "seed_config", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(self, global_seed):
        seed = max(0, min(0xFFFFFFFF, int(global_seed)))
        config = build_seed_lock_config_json(base_seed=seed, reroll_axis="none", reroll_seed=-1)
        return seed, config, f"global seed {seed}; all axes locked"


class SxCPSeedLocker:
    @classmethod
    def INPUT_TYPES(cls):
        seed_spec = {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}
        reroll_seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}
        return {
            "required": {
                "base_seed": ("INT", seed_spec),
                "reroll_axis": (
                    seed_reroll_axis_choices(),
                    {"default": "none"},
                ),
                "reroll_seed": ("INT", reroll_seed_spec),
            }
        }

    RETURN_TYPES = (SXCP_SEED_CONFIG, "STRING")
    RETURN_NAMES = ("seed_config", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder"

    def build(self, base_seed, reroll_axis, reroll_seed):
        normalized_axis = normalize_reroll_axis(reroll_axis)
        config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=normalized_axis, reroll_seed=reroll_seed)
        summary = f"base {base_seed}; reroll {normalized_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}"
        return config, summary


class SxCPSDXLBucketSize:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "orientation": (["any", "portrait", "square", "landscape"], {"default": "any"}),
                "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}),
                "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}),
                "bucket_index": ("INT", {"default": 0, "min": 0, "max": len(SDXL_BUCKET_RESOLUTIONS), "step": 1}),
            },
            "optional": {
                "seed_config": (SXCP_SEED_CONFIG,),
            },
        }

    RETURN_TYPES = ("INT", "INT", "STRING", "STRING", "FLOAT", "FLOAT", "INT", "STRING")
    RETURN_NAMES = ("width", "height", "resolution", "orientation", "aspect", "megapixels", "bucket_index", "summary")
    FUNCTION = "build"
    CATEGORY = "prompt_builder/util"

    @staticmethod
    def _configured_bucket_seed(seed_config):
        return configured_seed_from_axes(
            seed_config,
            ("composition", "content"),
            extra_keys=("seed", "global_seed"),
        )

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

    def build(self, orientation, seed, row_number, bucket_index, seed_config=""):
        orientation = str(orientation or "any").strip().lower()
        pool = [
            (index + 1, bucket)
            for index, bucket in enumerate(SDXL_BUCKET_RESOLUTIONS)
            if orientation == "any" or bucket["orientation"] == orientation
        ]
        if not pool:
            pool = list(enumerate(SDXL_BUCKET_RESOLUTIONS, start=1))
        if int(bucket_index) > 0:
            pool_position = max(1, min(len(pool), int(bucket_index))) - 1
        else:
            configured_seed = self._configured_bucket_seed(seed_config)
            if configured_seed is None and int(seed) < 0:
                rng = random.Random(random.getrandbits(64))
            else:
                bucket_seed = configured_seed if configured_seed is not None else int(seed)
                rng = random.Random(f"sdxl_bucket:{bucket_seed}:{int(row_number)}:{orientation}")
            pool_position = rng.randrange(len(pool))
        selected_index, selected = pool[pool_position]
        width = int(selected["width"])
        height = int(selected["height"])
        selected_orientation = str(selected["orientation"])
        aspect = float(selected["aspect"])
        mp = float(selected["mp"])
        resolution = f"{width}x{height}"
        summary = (
            f"{selected_orientation} bucket {pool_position + 1}/{len(pool)} "
            f"(table {selected_index}): {resolution}, aspect {aspect:.2f}, {mp:.2f} MP"
        )
        return width, height, resolution, selected_orientation, aspect, mp, selected_index, summary


class SxCPKrea2ResolutionSelector:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "megapixels": (KREA2_MEGAPIXEL_PRESETS, {"default": "1.0MP"}),
                "aspect_ratio": (KREA2_ASPECT_RATIOS, {"default": "1:1"}),
            },
        }

    RETURN_TYPES = ("INT", "INT", "STRING", "STRING", "STRING", "STRING", "FLOAT", "FLOAT", "STRING", "STRING", "STRING")
    RETURN_NAMES = (
        "width",
        "height",
        "resolution",
        "aspect_ratio",
        "api_aspect_ratio",
        "api_resolution",
        "megapixels",
        "max_megapixels_for_aspect",
        "orientation",
        "summary",
        "config_json",
    )
    FUNCTION = "select"
    CATEGORY = "prompt_builder/util"

    @staticmethod
    def _aspect_value(aspect_ratio, custom_aspect_width, custom_aspect_height, rng):
        selected = str(aspect_ratio or "1:1").strip()
        if selected == "random_api":
            selected = rng.choice(KREA2_API_ASPECT_RATIOS)
        if selected == "custom":
            width = max(0.1, float(custom_aspect_width))
            height = max(0.1, float(custom_aspect_height))
            return selected, width / height
        try:
            left, right = selected.split(":", 1)
            return selected, max(0.01, float(left) / float(right))
        except (TypeError, ValueError):
            return "1:1", 1.0

    @staticmethod
    def _closest_api_aspect(ratio):
        def parse(value):
            left, right = value.split(":", 1)
            return float(left) / float(right)

        return min(KREA2_API_ASPECT_RATIOS, key=lambda item: abs(math.log(parse(item) / max(0.01, ratio))))

    @staticmethod
    def _continuous_limit_mp(ratio, max_long_edge, max_megapixels):
        ratio = max(0.01, float(ratio))
        max_long = max(16.0, float(max_long_edge))
        if ratio >= 1.0:
            exact_width = max_long
            exact_height = max_long / ratio
        else:
            exact_width = max_long * ratio
            exact_height = max_long
        exact_mp = (exact_width * exact_height) / 1_000_000.0
        return max(0.01, min(float(max_megapixels), exact_mp))

    @staticmethod
    def _nearby_multiples(value, multiple):
        scaled = float(value) / float(multiple)
        values = {
            int(math.floor(scaled)) * multiple,
            int(round(scaled)) * multiple,
            int(math.ceil(scaled)) * multiple,
        }
        return {int(v) for v in values if int(v) > 0}

    @classmethod
    def _candidate_sizes(cls, ratio, max_long_edge, max_megapixels, multiple):
        max_long = max(multiple, int(max_long_edge) // multiple * multiple)
        max_pixels = float(max_megapixels) * 1_000_000.0
        candidates = set()
        for width in range(multiple, max_long + 1, multiple):
            for height in cls._nearby_multiples(float(width) / ratio, multiple):
                candidates.add((width, height))
        for height in range(multiple, max_long + 1, multiple):
            for width in cls._nearby_multiples(float(height) * ratio, multiple):
                candidates.add((width, height))
        valid = []
        for width, height in candidates:
            if width < multiple or height < multiple:
                continue
            if max(width, height) > max_long:
                continue
            if width * height > max_pixels + 1:
                continue
            valid.append((width, height))
        return valid

    @classmethod
    def _best_size(cls, ratio, target_megapixels, max_long_edge, max_megapixels, multiple):
        candidates = cls._candidate_sizes(ratio, max_long_edge, max_megapixels, multiple)
        if not candidates:
            fallback = max(multiple, int(max_long_edge) // multiple * multiple)
            return fallback, fallback, (fallback * fallback) / 1_000_000.0, 1.0
        target = max((multiple * multiple) / 1_000_000.0, float(target_megapixels))
        best = None
        best_score = None
        for width, height in candidates:
            actual_mp = (width * height) / 1_000_000.0
            actual_ratio = float(width) / float(height)
            ratio_error = abs(math.log(actual_ratio / max(0.01, ratio)))
            mp_error = abs(actual_mp - target) / max(target, 0.01)
            score = ratio_error * 4.0 + mp_error
            if best_score is None or score < best_score:
                best = (width, height, actual_mp, actual_ratio)
                best_score = score
        return best

    @staticmethod
    def _profile_limits(profile, custom_max_long_edge, custom_max_megapixels):
        profile = str(profile or "turbo_local_2k").strip()
        if profile == "raw_local_1k":
            return 1024, 1.05, "Krea2 RAW local explicit size, up to 1K"
        if profile == "api_hosted_1k":
            return 1024, 1.05, "Krea hosted API fields, 1K only"
        if profile == "custom_limit":
            return max(256, int(custom_max_long_edge)), max(0.10, float(custom_max_megapixels)), "custom explicit size limit"
        return 2048, 4.20, "Krea2 Turbo local explicit size, up to 2K"

    @staticmethod
    def _preset_megapixels(megapixel_preset):
        value = str(megapixel_preset or "1.0MP").strip()
        if value.endswith("MP"):
            try:
                return float(value[:-2])
            except ValueError:
                return 1.0
        return None

    def select(self, megapixels, aspect_ratio):
        multiple = 16
        profile = "turbo_local_2k"
        max_long_edge, max_profile_mp, _profile_label = self._profile_limits(profile, 2048, 4.20)
        resolved_aspect, ratio = self._aspect_value(aspect_ratio, 1.0, 1.0, random.Random("krea2_resolution"))
        api_aspect_ratio = resolved_aspect if resolved_aspect in KREA2_API_ASPECT_RATIOS else self._closest_api_aspect(ratio)

        continuous_max_mp = self._continuous_limit_mp(ratio, max_long_edge, max_profile_mp)
        max_width, max_height, max_actual_mp, max_actual_ratio = self._best_size(
            ratio, continuous_max_mp, max_long_edge, max_profile_mp, multiple
        )

        preset = str(megapixels or "1.0MP").strip()
        target_mp = self._preset_megapixels(preset)
        if preset == "max_for_aspect":
            target_mp = max_actual_mp
        if target_mp is None:
            target_mp = 1.0

        clamped = target_mp > max_actual_mp + 0.001
        effective_target_mp = min(float(target_mp), max_actual_mp)
        width, height, actual_mp, actual_ratio = self._best_size(
            ratio, effective_target_mp, max_long_edge, max_profile_mp, multiple
        )
        orientation = "square"
        if width > height:
            orientation = "landscape"
        elif height > width:
            orientation = "portrait"

        resolution = f"{width}x{height}"
        api_resolution = "1K"
        summary_parts = [
            f"{resolution}",
            f"{actual_mp:.2f} MP",
            f"aspect {resolved_aspect} ({actual_ratio:.3f})",
            f"max for aspect {max_width}x{max_height} / {max_actual_mp:.2f} MP",
            "Krea2 Turbo 2K",
            f"API equivalent {api_aspect_ratio} {api_resolution}",
        ]
        if clamped:
            summary_parts.append(f"target {target_mp:.2f} MP clamped to aspect/profile limit")
        summary = "; ".join(summary_parts)

        config = {
            "profile": profile,
            "width": width,
            "height": height,
            "resolution": resolution,
            "aspect_ratio": resolved_aspect,
            "aspect_ratio_value": actual_ratio,
            "target_megapixels": round(float(target_mp), 4),
            "megapixels": round(actual_mp, 4),
            "max_width_for_aspect": max_width,
            "max_height_for_aspect": max_height,
            "max_megapixels_for_aspect": round(max_actual_mp, 4),
            "api_aspect_ratio": api_aspect_ratio,
            "api_resolution": api_resolution,
            "orientation": orientation,
            "round_to": multiple,
            "clamped": clamped,
        }
        return (
            width,
            height,
            resolution,
            resolved_aspect,
            api_aspect_ratio,
            api_resolution,
            round(actual_mp, 4),
            round(max_actual_mp, 4),
            orientation,
            summary,
            json.dumps(config, ensure_ascii=True, sort_keys=True),
        )


NODE_CLASS_MAPPINGS = {
    "SxCPGlobalSeed": SxCPGlobalSeed,
    "SxCPSeedControl": SxCPSeedControl,
    "SxCPSeedLocker": SxCPSeedLocker,
    "SxCPSDXLBucketSize": SxCPSDXLBucketSize,
    "SxCPKrea2ResolutionSelector": SxCPKrea2ResolutionSelector,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "SxCPGlobalSeed": "SxCP Global Seed",
    "SxCPSeedControl": "SxCP Seed Control",
    "SxCPSeedLocker": "SxCP Seed Locker",
    "SxCPSDXLBucketSize": "SxCP SDXL Bucket Size",
    "SxCPKrea2ResolutionSelector": "SxCP Krea2 Resolution Selector",
}
