from __future__ import annotations

import json
import random
from typing import Any, Iterable


SEED_AXIS_SALTS = {
    "category": 31,
    "subcategory": 37,
    "content": 41,
    "clothing": 41,
    "person": 43,
    "scene": 47,
    "pose": 53,
    "role": 57,
    "expression": 59,
    "composition": 61,
}

SEED_AXIS_ALIASES = {
    "category": ("category_seed", "category"),
    "subcategory": ("subcategory_seed", "subcategory"),
    "content": ("content_seed", "item_seed", "sexual_pose_seed", "content"),
    "clothing": ("clothing_seed", "outfit_seed", "wardrobe_seed", "content_seed", "content"),
    "person": ("person_seed", "appearance_seed", "cast_seed", "person"),
    "scene": ("scene_seed", "scene"),
    "pose": ("pose_seed", "sexual_pose_seed", "pose"),
    "role": ("role_seed", "role", "pose_seed", "sexual_pose_seed"),
    "expression": ("expression_seed", "face_seed", "expression"),
    "composition": ("composition_seed", "camera_seed", "composition"),
}

SEED_LOCK_AXES = (
    "category",
    "subcategory",
    "content",
    "clothing",
    "person",
    "scene",
    "pose",
    "role",
    "expression",
    "composition",
)
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
SEED_REROLL_GROUPS = {
    "none": (),
    "category": ("category",),
    "subcategory": ("subcategory",),
    "content": ("content",),
    "clothing": ("clothing",),
    "person": ("person",),
    "scene": ("scene",),
    "pose": ("pose", "role"),
    "role": ("role",),
    "expression": ("expression",),
    "composition": ("composition",),
    "content_pose": ("content", "pose", "role"),
    "content_clothing": ("content", "clothing"),
    "clothing_pose": ("clothing", "pose", "role"),
    "scene_pose": ("scene", "pose", "role"),
}
SEED_REROLL_AXIS_CHOICES = list(SEED_REROLL_GROUPS.keys())


def _normal_key(value: Any) -> str:
    return str(value or "").strip().lower().replace("-", "_").replace(" ", "_")


def seed_mode_choices() -> list[str]:
    return list(SEED_MODE_CHOICES)


def normalize_seed_mode(value: Any) -> str:
    normalized = _normal_key(value)
    aliases = {
        "follow": "follow_main",
        "followmain": "follow_main",
        "follow_main_seed": "follow_main",
        "main": "follow_main",
        "main_seed": "follow_main",
    }
    normalized = aliases.get(normalized, normalized)
    return normalized if normalized in SEED_MODE_CHOICES else "auto"


def seed_reroll_axis_choices() -> list[str]:
    return list(SEED_REROLL_AXIS_CHOICES)


def normalize_reroll_axis(value: Any) -> str:
    normalized = _normal_key(value)
    aliases = {
        "contentpose": "content_pose",
        "contentclothing": "content_clothing",
        "clothingpose": "clothing_pose",
        "scenepose": "scene_pose",
    }
    normalized = aliases.get(normalized, normalized)
    return normalized if normalized in SEED_REROLL_GROUPS else "none"


def row_seed(seed: int, row_number: int, salt: int = 0) -> int:
    return int(seed) + int(row_number) * 1009 + salt * 9176


def build_seed_config_json(
    category_seed: int = -1,
    subcategory_seed: int = -1,
    content_seed: int = -1,
    person_seed: int = -1,
    scene_seed: int = -1,
    pose_seed: int = -1,
    role_seed: int = -1,
    expression_seed: int = -1,
    composition_seed: int = -1,
    category_seed_mode: str = "auto",
    subcategory_seed_mode: str = "auto",
    content_seed_mode: str = "auto",
    person_seed_mode: str = "auto",
    scene_seed_mode: str = "auto",
    pose_seed_mode: str = "auto",
    role_seed_mode: str = "auto",
    expression_seed_mode: str = "auto",
    composition_seed_mode: str = "auto",
    clothing_seed: int = -1,
    clothing_seed_mode: str = "auto",
) -> str:
    rng = random.SystemRandom()

    def axis_seed(value: int, mode: str) -> int:
        mode = normalize_seed_mode(mode)
        if mode == "auto":
            return int(value)
        if mode == "random":
            return rng.randint(0, 0xFFFFFFFF)
        if mode == "fixed":
            return max(0, int(value))
        return -1

    return json.dumps(
        {
            "category_seed": axis_seed(category_seed, category_seed_mode),
            "subcategory_seed": axis_seed(subcategory_seed, subcategory_seed_mode),
            "content_seed": axis_seed(content_seed, content_seed_mode),
            "clothing_seed": axis_seed(clothing_seed, clothing_seed_mode),
            "person_seed": axis_seed(person_seed, person_seed_mode),
            "scene_seed": axis_seed(scene_seed, scene_seed_mode),
            "pose_seed": axis_seed(pose_seed, pose_seed_mode),
            "role_seed": axis_seed(role_seed, role_seed_mode),
            "expression_seed": axis_seed(expression_seed, expression_seed_mode),
            "composition_seed": axis_seed(composition_seed, composition_seed_mode),
        },
        ensure_ascii=True,
        sort_keys=True,
    )


def build_seed_lock_config_json(
    base_seed: int = 20260614,
    reroll_axis: str = "none",
    reroll_seed: int = -1,
) -> str:
    base_seed = int(base_seed)
    reroll_seed = int(reroll_seed)
    reroll = set(SEED_REROLL_GROUPS[normalize_reroll_axis(reroll_axis)])
    config: dict[str, int] = {}
    for axis in SEED_LOCK_AXES:
        config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
    return json.dumps(config, ensure_ascii=True, sort_keys=True)


def parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
    if not seed_config:
        return {}
    if isinstance(seed_config, dict):
        raw = seed_config
    else:
        try:
            raw = json.loads(str(seed_config))
        except json.JSONDecodeError as exc:
            raise ValueError(f"Invalid seed_config JSON: {exc}") from exc
    if not isinstance(raw, dict):
        raise ValueError("seed_config must be a JSON object")
    parsed: dict[str, int] = {}
    for key, value in raw.items():
        try:
            parsed[str(key)] = int(value)
        except (TypeError, ValueError):
            continue
    return parsed


def configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
    for key in SEED_AXIS_ALIASES.get(axis, (axis,)):
        value = seed_config.get(key)
        if value is not None and value >= 0:
            return value
    return None


def configured_seed_from_axes(
    seed_config: str | dict[str, Any] | None,
    axes: Iterable[str],
    *,
    extra_keys: Iterable[str] = (),
) -> int | None:
    try:
        parsed = parse_seed_config(seed_config)
    except ValueError:
        return None
    for axis in axes:
        value = configured_axis_seed(parsed, axis)
        if value is not None:
            return value
    for key in extra_keys:
        value = parsed.get(str(key))
        if value is not None and value >= 0:
            return value
    return None


def axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
    configured = configured_axis_seed(seed_config, axis)
    salt = SEED_AXIS_SALTS.get(axis, 0)
    if configured is None:
        return random.Random(row_seed(base_seed, row_number, salt))
    return random.Random(row_seed(configured, row_number, salt))


def axis_seed_trace(
    seed_config: str | dict[str, Any] | None,
    base_seed: int,
    row_number: int,
    axes: Iterable[str] = SEED_LOCK_AXES,
) -> dict[str, dict[str, int | str]]:
    parsed = parse_seed_config(seed_config)
    trace: dict[str, dict[str, int | str]] = {}
    for axis in axes:
        configured = configured_axis_seed(parsed, axis)
        seed_value = int(configured) if configured is not None else int(base_seed)
        source = "configured" if configured is not None else "main"
        salt = SEED_AXIS_SALTS.get(axis, 0)
        trace[axis] = {
            "source": source,
            "seed": seed_value,
            "rng_seed": row_seed(seed_value, row_number, salt),
        }
    return trace
