from __future__ import annotations

import json
import random
from typing import Any

try:
    from . import character_config as character_policy
    from . import character_profile as character_profile_policy
    from . import filter_config as filter_policy
    from . import pov_policy
    from . import seed_config as seed_policy
except ImportError:  # Allows local smoke tests with top-level imports.
    import character_config as character_policy
    import character_profile as character_profile_policy
    import filter_config as filter_policy
    import pov_policy
    import seed_config as seed_policy


def _is_false(value: Any) -> bool:
    if isinstance(value, bool):
        return value is False
    if isinstance(value, str):
        return value.strip().lower() in ("false", "0", "no", "off")
    return False


def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
    try:
        number = float(value)
    except (TypeError, ValueError):
        return default
    return max(min_value, min(max_value, number))


def normalize_slot_expression_intensity(value: Any) -> float:
    try:
        intensity = float(value)
    except (TypeError, ValueError):
        return -1.0
    if intensity < 0:
        return -1.0
    return _clamped_float(intensity, 0.5)


def slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
    if not slot:
        return True
    return not _is_false(slot.get("expression_enabled", True))


def slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
    if not slot or not slot_expression_enabled(slot):
        return None
    intensity = normalize_slot_expression_intensity(slot.get("expression_intensity"))
    return intensity if intensity >= 0 else None


def slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
    if not slot or not slot_expression_enabled(slot):
        return None
    phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else ""
    if phase_key:
        intensity = normalize_slot_expression_intensity(slot.get(phase_key))
        if intensity >= 0:
            return intensity
    return slot_expression_intensity(slot)


def normalize_slot_seed(value: Any) -> int:
    return character_policy.normalize_slot_seed(value)


def slot_seed(slot: dict[str, Any] | None) -> int:
    if not slot:
        return -1
    return normalize_slot_seed(slot.get("slot_seed"))


def slot_seeded_rng(slot: dict[str, Any] | None, salt: int) -> random.Random | None:
    seed = slot_seed(slot)
    if seed < 0:
        return None
    return random.Random(seed_policy.row_seed(seed, 1, salt))


def slot_context_rng(slot: dict[str, Any], fallback_rng: random.Random) -> random.Random:
    return slot_seeded_rng(slot, 701) or fallback_rng


def slot_effective_figure(
    slot: dict[str, Any],
    subject_type: str,
    fallback_figure: str,
) -> str:
    raw_figure = str(slot.get("figure") or "random").strip()
    if raw_figure in ("curvy", "balanced", "bombshell"):
        return raw_figure
    seeded_rng = slot_seeded_rng(slot, 709)
    if subject_type == "woman" and seeded_rng is not None:
        options = ["curvy", "balanced", "bombshell"]
        return options[seeded_rng.randrange(len(options))]
    return fallback_figure


def slot_manual_or_choice(choice: str, manual_value: str) -> str:
    choice = str(choice or "").strip()
    manual_value = str(manual_value or "").strip()
    if choice == "manual":
        return manual_value or "random"
    if choice.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
        return "random"
    return choice


def normalize_slot_ethnicity(value: Any) -> str:
    return filter_policy.normalize_ethnicity_filter(value, "random", allow_random=True)


def normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
    subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
    if subject_type not in ("woman", "man"):
        subject_type = "woman"
    label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
    label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
    if label == "AUTO_CHAIN":
        label = "auto_chain"
    if label not in character_policy.CHARACTER_LABEL_CHOICES:
        label = "auto_chain"

    manual_config = character_profile_policy.parse_character_manual_config(slot.get("manual") or slot.get("manual_config"))

    raw_age = str(slot.get("age") or "random")
    raw_manual_age = str(slot.get("manual_age") or "").strip()
    if not raw_manual_age and manual_config.get("manual_age"):
        raw_manual_age = manual_config["manual_age"]
        if raw_age.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
            raw_age = "manual"
    age = slot_manual_or_choice(raw_age, raw_manual_age)

    raw_body = str(slot.get("body") or "random")
    raw_manual_body = str(slot.get("manual_body") or "").strip()
    if not raw_manual_body and manual_config.get("manual_body"):
        raw_manual_body = manual_config["manual_body"]
        if raw_body.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
            raw_body = "manual"
    body = slot_manual_or_choice(raw_body, raw_manual_body)
    figure = str(slot.get("figure") or "random").strip()
    if figure not in character_policy.CHARACTER_FIGURE_CHOICES:
        figure = "random"

    def manual_fallback(field: str) -> str:
        direct = character_policy.slot_value(slot.get(field))
        return direct or manual_config.get(field, "")

    normalized = {
        "profile_type": "character_slot",
        "subject_type": subject_type,
        "label": label,
        "slot_seed": normalize_slot_seed(slot.get("slot_seed")),
        "age": age,
        "ethnicity": normalize_slot_ethnicity(slot.get("ethnicity")),
        "figure": figure,
        "body": body,
        "body_phrase": manual_fallback("body_phrase"),
        "skin": manual_fallback("skin"),
        "hair": manual_fallback("hair"),
        "manual": manual_config,
        "characteristics": (
            slot.get("characteristics")
            if isinstance(slot.get("characteristics"), dict)
            else character_policy.slot_value(slot.get("characteristics") or slot.get("characteristics_config"))
        ),
        "hair_config": (
            slot.get("hair_config")
            if isinstance(slot.get("hair_config"), dict)
            else character_policy.slot_value(slot.get("hair_config"))
        ),
        "hair_color": character_policy.normalize_hair_choice(slot.get("hair_color"), character_policy.CHARACTER_HAIR_COLOR_CHOICES),
        "hair_length": character_policy.normalize_hair_choice(
            slot.get("hair_length"),
            character_policy.CHARACTER_HAIR_LENGTH_CHOICES,
        ),
        "hair_style": character_policy.normalize_hair_choice(slot.get("hair_style"), character_policy.CHARACTER_HAIR_STYLE_CHOICES),
        "eyes": manual_fallback("eyes"),
        "descriptor_detail": character_policy.normalize_descriptor_detail(slot.get("descriptor_detail")),
        "presence_mode": character_policy.normalize_presence_mode(slot.get("presence_mode"), subject_type),
        "softcore_outfit": manual_fallback("softcore_outfit"),
        "hardcore_clothing": (
            character_policy.slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit"))
            or manual_config.get("hardcore_clothing", "")
        ),
        "expression_enabled": not _is_false(slot.get("expression_enabled", True)),
        "expression_intensity": normalize_slot_expression_intensity(slot.get("expression_intensity")),
        "softcore_expression_intensity": normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
        "hardcore_expression_intensity": normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")),
    }
    normalized["summary"] = character_slot_summary(normalized)
    return normalized


def parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
    if not character_cast:
        return []
    if isinstance(character_cast, list):
        raw = character_cast
    elif isinstance(character_cast, dict):
        raw = character_cast
    else:
        try:
            raw = json.loads(str(character_cast))
        except json.JSONDecodeError as exc:
            raise ValueError(f"Invalid character_cast JSON: {exc}") from exc

    if isinstance(raw, list):
        slots = raw
    elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
        slots = raw["slots"]
    elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
        slots = [raw]
    elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
        slots = [raw]
    else:
        return []
    return [normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]


def character_slot_summary(slot: dict[str, Any]) -> str:
    subject = str(slot.get("subject_type") or "woman")
    label = str(slot.get("label") or "auto_chain")
    label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
    parts = [
        subject,
        label_text,
        f"seed={slot.get('slot_seed')}" if slot_seed(slot) >= 0 else "",
        f"age={slot.get('age', 'random')}",
        f"ethnicity={slot.get('ethnicity', 'random')}",
        f"figure={slot.get('figure', 'random')}",
        f"body={slot.get('body', 'random')}",
        f"detail={slot.get('descriptor_detail', 'auto')}",
    ]
    parts = [part for part in parts if part]
    if pov_policy.slot_is_pov(slot):
        parts.append("presence=pov")
    if not slot_expression_enabled(slot):
        parts.append("expression=disabled")
    else:
        expression_intensity = slot_expression_intensity(slot)
        if expression_intensity is not None:
            parts.append(f"expression={expression_intensity:.2f}")
        softcore_expression_intensity = slot_expression_intensity_for_phase(slot, "softcore")
        hardcore_expression_intensity = slot_expression_intensity_for_phase(slot, "hardcore")
        if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity:
            parts.append(f"soft_expr={softcore_expression_intensity:.2f}")
        if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity:
            parts.append(f"hard_expr={hardcore_expression_intensity:.2f}")
    if slot.get("softcore_outfit"):
        parts.append(f"soft_outfit={slot['softcore_outfit']}")
    if slot.get("hardcore_clothing"):
        parts.append(f"hard_clothing={slot['hardcore_clothing']}")
    characteristics = character_policy.parse_characteristics_config(slot.get("characteristics"))
    characteristics_summary = character_policy.characteristics_summary(characteristics)
    if characteristics_summary != "characteristics unrestricted":
        parts.append(f"characteristics={characteristics_summary}")
    hair_config = character_policy.parse_hair_config(slot.get("hair_config"))
    hair_config_summary = character_policy.hair_config_summary(hair_config)
    if hair_config_summary != "hair unrestricted":
        parts.append(f"hair={hair_config_summary}")
    for key in ("hair_color", "hair_length", "hair_style"):
        value = slot.get(key)
        if value and value != "random":
            parts.append(f"{key}={value}")
    for key in ("body_phrase", "skin", "hair", "eyes"):
        value = slot.get(key)
        if value:
            parts.append(f"{key}={value}")
    return "; ".join(parts)


def build_character_slot_json(
    subject_type: str = "woman",
    label: str = "auto_chain",
    slot_seed: int = -1,
    age: str = "random",
    manual_age: str = "",
    manual: str | dict[str, Any] | None = "",
    ethnicity: str = "random",
    figure: str = "random",
    body: str = "random",
    manual_body: str = "",
    body_phrase: str = "",
    skin: str = "",
    hair: str = "",
    characteristics: str | dict[str, Any] | None = "",
    hair_config: str | dict[str, Any] | None = "",
    hair_color: str = "random",
    hair_length: str = "random",
    hair_style: str = "random",
    eyes: str = "",
    descriptor_detail: str = "auto",
    expression_enabled: bool = True,
    expression_intensity: float = -1.0,
    enabled: bool = True,
    character_cast: str | dict[str, Any] | list[Any] | None = "",
    presence_mode: str = "visible",
    softcore_expression_intensity: float = -1.0,
    hardcore_expression_intensity: float = -1.0,
    softcore_outfit: str = "",
    hardcore_clothing: str = "",
) -> dict[str, str]:
    existing_slots = parse_character_cast(character_cast)
    slot = normalize_character_slot(
        {
            "subject_type": subject_type,
            "label": label,
            "slot_seed": slot_seed,
            "age": age,
            "manual_age": manual_age,
            "manual": manual,
            "ethnicity": ethnicity,
            "figure": figure,
            "body": body,
            "manual_body": manual_body,
            "body_phrase": body_phrase,
            "skin": skin,
            "hair": hair,
            "characteristics": characteristics,
            "hair_config": hair_config,
            "hair_color": hair_color,
            "hair_length": hair_length,
            "hair_style": hair_style,
            "eyes": eyes,
            "descriptor_detail": descriptor_detail,
            "presence_mode": presence_mode,
            "softcore_outfit": softcore_outfit,
            "hardcore_clothing": hardcore_clothing,
            "expression_enabled": expression_enabled,
            "expression_intensity": expression_intensity,
            "softcore_expression_intensity": softcore_expression_intensity,
            "hardcore_expression_intensity": hardcore_expression_intensity,
        }
    )
    slots = existing_slots + ([slot] if enabled else [])
    cast = {
        "profile_type": "character_cast",
        "version": 1,
        "slots": slots,
    }
    return {
        "character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
        "character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
        "summary": slot["summary"] if enabled else "disabled",
        "status": f"{len(slots)} slot(s)",
    }
