from __future__ import annotations

import json
import re
from string import Formatter
from typing import Any, Callable

try:
    from . import item_axis_policy
except ImportError:  # Allows local smoke tests with top-level imports.
    import item_axis_policy


HARDCORE_POSITION_FAMILY_CHOICES = [
    "any",
    "penetrative",
    "foreplay",
    "interaction",
    "manual",
    "oral",
    "outercourse",
    "anal",
    "climax",
    "threesome",
    "group",
]
HARDCORE_POSITION_FOCUS_CHOICES = [
    "keep_pool",
    "penetration_only",
    "foreplay_only",
    "interaction_only",
    "manual_only",
    "oral_only",
    "outercourse_only",
    "anal_only",
    "climax_only",
    "threesome_only",
    "group_only",
]
HARDCORE_POSITION_KEY_CHOICES = [
    "missionary",
    "missionary_folded",
    "cowgirl",
    "cowgirl_alt",
    "reverse_cowgirl",
    "reverse_cowgirl_alt",
    "doggy",
    "bent_over",
    "face_down_ass_up",
    "standing",
    "side_lying",
    "edge_supported",
    "kneeling",
    "top_down_oral",
    "lotus_lap",
    "face_sitting",
    "sixty_nine",
    "reclining_oral",
    "blowjob_sitting",
    "straddled_oral",
    "spread_leg_oral",
    "chair_oral",
    "kissing",
    "caressing",
    "breast_touch",
    "face_touch",
    "undressing",
    "body_worship",
    "nipple_play",
    "ass_grab",
    "thigh_kissing",
    "hair_holding",
    "wrist_pinning",
    "dirty_talk",
    "position_transition",
    "guided_positioning",
    "camera_showing",
    "watching",
    "aftercare",
    "cleanup",
    "fingering",
    "clit_rubbing",
    "mutual_masturbation",
    "boobjob",
    "testicle_sucking",
    "penis_licking",
    "handjob",
    "footjob",
    "open_thighs",
    "front_back",
]
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
    "any": [
        "penetrative_sex",
        "foreplay_teasing",
        "body_worship_touching",
        "clothing_position_transitions",
        "dominant_guidance",
        "camera_performance",
        "manual_stimulation",
        "oral_sex",
        "outercourse_sex",
        "anal_double_penetration",
        "threesomes",
        "group_coordination",
        "group_sex_orgy",
        "cumshot_climax",
        "aftercare_cleanup",
    ],
    "penetrative": ["penetrative_sex"],
    "foreplay": ["foreplay_teasing"],
    "interaction": [
        "foreplay_teasing",
        "body_worship_touching",
        "clothing_position_transitions",
        "dominant_guidance",
        "camera_performance",
        "group_coordination",
        "aftercare_cleanup",
    ],
    "manual": ["manual_stimulation"],
    "oral": ["oral_sex"],
    "outercourse": ["outercourse_sex", "manual_stimulation"],
    "anal": ["anal_double_penetration"],
    "climax": ["cumshot_climax"],
    "threesome": ["threesomes"],
    "group": ["group_sex_orgy"],
}
HARDCORE_POSITION_KEY_MATCHES = {
    "missionary": ("missionary", "above her", "under her"),
    "missionary_folded": ("folded missionary", "knees-to-chest", "knees to chest", "folded legs", "folded high"),
    "cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
    "cowgirl_alt": ("cowgirl-alt", "low cowgirl", "seated-squat cowgirl", "low seated squat"),
    "reverse_cowgirl": ("reverse cowgirl", "facing away"),
    "reverse_cowgirl_alt": ("reverse cowgirl alt", "upright reverse cowgirl", "upright back-facing straddle"),
    "doggy": ("doggy", "all fours", "rear-entry", "from behind"),
    "bent_over": ("bent-over", "bent over", "hips raised"),
    "face_down_ass_up": ("face-down", "ass-up"),
    "standing": ("standing", "stands", "braced standing"),
    "side_lying": ("side-lying", "side lying", "spooning", "on the side", "on her side"),
    "edge_supported": ("edge-of-bed", "edge of bed", "bed edge", "raised edge", "edge-supported"),
    "kneeling": ("kneeling", "kneels", "kneeling center"),
    "top_down_oral": ("top-down oral", "top view oral", "top-view oral", "nadir-angle", "overhead oral"),
    "lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
    "face_sitting": ("face-sitting", "face sitting"),
    "sixty_nine": ("sixty-nine", "69"),
    "reclining_oral": ("reclining cunnilingus",),
    "blowjob_sitting": ("blowjob_sitting", "upright sitting oral", "sitting upright oral", "seated oral"),
    "straddled_oral": ("straddled oral",),
    "spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
    "chair_oral": ("chair oral",),
    "kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
    "caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
    "breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
    "face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
    "undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
    "body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
    "nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
    "ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
    "thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
    "hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
    "wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
    "dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
    "position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
    "guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
    "camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
    "watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
    "aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
    "cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
    "fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
    "clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
    "mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
    "boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
    "testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
    "penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
    "handjob": ("handjob", "hand job", "stroking the penis", "hand stroking", "manual stimulation"),
    "footjob": ("footjob", "soles", "toes curled", "feet stroking"),
    "open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
    "front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
}


def _text_matches_position_key(text: str, position: str) -> bool:
    terms = HARDCORE_POSITION_KEY_MATCHES.get(position, ())
    if not any(term in text for term in terms):
        return False
    if position == "missionary" and any(term in text for term in HARDCORE_POSITION_KEY_MATCHES["missionary_folded"]):
        return False
    if position == "cowgirl" and any(
        term in text
        for term in (
            HARDCORE_POSITION_KEY_MATCHES["cowgirl_alt"]
            + HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl"]
            + HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl_alt"]
        )
    ):
        return False
    if position == "reverse_cowgirl" and any(term in text for term in HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl_alt"]):
        return False
    return True


HARDCORE_POSITION_AXIS_KEYS = {
    "position",
    "body_position",
    "body_arrangement",
    "arrangement",
    "tease_act",
    "touch_detail",
    "manual_act",
    "manual_detail",
    "worship_act",
    "transition_act",
    "control_act",
    "performance_act",
    "coordination_act",
    "aftercare_act",
    "cleanup_detail",
}
RESTORE_PROMPT_AXIS_CHOICES = [
    "clothing_detail",
    "face_detail",
    "expression_detail",
    "mouth_detail",
    "reaction_detail",
    "body_contact",
    "hand_detail",
    "touch_detail",
    "foreplay_detail",
    "performance_act",
    "visibility",
    "angle",
]
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
    "penetrative_sex": "penetrative",
    "foreplay_teasing": "foreplay",
    "body_worship_touching": "interaction",
    "clothing_position_transitions": "interaction",
    "dominant_guidance": "interaction",
    "camera_performance": "interaction",
    "manual_stimulation": "manual",
    "oral_sex": "oral",
    "outercourse_sex": "outercourse",
    "anal_double_penetration": "anal",
    "threesomes": "threesome",
    "group_coordination": "interaction",
    "group_sex_orgy": "group",
    "cumshot_climax": "climax",
    "aftercare_cleanup": "interaction",
}
FOCUS_FAMILY_BY_KEY = {
    "penetration_only": "penetrative",
    "foreplay_only": "foreplay",
    "interaction_only": "interaction",
    "manual_only": "manual",
    "oral_only": "oral",
    "outercourse_only": "outercourse",
    "anal_only": "anal",
    "climax_only": "climax",
    "threesome_only": "threesome",
    "group_only": "group",
}


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 _list_from(value: Any) -> list[Any]:
    if value is None:
        return []
    if isinstance(value, list):
        return value
    return [value]


def normalize_restore_prompt_axes(values: Any) -> list[str]:
    allowed = set(RESTORE_PROMPT_AXIS_CHOICES)
    normalized: list[str] = []
    for value in _list_from(values):
        text = str(value or "").strip()
        if text in allowed and text not in normalized:
            normalized.append(text)
    return normalized


def _entry_text(item: Any) -> str:
    if isinstance(item, dict):
        return str(
            item.get("template")
            or item.get("prompt")
            or item.get("text")
            or item.get("description")
            or item.get("name")
            or ""
        ).strip()
    return str(item).strip()


def _metadata_tokens(item: Any, keys: tuple[str, ...]) -> set[str]:
    if not isinstance(item, dict):
        return set()
    tokens: set[str] = set()
    for key in keys:
        for value in _list_from(item.get(key)):
            token = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
            if token and token != "any":
                tokens.add(token)
    return tokens


def _entry_position_keys(item: Any) -> list[str]:
    if not isinstance(item, dict):
        return []
    values: list[Any] = []
    if item.get("position_keys") is not None:
        values.extend(_list_from(item.get("position_keys")))
    if item.get("position_key") is not None:
        values.append(item.get("position_key"))
    return normalize_hardcore_position_values(values)


def hardcore_position_family_choices() -> list[str]:
    return list(HARDCORE_POSITION_FAMILY_CHOICES)


def hardcore_position_focus_choices() -> list[str]:
    return list(HARDCORE_POSITION_FOCUS_CHOICES)


def hardcore_position_key_choices() -> list[str]:
    return list(HARDCORE_POSITION_KEY_CHOICES)


def normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
    text = re.sub(r"[^a-z0-9]+", "_", str(value or default).strip().lower()).strip("_")
    aliases = {
        "penetration": "penetrative",
        "penetrative_sex": "penetrative",
        "penetration_sex": "penetrative",
        "vaginal": "penetrative",
        "vaginal_penetration": "penetrative",
        "foreplay_teasing": "foreplay",
        "body_worship": "interaction",
        "body_worship_touching": "interaction",
        "clothing_position_transitions": "interaction",
        "dominant_guidance": "interaction",
        "camera_performance": "interaction",
        "group_coordination": "interaction",
        "aftercare_cleanup": "interaction",
        "manual_stimulation": "manual",
        "oral_sex": "oral",
        "outer_course": "outercourse",
        "outercourse_sex": "outercourse",
        "anal_double_penetration": "anal",
        "three_some": "threesome",
        "threesomes": "threesome",
        "group_sex": "group",
        "group_sex_orgy": "group",
        "orgy": "group",
        "cumshot": "climax",
        "cumshot_climax": "climax",
        "orgasm_aftermath": "climax",
    }
    text = aliases.get(text, text)
    return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default


def normalize_hardcore_position_values(values: Any) -> list[str]:
    raw_values = _list_from(values)
    selected: list[str] = []
    for value in raw_values:
        text = str(value or "").strip()
        if not text or text == "any":
            continue
        normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
        if normalized in HARDCORE_POSITION_KEY_CHOICES and normalized not in selected:
            selected.append(normalized)
    return selected


def empty_hardcore_position_config() -> dict[str, Any]:
    return {
        "config_type": "hardcore_position",
        "enabled": False,
        "family": "any",
        "positions": [],
        "require_position": False,
        "allow_toys": True,
        "allow_double": True,
        "allow_penetration": True,
        "allow_foreplay": True,
        "allow_interaction": True,
        "allow_manual": True,
        "allow_oral": True,
        "allow_outercourse": True,
        "allow_anal": True,
        "allow_climax": True,
        "restore_prompt_axes": [],
        "relax_non_pose_axis_conflicts": False,
    }


def parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
    if not value:
        return empty_hardcore_position_config()
    if isinstance(value, dict):
        raw = value
    else:
        try:
            raw = json.loads(str(value))
        except json.JSONDecodeError:
            return empty_hardcore_position_config()
    if not isinstance(raw, dict):
        return empty_hardcore_position_config()
    parsed = {**empty_hardcore_position_config(), **raw}
    parsed["enabled"] = bool(parsed.get("enabled", True))
    parsed["family"] = normalize_hardcore_position_family(parsed.get("family"))
    parsed["positions"] = normalize_hardcore_position_values(parsed.get("positions"))
    parsed["require_position"] = not _is_false(parsed.get("require_position", False))
    for key in (
        "allow_toys",
        "allow_double",
        "allow_penetration",
        "allow_foreplay",
        "allow_interaction",
        "allow_manual",
        "allow_oral",
        "allow_outercourse",
        "allow_anal",
        "allow_climax",
    ):
        parsed[key] = not _is_false(parsed.get(key, True))
    parsed["restore_prompt_axes"] = normalize_restore_prompt_axes(parsed.get("restore_prompt_axes"))
    parsed["relax_non_pose_axis_conflicts"] = not _is_false(parsed.get("relax_non_pose_axis_conflicts", False))
    return parsed


def hardcore_position_summary(config: dict[str, Any]) -> str:
    if not config.get("enabled"):
        return "hardcore position unrestricted"
    parts = [f"family={config.get('family', 'any')}"]
    positions = config.get("positions") or []
    if positions:
        parts.append("positions=" + ",".join(positions))
    elif config.get("require_position"):
        parts.append("position_templates=required")
    disabled = [
        label
        for key, label in (
            ("allow_toys", "toys"),
            ("allow_double", "double"),
            ("allow_penetration", "penetration"),
            ("allow_foreplay", "foreplay"),
            ("allow_interaction", "interaction"),
            ("allow_manual", "manual"),
            ("allow_oral", "oral"),
            ("allow_outercourse", "outercourse"),
            ("allow_anal", "anal"),
            ("allow_climax", "climax"),
        )
        if not config.get(key, True)
    ]
    if disabled:
        parts.append("blocked=" + ",".join(disabled))
    restore_axes = normalize_restore_prompt_axes(config.get("restore_prompt_axes"))
    if restore_axes:
        parts.append("restore_axes=" + ",".join(restore_axes))
    if restore_axes and config.get("relax_non_pose_axis_conflicts"):
        parts.append("relaxed_non_pose_conflicts")
    return "; ".join(parts)


def build_hardcore_position_pool_json(
    hardcore_position_config: str | dict[str, Any] | None = "",
    combine_mode: str = "replace",
    family: str = "any",
    selected_positions: list[str] | tuple[str, ...] | str | None = None,
) -> str:
    base = parse_hardcore_position_config(hardcore_position_config)
    if combine_mode == "replace":
        restore_axes = normalize_restore_prompt_axes(base.get("restore_prompt_axes"))
        relax_non_pose_axis_conflicts = bool(base.get("relax_non_pose_axis_conflicts"))
        base = {**empty_hardcore_position_config(), "enabled": True}
        base["restore_prompt_axes"] = restore_axes
        base["relax_non_pose_axis_conflicts"] = relax_non_pose_axis_conflicts
    else:
        base["enabled"] = True
    base["family"] = normalize_hardcore_position_family(family, base.get("family", "any"))
    selected = normalize_hardcore_position_values(selected_positions)
    if combine_mode == "add":
        existing = list(base.get("positions") or [])
        for value in selected:
            if value not in existing:
                existing.append(value)
        base["positions"] = existing
    else:
        base["positions"] = selected
    base["require_position"] = bool(base.get("require_position")) or bool(base["positions"]) or base["family"] != "any"
    base["summary"] = hardcore_position_summary(base)
    return json.dumps(base, ensure_ascii=True, sort_keys=True)


def build_hardcore_action_filter_json(
    hardcore_position_config: str | dict[str, Any] | None = "",
    focus: str = "keep_pool",
    allow_toys: bool = False,
    allow_double: bool = False,
    allow_penetration: bool = True,
    allow_foreplay: bool = True,
    allow_interaction: bool = True,
    allow_manual: bool = True,
    allow_oral: bool = True,
    allow_outercourse: bool = True,
    allow_anal: bool = True,
    allow_climax: bool = True,
) -> str:
    config = parse_hardcore_position_config(hardcore_position_config)
    config["enabled"] = True
    focus = str(focus or "keep_pool").strip()
    focus_family = FOCUS_FAMILY_BY_KEY.get(focus)
    if focus_family:
        config["family"] = focus_family
    config["allow_toys"] = bool(allow_toys)
    config["allow_double"] = bool(allow_double)
    config["allow_penetration"] = bool(allow_penetration)
    config["allow_foreplay"] = bool(allow_foreplay)
    config["allow_interaction"] = bool(allow_interaction)
    config["allow_manual"] = bool(allow_manual)
    config["allow_oral"] = bool(allow_oral)
    config["allow_outercourse"] = bool(allow_outercourse)
    config["allow_anal"] = bool(allow_anal)
    config["allow_climax"] = bool(allow_climax)

    if not focus_family and config["family"] != "any":
        enabled_action_families = {
            family
            for enabled, family in (
                (config["allow_penetration"], "penetrative"),
                (config["allow_foreplay"], "foreplay"),
                (config["allow_interaction"], "interaction"),
                (config["allow_manual"], "manual"),
                (config["allow_oral"], "oral"),
                (config["allow_outercourse"], "outercourse"),
                (config["allow_anal"], "anal"),
                (config["allow_climax"], "climax"),
            )
            if enabled
        }
        if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
            config["family"] = "any"

    if focus == "foreplay_only":
        config["allow_foreplay"] = True
        config["allow_interaction"] = True
    elif focus == "interaction_only":
        config["allow_interaction"] = True
        config["allow_foreplay"] = True
    elif focus == "manual_only":
        config["allow_manual"] = True
    elif focus == "oral_only":
        config["allow_oral"] = True
        config["allow_penetration"] = False
    elif focus == "outercourse_only":
        config["allow_outercourse"] = True
        config["allow_oral"] = False
        config["allow_penetration"] = False
    elif focus == "anal_only":
        config["allow_anal"] = True
        config["allow_penetration"] = True
    elif focus == "climax_only":
        config["allow_climax"] = True
    config["summary"] = hardcore_position_summary(config)
    return json.dumps(config, ensure_ascii=True, sort_keys=True)


def hardcore_position_config_active(config: dict[str, Any]) -> bool:
    return bool(config.get("enabled"))


def hardcore_position_template_required(config: dict[str, Any]) -> bool:
    if not hardcore_position_config_active(config):
        return False
    return (
        bool(config.get("require_position"))
        or bool(config.get("positions"))
        or normalize_hardcore_position_family(config.get("family")) != "any"
    )


def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
    family = normalize_hardcore_position_family(config.get("family"))
    base_allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
    allowed = set(base_allowed)
    if not config.get("allow_penetration", True):
        allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
    if not config.get("allow_foreplay", True):
        allowed.discard("foreplay_teasing")
    if not config.get("allow_interaction", True):
        allowed.difference_update(
            {
                "foreplay_teasing",
                "body_worship_touching",
                "clothing_position_transitions",
                "dominant_guidance",
                "camera_performance",
                "group_coordination",
                "aftercare_cleanup",
            }
        )
    if not config.get("allow_manual", True):
        allowed.discard("manual_stimulation")
    if not config.get("allow_oral", True):
        allowed.discard("oral_sex")
    if not config.get("allow_outercourse", True):
        allowed.discard("outercourse_sex")
    if not config.get("allow_anal", True):
        allowed.discard("anal_double_penetration")
    if not config.get("allow_climax", True):
        allowed.discard("cumshot_climax")
    if not config.get("allow_double", True) and family == "anal":
        allowed.add("anal_double_penetration")
    if allowed:
        return allowed
    if family != "any":
        return base_allowed
    return set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])


def is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
    return (
        str(category.get("slug") or "").strip() == "hardcore_sexual_poses"
        or str(category.get("name") or "").strip().lower() == "hardcore sexual poses"
    )


def hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str, Any]) -> bool:
    text = str(text or "").lower()
    axis_name = str(axis_name or "").lower()
    if not config.get("allow_toys", True) and any(term in text for term in ("toy", "dildo", "strap-on", "strap on")):
        return True
    if not config.get("allow_double", True) and (
        axis_name == "double_act"
        or any(term in text for term in ("double penetration", "double-penetration", "front-and-back", "front and back", "second penetration", "both sides", "two partners penetrating", "multiple penetrations"))
    ):
        return True
    if not config.get("allow_anal", True) and (
        axis_name == "anal_act"
        or any(term in text for term in (" anal", "anal sex", "anal penetration", "anus", "rear-entry anal", "penis entering ass", "thrusts into her ass", "thrusts into his ass"))
    ):
        return True
    if not config.get("allow_oral", True) and (
        axis_name in ("oral_act", "oral_detail")
        or any(term in text for term in ("oral sex", "mouth on genitals", "mouth on pussy", "blowjob", "cunnilingus", "tongue on pussy", "deepthroat", "fellatio"))
    ):
        return True
    if not config.get("allow_outercourse", True) and (
        axis_name in ("outer_act", "contact_detail", "texture_detail")
        or any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex", "testicle", "balls", "penis licking", "penis-licking", "footjob", "soles", "toes"))
    ):
        return True
    if not config.get("allow_penetration", True) and (
        axis_name in ("penetration_act", "penetration_detail", "anal_act", "double_act", "thrust_detail")
        or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex"))
    ):
        return True
    if not config.get("allow_foreplay", True) and (
        axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail")
        or any(
            term in text
            for term in (
                "kiss",
                "kissing",
                "mouth-to-mouth",
                "caress",
                "caressing",
                "stroking skin",
                "hands roaming",
                "touching breasts",
                "cupping breasts",
                "hand on the cheek",
                "fingers under the chin",
                "undressing",
                "removing clothing",
                "removing clothes",
                "pulling clothing",
                "sliding straps",
                "unbuttoning",
            )
        )
    ):
        return True
    if not config.get("allow_interaction", True) and (
        axis_name
        in (
            "tease_act",
            "touch_detail",
            "clothing_detail",
            "foreplay_detail",
            "face_detail",
            "body_contact",
            "mood_detail",
            "worship_act",
            "transition_act",
            "control_act",
            "performance_act",
            "coordination_act",
            "aftercare_act",
            "cleanup_detail",
        )
        or any(
            term in text
            for term in (
                "kiss",
                "kissing",
                "caress",
                "body worship",
                "nipple",
                "ass grab",
                "thigh",
                "hair holding",
                "wrists",
                "dirty talk",
                "whispering",
                "undressing",
                "position transition",
                "guided",
                "camera",
                "watching",
                "aftercare",
                "cleanup",
                "wiping",
            )
        )
    ):
        return True
    if not config.get("allow_manual", True) and (
        axis_name in ("manual_act", "manual_detail")
        or any(
            term in text
            for term in (
                "fingering",
                "fingers inside",
                "clit",
                "clitoris",
                "manual stimulation",
                "mutual masturbation",
                "masturbating together",
                "fingers on pussy",
                "fingers on clit",
            )
        )
    ):
        return True
    if not config.get("allow_climax", True) and (
        axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
        or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
    ):
        return True
    return False


def hardcore_entry_blocked_by_action(entry: Any, axis_name: str, config: dict[str, Any]) -> bool:
    action_tokens = _metadata_tokens(entry, ("action_family", "action_type"))
    family_tokens = _metadata_tokens(entry, ("position_family", "family"))
    position_keys = set(_entry_position_keys(entry))
    route_tokens = action_tokens | family_tokens

    if not config.get("allow_toys", True) and action_tokens & {"toy", "toy_double"}:
        return True
    if not config.get("allow_double", True) and (action_tokens & {"double", "toy_double"} or "front_back" in position_keys):
        return True
    if not config.get("allow_anal", True) and "anal" in route_tokens:
        return True
    if not config.get("allow_oral", True) and "oral" in route_tokens:
        return True
    if not config.get("allow_outercourse", True) and "outercourse" in route_tokens:
        return True
    if not config.get("allow_penetration", True) and route_tokens & {"penetration", "penetrative", "toy_double", "anal"}:
        return True
    if not config.get("allow_foreplay", True) and "foreplay" in route_tokens:
        return True
    if not config.get("allow_interaction", True) and "interaction" in route_tokens:
        return True
    if not config.get("allow_manual", True) and "manual" in route_tokens:
        return True
    if not config.get("allow_climax", True) and "climax" in route_tokens:
        return True
    return hardcore_text_blocked_by_action(_entry_text(entry), axis_name, config)


def hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
    positions = config.get("positions") or []
    if not positions:
        return True
    metadata_keys = _entry_position_keys(entry)
    if metadata_keys:
        return bool(set(metadata_keys) & set(positions))
    text = _entry_text(entry).lower()
    for position in positions:
        if _text_matches_position_key(text, position):
            return True
    return False


def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> bool:
    selected = set(config.get("positions") or [])
    if not selected:
        return False
    metadata_keys = _entry_position_keys(entry)
    if metadata_keys:
        matched = set(metadata_keys)
    else:
        text = _entry_text(entry).lower()
        matched = {
            position
            for position in HARDCORE_POSITION_KEY_MATCHES
            if _text_matches_position_key(text, position)
        }
    return bool(matched) and not bool(matched & selected)


def restored_prompt_axis_relaxes_conflicts(axis_name: str, config: dict[str, Any]) -> bool:
    if str(axis_name or "") in HARDCORE_POSITION_AXIS_KEYS:
        return False
    if not config.get("relax_non_pose_axis_conflicts"):
        return False
    return str(axis_name or "") in set(normalize_restore_prompt_axes(config.get("restore_prompt_axes")))


def hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
    if not hardcore_position_template_required(config):
        return True
    axes = subcategory.get("item_axes")
    if not isinstance(axes, dict):
        return True
    for axis_name, values in axes.items():
        if str(axis_name) in HARDCORE_POSITION_AXIS_KEYS and any(
            hardcore_position_entry_matches(value, config)
            for value in _list_from(values)
        ):
            return True
    return False


def filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, Any]) -> list[Any]:
    if not hardcore_position_config_active(config):
        return values
    filtered = [
        value
        for value in values
        if not hardcore_entry_blocked_by_action(value, axis_name, config)
        and not (
            axis_name not in HARDCORE_POSITION_AXIS_KEYS
            and not restored_prompt_axis_relaxes_conflicts(axis_name, config)
            and hardcore_position_entry_conflicts(value, config)
        )
        and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config))
    ]
    return filtered or values


def filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> list[Any]:
    if not hardcore_position_config_active(config):
        return templates
    filtered: list[Any] = []
    for template in templates:
        text = _entry_text(template)
        fields = {key for _, key, _, _ in Formatter().parse(text) if key}
        has_position_route = bool(fields & HARDCORE_POSITION_AXIS_KEYS) or bool(_entry_position_keys(template))
        blocked = hardcore_position_template_required(config) and not has_position_route
        blocked = blocked or hardcore_entry_blocked_by_action(template, "", config)
        blocked = blocked or any(hardcore_text_blocked_by_action(text, field, config) for field in fields)
        if not blocked:
            filtered.append(template)
    return filtered or templates


def apply_hardcore_position_config_to_subcategory(
    subcategory: dict[str, Any],
    config: dict[str, Any],
) -> dict[str, Any]:
    if not hardcore_position_config_active(config):
        return subcategory
    subcategory_copy = dict(subcategory)
    if "item_templates" in subcategory_copy:
        subcategory_copy["item_templates"] = filter_hardcore_templates(_list_from(subcategory_copy["item_templates"]), config)
    raw_axes = subcategory_copy.get("item_axes")
    if isinstance(raw_axes, dict):
        axes = {}
        for axis_name, values in raw_axes.items():
            axes[axis_name] = filter_hardcore_axis(str(axis_name), _list_from(values), config)
        subcategory_copy["item_axes"] = axes
    subcategory_copy["hardcore_position_config"] = config
    return subcategory_copy


def filter_hardcore_categories_for_position(
    categories: list[dict[str, Any]],
    config: dict[str, Any],
    women_count: int,
    men_count: int,
    compatible_entry: Callable[[dict[str, Any], int, int], bool],
) -> list[dict[str, Any]]:
    if not hardcore_position_config_active(config):
        return categories
    allowed = hardcore_allowed_subcategory_slugs(config)
    filtered_categories: list[dict[str, Any]] = []
    for category in categories:
        if not is_hardcore_sexual_category(category):
            filtered_categories.append(category)
            continue
        category_copy = dict(category)
        subcategories = [
            subcategory
            for subcategory in category.get("subcategories", [])
            if str(subcategory.get("slug") or "") in allowed
            and compatible_entry(subcategory, women_count, men_count)
            and hardcore_subcategory_supports_positions(subcategory, config)
        ]
        if subcategories:
            category_copy["subcategories"] = subcategories
        filtered_categories.append(category_copy)
    return filtered_categories


def hardcore_source_position_family(subcategory: dict[str, Any], config: dict[str, Any] | None = None) -> str:
    slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
    family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
    if family:
        return family
    config_family = normalize_hardcore_position_family((config or {}).get("family"), "")
    return "" if config_family == "any" else config_family


def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
    text = item_axis_policy.context_text(*parts, axis_values=axis_values)
    if not text:
        return []
    keys: list[str] = []
    for key in HARDCORE_POSITION_KEY_MATCHES:
        if _text_matches_position_key(text, key):
            keys.append(key)
    return keys


_normalize_hardcore_position_family = normalize_hardcore_position_family
_normalize_hardcore_position_values = normalize_hardcore_position_values
_empty_hardcore_position_config = empty_hardcore_position_config
_parse_hardcore_position_config = parse_hardcore_position_config
_hardcore_position_summary = hardcore_position_summary
_hardcore_position_config_active = hardcore_position_config_active
_hardcore_position_template_required = hardcore_position_template_required
_hardcore_allowed_subcategory_slugs = hardcore_allowed_subcategory_slugs
_hardcore_source_position_family = hardcore_source_position_family
_hardcore_position_keys = hardcore_position_keys
