from __future__ import annotations

import re
from typing import Any

try:
    from . import krea2_pose_variant_catalog
    from . import outercourse_action_policy as outercourse_policy
    from .krea_action_context import (
        axis_values_text,
        is_climax_text,
        is_oral_text,
        is_outercourse_text,
        is_toy_assisted_double_text,
        position_context_text,
    )
    from .krea_detail import limit_detail_for_density
except ImportError:  # Allows local smoke tests with `python -c`.
    import krea2_pose_variant_catalog
    import outercourse_action_policy as outercourse_policy
    from krea_action_context import (
        axis_values_text,
        is_climax_text,
        is_oral_text,
        is_outercourse_text,
        is_toy_assisted_double_text,
        position_context_text,
    )
    from krea_detail import limit_detail_for_density


SITTING_ORAL_VARIANT = "pov_blowjob_sitting_upright_oral"


def _clean(value: Any) -> str:
    text = "" if value is None else str(value)
    text = text.replace("\n", " ")
    text = re.sub(r"\s+", " ", text).strip()
    text = re.sub(r"\s+([,.;:])", r"\1", text)
    return text


def _list_values(value: Any) -> list[str]:
    if isinstance(value, list):
        return [str(item) for item in value if str(item).strip()]
    if isinstance(value, str) and value.strip():
        return [part.strip().strip("[]'\" ") for part in value.split(",") if part.strip().strip("[]'\" ")]
    return []


def _has_krea2_variant(axis_values: Any, key: str) -> bool:
    if not isinstance(axis_values, dict):
        return False
    return key in _list_values(axis_values.get("krea2_variant_keys"))


def _metadata_text(axis_values: Any, key: str) -> str:
    if not isinstance(axis_values, dict):
        return ""
    return _clean(axis_values.get(key, "")).lower()


def _metadata_values(axis_values: Any, key: str) -> set[str]:
    if not isinstance(axis_values, dict):
        return set()
    return {value for value in _list_values(axis_values.get(key)) if value}


def _variant_matches_route(variant: dict[str, Any], axis_values: Any) -> bool:
    action_family = _metadata_text(axis_values, "action_family")
    if action_family and _clean(variant.get("action_family", "")).lower() != action_family:
        return False
    route_positions = _metadata_values(axis_values, "position_keys")
    route_position = _metadata_text(axis_values, "position_key")
    if route_position:
        route_positions.add(route_position)
    variant_positions = {str(position) for position in variant.get("position_keys", []) if str(position).strip()}
    return not route_positions or not variant_positions or bool(route_positions & variant_positions)


def _selected_krea2_atlas_variant(axis_values: Any) -> dict[str, Any]:
    if not isinstance(axis_values, dict):
        return {}
    keys = _list_values(axis_values.get("krea2_variant_keys"))
    variants = [krea2_pose_variant_catalog.get_variant(key) for key in keys]
    variants = [variant for variant in variants if variant.get("key")]
    if not variants:
        return {}
    if len(variants) == 1:
        return variants[0]
    matching = [variant for variant in variants if _variant_matches_route(variant, axis_values)]
    return matching[0] if len(matching) == 1 else {}


def _unique_texts(values: list[Any]) -> list[str]:
    texts: list[str] = []
    seen: set[str] = set()
    for value in values:
        text = _clean(value).strip(" .;")
        lower = text.lower()
        if not text or lower in seen:
            continue
        texts.append(text)
        seen.add(lower)
    return texts


def _restored_visible_details(axis_values: dict[str, Any]) -> list[str]:
    restored_details = _list_values(axis_values.get("restored_prompt_details"))
    restored_axes = _list_values(axis_values.get("restored_prompt_axes"))
    if restored_axes and len(restored_axes) == len(restored_details):
        return _unique_texts(
            [
                detail
                for axis, detail in zip(restored_axes, restored_details)
                if axis != "clothing_detail"
            ]
        )

    clothing_detail = _clean(axis_values.get("clothing_detail")).lower()
    return _unique_texts(
        [
            detail
            for detail in restored_details
            if not clothing_detail or _clean(detail).lower() != clothing_detail
        ]
    )


def _krea2_atlas_variant_sentence(axis_values: Any) -> str:
    variant = _selected_krea2_atlas_variant(axis_values)
    if not variant:
        return ""
    cue_sets = krea2_pose_variant_catalog.prompt_cue_sets(variant)
    selected_index = 0
    if isinstance(axis_values, dict):
        indices = axis_values.get("krea2_prompt_variant_indices")
        if isinstance(indices, dict):
            try:
                selected_index = int(indices.get(str(variant.get("key") or ""), 0))
            except (TypeError, ValueError):
                selected_index = 0
    if selected_index < 0 or selected_index >= len(cue_sets):
        selected_index = 0
    cues = _unique_texts(cue_sets[selected_index] if cue_sets else [variant.get("canonical_geometry")])
    sentence = _clean(". ".join(cues)).rstrip(".")
    if isinstance(axis_values, dict):
        restored_details = _restored_visible_details(axis_values)
        if restored_details:
            sentence = f"{sentence}. Additional visible detail: {'; '.join(restored_details)}"
    return sentence


def pov_ejaculation_target(context: str) -> str:
    if any(
        token in context
        for token in (
            "lower back",
            "ass",
            "rear-entry",
            "rear entry",
            "face-down",
            "face down",
            "bent-over",
            "bent over",
            "doggy",
            "on all fours",
            "hips high",
            "hips raised",
            "raised ass",
            "behind her",
        )
    ):
        return "across her ass, thighs, and lower back"
    if any(token in context for token in ("pussy", "open thighs", "thighs", "legs open")):
        return "across her pussy and thighs"
    if any(token in context for token in ("face", "mouth", "lips", "tongue", "chin")):
        return "onto her face and chest"
    return "onto her body"


def pov_contact_clause(
    action: Any,
    role_graph: Any,
    hard_item: Any,
    axis_values: Any,
    context: str,
) -> str:
    is_climax = is_climax_text(action, role_graph, hard_item, axis_values_text(axis_values))
    if is_climax:
        return f"as he ejaculates semen {pov_ejaculation_target(context)}"
    is_anal = any(
        token in context
        for token in (
            "anal",
            "into her ass",
            "penis entering ass",
            "ass stretched",
            "thrusts into her ass",
        )
    )
    contact = "as his penis penetrates her ass" if is_anal else "as his penis penetrates her pussy"
    if is_toy_assisted_double_text(action, role_graph, hard_item, axis_values_text(axis_values)):
        contact = f"{contact} while a toy is positioned at the second penetration point"
    return contact


def _is_open_thigh_aftermath_context(context: str, action_lower: str, position_context: str) -> bool:
    combined = f"{context} {action_lower} {position_context}"
    has_open_thighs = any(
        token in combined
        for token in (
            "open thighs",
            "thighs open",
            "legs open",
            "legs spread",
            "reclining with thighs open",
        )
    )
    has_aftermath = any(
        token in combined
        for token in (
            "post-ejaculation",
            "after ejaculation",
            "aftermath",
            "semen",
            "visible fluid",
            "thick fluid",
            "clear fluid",
        )
    )
    has_rear_entry = any(
        token in combined
        for token in (
            "rear-entry",
            "rear entry",
            "doggy",
            "on all fours",
            "face-down",
            "face down",
            "bent-over",
            "bent over",
            "behind her",
            "lower back",
        )
    )
    return has_open_thighs and has_aftermath and not has_rear_entry


def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
    detail = _clean(detail).strip(" .;")
    if not detail:
        return ""
    detail = re.sub(r"\bthe POV viewer\b", "the viewer", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\bthe man's\b", "the viewer's", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\bthe man\b", "the viewer", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\bhe\b", "the viewer", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\bhis\b", "the viewer's", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\bhim\b", "the viewer", detail, flags=re.IGNORECASE)
    detail = re.sub(
        r"^(?:folded missionary|missionary|low cowgirl seated-squat|low cowgirl|cowgirl-alt|cowgirl|reverse cowgirl|doggy style|standing sex|spooning sex|edge-supported|edge-of-bed|raised edge|kneeling straddle|lotus sex|bent-over|face-down ass-up|side-lying|kneeling rear-entry)\s+(?:position|pose)\s+(?:featuring|with|while|,)?\s*",
        "",
        detail,
        flags=re.IGNORECASE,
    )
    detail = re.sub(
        r"^(?:kneeling oral|standing oral|chair oral|side-lying oral|sixty-nine|edge-supported oral|edge-of-bed oral|reclining cunnilingus|straddled oral|spread-leg oral)\s+(?:position|pose),?\s*",
        "",
        detail,
        flags=re.IGNORECASE,
    )
    detail = re.sub(r"^(?:featuring|with)\s+", "", detail, flags=re.IGNORECASE)
    detail = re.sub(
        r"^(?:full-body|explicit|close-contact|deep|hardcore|vaginal|anal)?\s*(?:penetrative sex|vaginal sex|anal sex|penetration with visible genital contact|hardcore vaginal thrusting|hardcore anal thrusting),?\s*",
        "",
        detail,
        flags=re.IGNORECASE,
    )
    detail = re.sub(
        r"\b(?:front-facing|close-up|wide full-body|wide|overhead|mirror-reflected|low-angle|side-profile|bed-level)\s+view of\b",
        "visible",
        detail,
        flags=re.IGNORECASE,
    )
    detail = re.sub(
        r",?\s*\bthe viewer is behind her at hip level with (?:his|the viewer's) hands on her hips in the foreground as (?:his|the viewer's) penis (?:thrusts into her|penetrates her pussy)\b",
        "",
        detail,
        flags=re.IGNORECASE,
    )
    detail = re.sub(
        r",?\s*\bthe woman is on all fours directly in front of the viewer with hips raised and back arched\b",
        "",
        detail,
        flags=re.IGNORECASE,
    )
    if any(token in context for token in ("ass raised", "on all fours", "doggy", "rear-entry", "bent-over", "face-down")):
        detail = re.sub(
            r",?\s*\b(?:one body pinned under another|bodies stacked close together|bodies tangled on the sheets)\b",
            "",
            detail,
            flags=re.IGNORECASE,
        )
    if "toy is positioned at the second penetration point" in context:
        detail = re.sub(
            r",?\s*\b(?:toy aligned for a second penetration point|toy-assisted second contact aligned behind the body)\b",
            "",
            detail,
            flags=re.IGNORECASE,
        )
    detail = re.sub(r"\bwith with\b", "with", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\s*,\s*", ", ", detail)
    detail = re.sub(r",\s*,", ",", detail).strip(" ,;")
    return limit_detail_for_density(detail, detail_density, is_climax_text(context, detail))


def pov_clean_oral_detail(detail: Any, context: str, detail_density: str) -> str:
    detail = pov_clean_detail(detail, context, detail_density)
    if not detail:
        return ""
    duplicate_patterns = (
        r"\bthe woman takes the viewer's penis in her mouth with\s+",
        r"\bthe woman takes the viewer's penis in her mouth\b,?\s*",
        r"\bher mouth on the viewer's penis\b,?\s*",
        r"\bthe viewer's mouth on the woman's pussy\b,?\s*",
        r"\bmouth on the viewer's penis\b,?\s*",
        r"\bmouth on the woman's pussy\b,?\s*",
    )
    for pattern in duplicate_patterns:
        detail = re.sub(pattern, "", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\bwith\s+(?=[,;.]|$)", "", detail, flags=re.IGNORECASE)
    detail = re.sub(r"\s*,\s*", ", ", detail)
    detail = re.sub(r",\s*,", ",", detail)
    return _clean(detail).strip(" ,;")


def pov_hardcore_pose_sentence(
    action: Any,
    role_graph: Any,
    hard_item: Any,
    composition: Any = "",
    axis_values: Any = None,
    detail_density: str = "balanced",
) -> str:
    context = position_context_text(role_graph, hard_item, composition, axis_values)
    action_text = _clean(action)
    action_lower = action_text.lower()
    if not context:
        context = action_lower
    position_text = ""
    if isinstance(axis_values, dict):
        position_text = _clean(axis_values.get("position", "")).lower()
    position_context = position_text or context

    def sentence(base: str) -> str:
        details = ""
        if ";" in action_text:
            details = pov_clean_detail(action_text.split(";", 1)[1], f"{context} {base}", detail_density)
        return f"{base}; {details}" if details else base

    def outercourse_sentence(base: str) -> str:
        return _clean(base).rstrip(".")

    def oral_sentence(base: str) -> str:
        details = ""
        if ";" in action_text:
            details = pov_clean_oral_detail(action_text.split(";", 1)[1], f"{context} {base}", detail_density)
        return _clean(f"{base}; {details}" if details else base).rstrip(".")

    atlas_sentence = _krea2_atlas_variant_sentence(axis_values)
    if atlas_sentence:
        return atlas_sentence

    def oral_direction() -> tuple[bool, bool]:
        oral_context = f"{context} {action_lower}"
        woman_gives = any(
            token in oral_context
            for token in (
                "fellatio",
                "blowjob",
                "deepthroat",
                "penis sucking",
                "penis in her mouth",
                "mouth on the viewer's penis",
                "mouth on viewer's penis",
                "takes the viewer's penis",
                "takes the man's penis",
                "mouth at penis level",
                "lips wrapped",
            )
        )
        man_gives = any(
            token in oral_context
            for token in (
                "cunnilingus",
                "pussy licking",
                "mouth on the woman's pussy",
                "mouth on her pussy",
                "mouth pressed to her pussy",
                "tongue on pussy",
                "face-sitting",
                "straddles his face",
                "straddling the viewer's face",
            )
        )
        if "sixty-nine" in oral_context:
            return True, True
        return woman_gives, man_gives

    penetrative_tokens = (
        "penetrat",
        "thrust",
        "anal",
        "cowgirl",
        "missionary",
        "knees-to-chest",
        "knees to chest",
        "doggy",
        "rear-entry",
        "spooning",
        "bent-over",
        "face-down",
        "ejaculat",
        "semen",
        "cumshot",
        "climax",
    )
    has_penetrative_context = any(token in context or token in action_lower for token in penetrative_tokens)
    toy_contact_context = f"{context} {action_lower}"
    if (
        any(token in toy_contact_context for token in ("wand-style", "wand toy", "wand-toy", "vibrator", "massager"))
        and any(token in toy_contact_context for token in ("clit", "vulva", "toy-contact", "toy contact"))
        and not has_penetrative_context
    ):
        return outercourse_sentence(
            "Close first-person POV wand-toy contact: the woman reclines with thighs spread wide toward the camera; "
            "a single continuous teal wand-style massager is the largest lower-frame object, "
            "the rounded bulb head presses flat to her vulva and clit as the central contact point, "
            "and the smooth handle angles in from the bottom right inside the viewer's visible hand; "
            "her open thighs and knees form a V around the foreground wand while her face and torso remain visible behind the leg frame"
        )

    if (
        "face-sitting" in context
        or "face sitting" in context
        or ("straddles" in context and "face" in context and "pussy" in context)
    ):
        return outercourse_sentence(
            "The woman is above the camera in a close first-person underview, straddling the viewer's face with her thighs on both sides of his head; "
            "her pussy is directly over the viewer's mouth in the lower foreground, tongue contact visible from below"
        )

    if is_outercourse_text(context, action_lower):
        action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
        if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
            action_kind = outercourse_policy.infer_outercourse_action_kind(context, action_lower)
        if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
            return outercourse_sentence(
                "POV boobjob position: the viewer reclines with thighs open while the woman kneels upright between his legs facing him; "
                "the viewer's penis rises vertically in the lower foreground and is squeezed between her pressed-together breasts; "
                "the woman's own fingers and nails cup her breasts from the outside and push soft breast tissue inward around the shaft, "
                "with the glans emerging above the cleavage directly below her mouth"
            )
        if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
            return outercourse_sentence(
                "Low side-pelvis POV: the woman lies low beside the viewer's open thighs with her cheek against the viewer's inner thigh; "
                "her face is the closest visible partner part and her head stays low under the viewer's pelvis, with the viewer's scrotum at her mouth; scrotum is the mouth surface, "
                "testicles resting across her open lips while her tongue cups them from below, scrotal skin is the nearest mouth surface and both testicles rest against her tongue from below, "
                "and the viewer's abdomen and inner thighs framing the close foreground"
            )
        if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
            prone_laying = any(
                term in position_context
                for term in ("reclining", "prone", "belly-down", "belly down", "lying")
            )
            if prone_laying:
                return outercourse_sentence(
                    "POV prone frontal oral position: the viewer reclines with open thighs forming a wide symmetrical V-frame from the lower corners toward the center; "
                    "the woman lies belly-down between his thighs with her torso stretched low and horizontal, hips and legs trailing away behind her along the center line; "
                    "her front-facing mouth and tongue align to the shaft rising from the exact lower center, hands wrap the base, and the centered mouth-to-shaft contact stays framed by his thighs"
                )
            return outercourse_sentence(
                "The woman bends forward between the viewer's open thighs with her head low under the viewer's penis; "
                "her face is just under the penis while her tongue touches the underside from the base toward the glans at the tip, "
                "one hand steadying the base of the viewer's penis in the lower foreground"
            )
        if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
            return outercourse_sentence(
                "POV handjob position: the viewer reclines with thighs open while the woman kneels between his legs facing him, "
                "torso leaning forward and face visible behind the penis; "
                "the woman's right hand wraps around the viewer's penis in the lower foreground and strokes along the shaft toward the glans, "
                "while her left hand steadies the base with her fingers and nails visibly gripping the penis; "
                "the viewer's thighs and pelvis frame the lower edges without his hands covering the action"
            )
        if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
            return outercourse_sentence(
                "Frontal POV footjob close-up: the woman faces the viewer with hips back, torso behind raised legs, and knees bent open toward the camera; "
                "two large overlapping soles dominate the lower center foreground and clamp the upright shaft between them, inner arches press inward from both sides, "
                "toes curl around both edges, a narrow visible strip of shaft and glans rises between the compressed feet, and her face and torso stay visible behind the large foreground feet"
            )
        return outercourse_sentence(
            "The woman stays close to the viewer's pelvis, keeping the non-penetrative contact centered in the lower foreground with her face visible behind the contact"
        )

    if is_oral_text(context, action_lower) and not has_penetrative_context:
        woman_gives, man_gives = oral_direction()
        if _has_krea2_variant(axis_values, SITTING_ORAL_VARIANT):
            return oral_sentence(
                "POV upright sitting oral position: the viewer reclines with open thighs forming the lower V-frame and his lower abdomen anchoring the near edge; "
                "the woman sits low between his open thighs with hips between his knees, torso upright behind the action, shoulders square to the camera, and face lowered close to the exact center contact point; "
                "the vertical shaft rises from the exact lower center between the viewer thighs, her open mouth covers the tip at the centerline, lips wrapped around the glans, and mouth-to-shaft contact is the nearest facial detail; "
                "both hands stay low at the base directly below her mouth, fingers wrapped around the shaft, while her eyes, face, shoulders, torso, hands, shaft, and the viewer thigh frame remain readable in one first-person seated frame"
            )
        if "sixty-nine" in position_context:
            return oral_sentence(
                "POV sixty-nine oral position: the woman lies head-to-hips over the viewer, her pelvis close to his face and her head lowered toward his hips; "
                "her mouth on the viewer's penis and the viewer's mouth on the woman's pussy, with her torso, hips, mouth, and the viewer's lower-foreground body cues aligned in one first-person frame"
            )
        if woman_gives and not man_gives and any(
            term in position_context
            for term in (
                "upright sitting oral",
                "sitting oral",
                "seated oral",
                "blowjob_sitting",
            )
        ):
            return oral_sentence(
                "POV upright sitting oral position: the viewer reclines with open thighs forming the lower V-frame and his lower abdomen anchoring the near edge; "
                "the woman sits low between his open thighs with hips between his knees, torso upright behind the action, shoulders square to the camera, and face lowered close to the exact center contact point; "
                "the vertical shaft rises from the exact lower center between the viewer thighs, her open mouth covers the tip at the centerline, lips wrapped around the glans, and mouth-to-shaft contact is the nearest facial detail; "
                "both hands stay low at the base directly below her mouth, fingers wrapped around the shaft, while her eyes, face, shoulders, torso, hands, shaft, and the viewer thigh frame remain readable in one first-person seated frame"
            )
        if "side-lying oral" in position_context or "side lying oral" in position_context:
            if woman_gives and not man_gives:
                return oral_sentence(
                    "POV side-profile oral body-line position: the male viewer's abdomen, navel, pelvis, and near thigh create a broad horizontal body surface across the lower frame; "
                    "the adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground, with navel, abdomen hair, pelvis, and near thigh marking the camera owner's body; "
                    "the woman enters laterally from the left edge beside his hip, cheek and jaw in profile, mouth on the shaft at the male abdomen line, "
                    "lips touching the shaft at the male abdomen line, mouth-to-shaft contact is the nearest facial detail, "
                    "hand around the base under her lips, shoulder and torso trailing sideways along the edge"
                )
            return oral_sentence(
                "POV side-lying cunnilingus position: the woman lies on her side with her top thigh lifted while the viewer lies beside her hips; "
                "his face is at pussy height, with her thigh, hip, and torso forming a clear side-profile first-person frame"
            )
        if (
            "edge-supported oral" in position_context
            or "edge-of-bed oral" in position_context
            or "edge of bed oral" in position_context
            or "raised edge" in position_context
        ):
            if woman_gives and not man_gives:
                return oral_sentence(
                    "POV raised-edge oral position: the viewer sits at the raised edge with legs apart while the woman kneels directly between his thighs; "
                    "her head is at penis height, mouth on the viewer's penis, with his thighs framing her shoulders in the lower foreground"
                )
            return oral_sentence(
                "POV raised-edge cunnilingus position: the woman reclines at the raised edge with thighs open toward the viewer; "
                "the viewer kneels between her legs with his face at pussy height, her hips and open thighs framing the first-person view"
            )
        if "chair oral" in position_context:
            if woman_gives and not man_gives:
                return oral_sentence(
                    "POV chair oral position: the viewer sits in a chair with legs apart while the woman kneels between the viewer's thighs; "
                    "her head is low at his pelvis, mouth on the viewer's penis, with chair seat, thighs, and hands anchoring the lower foreground"
                )
            return oral_sentence(
                "POV chair cunnilingus position: the woman sits in the chair with thighs open while the viewer kneels between her legs; "
                "his face is at pussy height and her hips, knees, and chair seat define the first-person geometry"
            )
        if "standing oral" in position_context:
            if man_gives and not woman_gives:
                return oral_sentence(
                    "POV standing cunnilingus position: the woman stands braced with one thigh lifted while the viewer kneels in front of her; "
                    "his face is at pussy height, with her raised thigh, hips, and standing leg clearly framing the view"
                )
            return oral_sentence(
                "POV standing oral position: the viewer stands over her with hips forward while the woman kneels directly in front of him at hip height; "
                "her head is tilted up at penis level, mouth on the viewer's penis, with his thighs and hands in the lower foreground"
            )
        if (
            "reclining cunnilingus" in position_context
            or "spread-leg oral" in position_context
            or "open-thigh" in position_context
            or "open thigh" in position_context
        ):
            if woman_gives and not man_gives:
                return oral_sentence(
                    "POV reclining oral position: the viewer reclines with thighs apart while the woman kneels low between his legs; "
                    "her face stays at penis height with her mouth on the viewer's penis and his thighs framing the first-person view"
                )
            return oral_sentence(
                "POV open-thigh cunnilingus position: the woman reclines on her back with thighs spread toward the viewer; "
                "the viewer kneels between her legs with his face at pussy height, her knees, hips, and torso aligned toward the camera"
            )
        if "straddled oral" in position_context:
            if woman_gives and not man_gives:
                return oral_sentence(
                    "POV straddled oral position: the viewer leans forward near the woman's face while she kneels below his pelvis; "
                    "her mouth stays on the viewer's penis with her head tilted upward and his thighs framing the lower foreground"
                )
            return oral_sentence(
                "POV straddled cunnilingus position: the woman straddles above the viewer's face with her thighs framing his head; "
                "her pussy stays directly over the viewer's mouth in a close first-person oral frame"
            )
        if "kneeling oral" in position_context or "kneeling" in position_context:
            if man_gives and not woman_gives:
                return oral_sentence(
                    "POV kneeling cunnilingus position: the woman kneels with thighs parted and hips angled forward while the viewer kneels in front of her; "
                    "his face is at pussy height, with her knees, hips, and torso readable from the first-person angle"
                )
            return oral_sentence(
                "Nadir-angle standing male POV top-view oral position: the viewer looks almost straight down from his torso toward the floor, with nearby carpet/floor plane dominating the image; "
                "the viewer's abdomen, shorts, thighs, and feet frame the lower foreground, and the viewer's penis shaft appears as a short centered vertical column from the foreground; "
                "the kneeling woman is directly below the viewer between his feet, her face tilts upward beneath the shaft, her mouth seals around it, and one hand wraps the base; "
                "her hair crown, forehead, shoulders, hands, knees, and compact foreshortened torso are visible from above, with desk legs, chair wheels, carpet texture, and floor seams as top-down office anchors around her"
            )
        if man_gives and not woman_gives:
            return oral_sentence(
                "POV cunnilingus position: the woman lies back with thighs open toward the viewer while he kneels between her legs; "
                "his face is at pussy height, with her knees, hips, and torso forming the first-person frame"
            )
        return oral_sentence(
            "POV oral position: the woman kneels close at the viewer's pelvis with her head at penis height; "
            "her mouth is on the viewer's penis, shoulders between his thighs, and the viewer's hands or thighs anchor the lower foreground"
        )

    if not has_penetrative_context:
        return ""
    oral_only = any(token in context for token in ("oral", "blowjob", "cunnilingus", "mouth on", "penis in her mouth"))
    if oral_only and not any(token in context for token in ("penetrat", "thrust", "anal", "ejaculat", "semen", "cumshot", "climax")):
        return ""

    contact = pov_contact_clause(action, role_graph, hard_item, axis_values, context)
    if is_climax_text(action, role_graph, hard_item, axis_values_text(axis_values)) and _is_open_thigh_aftermath_context(
        context,
        action_lower,
        position_context,
    ):
        return sentence(
            "POV post-ejaculation open-thigh display: the woman reclines or sits back facing the viewer with thighs spread open; "
            "the wet aftermath detail is the exact center, thick semen and clear fluid cover the exposed pussy and inner thighs, "
            "her body stays still after ejaculation, and her face and torso remain visible behind the open-thigh frame"
        )

    if "reverse cowgirl alt" in position_context or "upright reverse cowgirl" in position_context or "upright back-facing straddle" in position_context:
        return sentence(
            "POV upright reverse cowgirl back-facing penetration position: the viewer lies on his back while the woman sits upright on his pelvis facing away; "
            "her back stays vertical and readable above her hips, her ass is centered over the viewer's pelvis, "
            f"viewer hands hold her hips, viewer thighs frame the lower corners, and centered contact remains visible below her ass {contact}"
        )
    if "reverse cowgirl" in position_context:
        return sentence(
            "POV reverse cowgirl position: the viewer lies on his back while the woman straddles his hips facing away; "
            "her back, hips, and ass are the nearest largest shapes to the camera; "
            f"the viewer thighs frame the lower corners, and the centered contact sits directly between her thighs below her ass {contact}"
        )
    if "folded missionary" in position_context or "knees-to-chest" in position_context or "knees to chest" in position_context:
        return sentence(
            "POV folded missionary high-leg penetration position: the viewer's lower abdomen anchors the bottom edge with a large centered shaft rising from the lower center; "
            "the woman lies on her back facing the viewer with both knees folded tightly toward her chest into a compact knee block above the contact; "
            f"viewer hands hold her calves, her feet and ankles sit close to the camera, and her face and torso remain visible behind the raised knees {contact}"
        )
    if "cowgirl-alt" in position_context or "low cowgirl" in position_context or "seated-squat cowgirl" in position_context or "low seated squat" in position_context:
        return sentence(
            "POV low cowgirl seated-squat penetration position: the viewer lies flat on his back underneath her, and the lens sits low at the viewer's abdomen looking upward from his pelvis; "
            "the woman faces the viewer in a low squat mounted over his hips with knees bent wide and close to the camera; "
            f"the viewer supports the underside of her thighs, her torso stays close above the centered contact, and the high room background behind her upper body reinforces the low supine viewpoint {contact}"
        )
    if "cowgirl" in position_context or "straddling a partner" in position_context or "squatting on top" in position_context:
        return sentence(
            "POV frontal cowgirl wide-thigh bridge position: the viewer reclines underneath her with lower abdomen and pelvis anchoring the bottom edge; "
            "the woman straddles his hips facing him, her thighs form a wide horizontal thigh bridge from left edge to right edge, "
            f"knees planted outside the viewer's hips, torso upright above the centered contact point, viewer hands grip the sides of her thighs, and centered contact remains below her belly {contact}"
        )
    if "lotus" in position_context or "seated in a partner's lap" in position_context:
        return sentence(
            "POV lotus position: the viewer sits upright while the woman sits in his lap facing him with her legs around his hips; "
            f"her torso and hips stay close to the viewer {contact}"
        )
    if "kneeling straddle" in position_context:
        return sentence(
            "POV kneeling straddle position: the viewer kneels upright while the woman straddles his hips facing him; "
            f"both torsos are upright and her hips press directly against him {contact}"
        )
    if "face-down" in position_context or "face down" in position_context:
        return sentence(
            "The woman is seen from behind with her ass raised toward the POV viewer, lying face-down with hips lifted; "
            f"the viewer looks down at her raised ass with foreground hands on her hips {contact}"
        )
    if (
        "edge-supported" in position_context
        or "raised edge" in position_context
        or "edge of bed" in position_context
        or "bed edge" in position_context
        or (not position_text and "kneels between her legs" in context)
    ):
        if "penetrates her ass" in contact:
            return sentence(
                "POV raised-edge penetration position: the woman reclines at the raised edge with thighs open toward the viewer; "
                f"the viewer kneels between her legs with his hands near her hips {contact}"
            )
        return sentence(
            "POV elevated-edge missionary position: the woman lies flat on her back across a flat elevated support with hair, shoulders, spine, and hips aligned on one horizontal surface; "
            "her legs open toward the viewer at the foot edge, thighs forming a broad U-frame around the centered contact line; "
            f"the viewer stands, kneels, or braces at the foot edge with hands holding her calves or outer thighs and feet, shins, or side-dropping legs placed below the support edge {contact}"
        )
    if "standing" in position_context:
        return sentence(
            "POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; "
            f"the viewer stands behind her at hip level {contact}"
        )
    if "spooning" in position_context or "side-lying" in position_context or "lies on her side" in position_context:
        return sentence(
            "POV side-lying sex position: the woman lies on her side in front of the viewer with thighs parted; "
            f"the viewer is behind her along the same body line {contact}"
        )
    if "doggy" in position_context or "all fours" in position_context or "rear-entry" in position_context:
        return sentence(
            "Top-down POV doggy position from behind: the camera looks down over the viewer's hands onto the woman's raised hips; "
            f"the woman is on all fours with chest low, forearms folded, cheek turned sideways far ahead, back arched, and hips raised high toward the camera; "
            f"the viewer's hands hold her hips with natural lower-body POV cues in the foreground {contact}"
        )
    if "kneeling" in position_context:
        return sentence(
            "POV kneeling rear-entry position: the woman kneels forward in front of the viewer with hips raised and thighs apart; "
            f"the viewer kneels behind her at hip level with foreground hands near her waist {contact}"
        )
    if "bent-over" in position_context or "bent over" in position_context or "bent forward" in position_context:
        return sentence(
            "The woman is seen from behind with her ass raised toward the POV viewer, bent forward at the waist with hips lifted and head turned back; "
            f"the viewer looks down at her raised ass from behind with foreground hands near her hips {contact}"
        )
    if "missionary" in position_context or (not position_text and "lies on her back" in context and ("legs open" in context or "thighs open" in context)):
        return sentence(
            "POV missionary position: the woman lies on her back with legs open around the viewer's hips; "
            f"the viewer is above her with foreground arms braced beside her body {contact}"
        )
    return sentence(
        "POV penetrative sex position: the woman is directly in front of the viewer with legs open around his hips; "
        f"the viewer's foreground hands and body position define the first-person angle {contact}"
    )


def pov_action_phrase(
    action: Any,
    pov_labels: list[str],
    role_graph: Any = "",
    hard_item: Any = "",
    composition: Any = "",
    axis_values: Any = None,
    detail_density: str = "balanced",
) -> str:
    rendered = _clean(action)
    if not rendered or not pov_labels:
        return rendered
    if "Man A" in pov_labels:
        pov_sentence = pov_hardcore_pose_sentence(
            rendered,
            role_graph,
            hard_item,
            composition,
            axis_values,
            detail_density,
        )
        if pov_sentence:
            return pov_sentence
    for label in sorted(pov_labels, key=len, reverse=True):
        escaped = re.escape(label)
        rendered = re.sub(rf"\b{escaped}'s\b", "the viewer's", rendered)
        rendered = re.sub(rf"\b{escaped}\b", "the viewer", rendered)
    if "Man A" in pov_labels:
        rendered = re.sub(r"\bthe man's\b", "the viewer's", rendered, flags=re.IGNORECASE)
        rendered = re.sub(r"\bthe man\b", "the viewer", rendered, flags=re.IGNORECASE)
        rendered = re.sub(r"\bhe\b", "the viewer", rendered, flags=re.IGNORECASE)
        rendered = re.sub(r"\bhim\b", "the viewer", rendered, flags=re.IGNORECASE)
        rendered = re.sub(r"\bhis\b", "the viewer's", rendered, flags=re.IGNORECASE)
    rendered = re.sub(
        r"\bthe viewer lies on the viewer's back under her\b",
        "the viewer reclines underneath her",
        rendered,
        flags=re.IGNORECASE,
    )
    rendered = re.sub(
        r"\bthe viewer lies on the viewer's back\b",
        "the viewer reclines",
        rendered,
        flags=re.IGNORECASE,
    )
    rendered = re.sub(r"\bthe viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
    return rendered
