from __future__ import annotations

import json
from typing import Any

try:
    from .character_slot import parse_character_cast
    from .hardcore_position_config import (
        build_hardcore_position_pool_json,
        hardcore_position_family_choices,
        hardcore_position_key_choices,
        normalize_hardcore_position_family,
        normalize_hardcore_position_values,
    )
    from .location_config import build_composition_pool_json, build_location_pool_json
except ImportError:  # Allows local smoke tests from the repository root.
    from character_slot import parse_character_cast
    from hardcore_position_config import (
        build_hardcore_position_pool_json,
        hardcore_position_family_choices,
        hardcore_position_key_choices,
        normalize_hardcore_position_family,
        normalize_hardcore_position_values,
    )
    from location_config import build_composition_pool_json, build_location_pool_json


SXCP_LOCATION_CONFIG = "SXCP_LOCATION_CONFIG"
SXCP_COMPOSITION_CONFIG = "SXCP_COMPOSITION_CONFIG"
SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"

LOCK_CHOICES = [
    "none",
    "location_from_softcore",
    "location_from_hardcore",
    "composition_from_softcore",
    "composition_from_hardcore",
    "hardcore_position_current",
    "softcore_outfit_current",
]
WARDROBE_SUBJECTS = ["Woman A", "Man A", "all women", "all men", "all"]


def _loads(value: Any) -> dict[str, Any]:
    if not value:
        return {}
    if isinstance(value, dict):
        return value
    try:
        loaded = json.loads(str(value))
    except json.JSONDecodeError:
        return {}
    return loaded if isinstance(loaded, dict) else {}


def _dump(value: Any) -> str:
    return json.dumps(value, ensure_ascii=True, sort_keys=True)


def _text(value: Any) -> str:
    return str(value or "").strip()


def _row(meta: dict[str, Any], key: str) -> dict[str, Any]:
    value = meta.get(key)
    return value if isinstance(value, dict) else {}


def _first_text(*values: Any) -> str:
    for value in values:
        text = _text(value)
        if text:
            return text
    return ""


def _first_position_key(row: dict[str, Any], meta: dict[str, Any]) -> str:
    values: list[Any] = []
    if row.get("position_key") is not None:
        values.append(row.get("position_key"))
    if row.get("position_keys") is not None:
        position_keys = row.get("position_keys")
        values.extend(position_keys if isinstance(position_keys, list) else [position_keys])
    config = meta.get("hardcore_position_config")
    if isinstance(config, dict) and config.get("positions") is not None:
        positions = config.get("positions")
        values.extend(positions if isinstance(positions, list) else [positions])
    selected = normalize_hardcore_position_values(values)
    return selected[0] if selected else ""


def _position_family(row: dict[str, Any], meta: dict[str, Any]) -> str:
    config = meta.get("hardcore_position_config")
    raw_config_family = config.get("family") if isinstance(config, dict) else ""
    return normalize_hardcore_position_family(
        _first_text(row.get("position_family"), row.get("source_position_family"), raw_config_family),
        "any",
    )


def _choice_rows(meta: dict[str, Any]) -> list[dict[str, Any]]:
    soft = _row(meta, "softcore_row")
    hard = _row(meta, "hardcore_row")
    options = meta.get("options") if isinstance(meta.get("options"), dict) else {}
    rows = [
        ("softcore.level", "softcore", "Softcore Level", options.get("softcore_level")),
        ("softcore.cast", "softcore", "Softcore Cast", options.get("softcore_cast")),
        ("softcore.outfit", "softcore", "Softcore Outfit", soft.get("item")),
        ("softcore.pose", "softcore", "Softcore Pose", soft.get("pose")),
        ("softcore.expression", "softcore", "Softcore Expression", soft.get("expression")),
        ("hardcore.level", "hardcore", "Hardcore Level", options.get("hardcore_level")),
        ("hardcore.cast", "hardcore", "Hardcore Cast", options.get("hardcore_cast")),
        ("hardcore.position_family", "hardcore", "Position Family", _position_family(hard, meta)),
        ("hardcore.position_key", "hardcore", "Position Key", _first_position_key(hard, meta)),
        ("hardcore.role_graph", "hardcore", "Role Graph", hard.get("role_graph")),
        ("hardcore.action", "hardcore", "Action Text", hard.get("item")),
        ("hardcore.expression", "hardcore", "Hardcore Expression", hard.get("expression")),
        ("scene.softcore", "scene", "Softcore Location", soft.get("scene_text")),
        ("scene.hardcore", "scene", "Hardcore Location", hard.get("scene_text")),
        ("composition.softcore", "composition", "Softcore Composition", soft.get("composition")),
        ("composition.hardcore", "composition", "Hardcore Composition", hard.get("composition")),
        ("camera.softcore", "camera", "Softcore Camera", meta.get("softcore_camera_directive")),
        ("camera.hardcore", "camera", "Hardcore Camera", meta.get("hardcore_camera_directive")),
        ("style.softcore", "style", "Softcore Style", soft.get("positive_suffix")),
        ("style.hardcore", "style", "Hardcore Style", hard.get("positive_suffix")),
    ]
    return [
        {
            "axis": axis,
            "branch": branch,
            "label": label,
            "value": _text(value),
            "active": bool(_text(value)),
        }
        for axis, branch, label, value in rows
        if _text(value)
    ]


def _metadata_slots(meta: dict[str, Any]) -> list[dict[str, Any]]:
    slots = meta.get("character_cast_slots")
    if isinstance(slots, list):
        return parse_character_cast(slots)
    chain = meta.get("scene_chain") if isinstance(meta.get("scene_chain"), dict) else {}
    for branch in ("hardcore", "softcore"):
        branch_scene = chain.get(branch) if isinstance(chain.get(branch), dict) else {}
        configs = branch_scene.get("configs") if isinstance(branch_scene.get("configs"), dict) else {}
        parsed = parse_character_cast(configs.get("character_cast"))
        if parsed:
            return parsed
    return []


def _slot_matches_subject(slot: dict[str, Any], subject: str) -> bool:
    subject = _text(subject).lower()
    subject_type = _text(slot.get("subject_type")).lower()
    label = _text(slot.get("label")).upper()
    if subject == "all":
        return True
    if subject == "all women":
        return subject_type == "woman"
    if subject == "all men":
        return subject_type == "man"
    if subject.startswith("woman "):
        return subject_type == "woman" and label == subject.split(" ", 1)[1].upper()
    if subject.startswith("man "):
        return subject_type == "man" and label == subject.split(" ", 1)[1].upper()
    return False


def _wardrobe_character_cast(
    meta: dict[str, Any],
    subject: str,
    softcore_outfit: str,
    hardcore_clothing: str,
) -> tuple[str, int]:
    slots = _metadata_slots(meta)
    if not slots:
        return "", 0
    changed = 0
    for slot in slots:
        if not _slot_matches_subject(slot, subject):
            continue
        if softcore_outfit:
            slot["softcore_outfit"] = softcore_outfit
            changed += 1
        if hardcore_clothing:
            slot["hardcore_clothing"] = hardcore_clothing
            changed += 1
    if not changed:
        return _dump({"profile_type": "character_cast", "version": 1, "slots": slots}), 0
    return _dump({"profile_type": "character_cast", "version": 1, "slots": slots}), changed


def _lock_location(meta: dict[str, Any], lock_choice: str) -> str:
    soft = _row(meta, "softcore_row")
    hard = _row(meta, "hardcore_row")
    if lock_choice == "location_from_softcore":
        return _text(soft.get("scene_text"))
    if lock_choice == "location_from_hardcore":
        return _text(hard.get("scene_text"))
    return ""


def _lock_composition(meta: dict[str, Any], lock_choice: str) -> str:
    soft = _row(meta, "softcore_row")
    hard = _row(meta, "hardcore_row")
    if lock_choice == "composition_from_softcore":
        return _text(soft.get("composition"))
    if lock_choice == "composition_from_hardcore":
        return _text(hard.get("composition"))
    return ""


def _summary_parts(*parts: str) -> str:
    clean = [part for part in (_text(part) for part in parts) if part]
    return "; ".join(clean) if clean else "no overrides active"


class SxCPChoiceBoard:
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "metadata_json": ("STRING", {"default": "", "multiline": True}),
                "lock_choice": (LOCK_CHOICES, {"default": "none"}),
                "location_override": ("STRING", {"default": "", "multiline": True}),
                "composition_override": ("STRING", {"default": "", "multiline": True}),
                "hardcore_position_family": (["auto"] + hardcore_position_family_choices(), {"default": "auto"}),
                "hardcore_position_key": (["auto"] + hardcore_position_key_choices(), {"default": "auto"}),
                "wardrobe_subject": (WARDROBE_SUBJECTS, {"default": "Woman A"}),
                "softcore_outfit_override": ("STRING", {"default": "", "multiline": True}),
                "hardcore_clothing_override": ("STRING", {"default": "", "multiline": True}),
            },
        }

    RETURN_TYPES = (
        SXCP_LOCATION_CONFIG,
        SXCP_COMPOSITION_CONFIG,
        SXCP_HARDCORE_POSITION_CONFIG,
        SXCP_CHARACTER_CAST,
        "STRING",
        "STRING",
    )
    RETURN_NAMES = (
        "location_config",
        "composition_config",
        "hardcore_position_config",
        "character_cast",
        "choice_board_json",
        "summary",
    )
    FUNCTION = "build"
    CATEGORY = "prompt_builder/v2_scene"

    def build(
        self,
        metadata_json,
        lock_choice,
        location_override,
        composition_override,
        hardcore_position_family,
        hardcore_position_key,
        wardrobe_subject,
        softcore_outfit_override,
        hardcore_clothing_override,
    ):
        meta = _loads(metadata_json)
        soft = _row(meta, "softcore_row")
        hard = _row(meta, "hardcore_row")

        location_text = _text(location_override) or _lock_location(meta, lock_choice)
        composition_text = _text(composition_override) or _lock_composition(meta, lock_choice)

        family = _text(hardcore_position_family)
        key = _text(hardcore_position_key)
        if lock_choice == "hardcore_position_current":
            if family == "auto":
                family = _position_family(hard, meta)
            if key == "auto":
                key = _first_position_key(hard, meta)
        if family == "auto":
            family = "any"
        if key == "auto":
            key = ""

        softcore_outfit = _text(softcore_outfit_override)
        if lock_choice == "softcore_outfit_current" and not softcore_outfit:
            softcore_outfit = _text(soft.get("item"))
        hardcore_clothing = _text(hardcore_clothing_override)

        location_config = (
            build_location_pool_json(
                enabled=True,
                combine_mode="replace",
                preset="custom_only",
                custom_locations=location_text,
            )
            if location_text
            else ""
        )
        composition_config = (
            build_composition_pool_json(
                enabled=True,
                combine_mode="replace",
                preset="custom_only",
                custom_compositions=composition_text,
            )
            if composition_text
            else ""
        )
        selected_positions = [key] if key else []
        position_active = bool(selected_positions) or family != "any"
        hardcore_position_config = (
            build_hardcore_position_pool_json(
                combine_mode="replace",
                family=family,
                selected_positions=selected_positions,
            )
            if position_active
            else ""
        )
        character_cast, wardrobe_changes = _wardrobe_character_cast(
            meta,
            wardrobe_subject,
            softcore_outfit,
            hardcore_clothing,
        ) if softcore_outfit or hardcore_clothing else ("", 0)

        board = {
            "version": 1,
            "choices": _choice_rows(meta),
            "overrides": {
                "lock_choice": lock_choice,
                "location": location_text,
                "composition": composition_text,
                "hardcore_position_family": family if position_active else "",
                "hardcore_position_key": key,
                "wardrobe_subject": wardrobe_subject,
                "softcore_outfit": softcore_outfit,
                "hardcore_clothing": hardcore_clothing,
            },
            "outputs": {
                "location_config": bool(location_config),
                "composition_config": bool(composition_config),
                "hardcore_position_config": bool(hardcore_position_config),
                "character_cast": bool(character_cast),
                "wardrobe_changes": wardrobe_changes,
            },
        }
        summary = _summary_parts(
            f"location locked" if location_config else "",
            f"composition locked" if composition_config else "",
            f"position locked ({family}{':' + key if key else ''})" if hardcore_position_config else "",
            f"wardrobe updated x{wardrobe_changes}" if wardrobe_changes else "",
        )
        board_json = _dump(board)
        return {
            "ui": {
                "choice_board_json": [board_json],
                "summary": [summary],
            },
            "result": (
                location_config,
                composition_config,
                hardcore_position_config,
                character_cast,
                board_json,
                summary,
            ),
        }


NODE_CLASS_MAPPINGS = {
    "SxCPChoiceBoard": SxCPChoiceBoard,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "SxCPChoiceBoard": "SxCP Choice Board",
}
