from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any, Callable

try:
    from . import formatter_input as input_policy
    from . import formatter_target as target_policy
except ImportError:  # pragma: no cover - plain-script smoke tests
    import formatter_input as input_policy
    import formatter_target as target_policy


@dataclass(frozen=True)
class CaptionMetadataRouteRequest:
    row: dict[str, Any]
    detail_level: str
    keep_style: bool
    target: str = "auto"


@dataclass(frozen=True)
class CaptionMetadataRoute:
    prose: str
    method: str

    def as_tuple(self) -> tuple[str, str]:
        return self.prose, self.method


@dataclass(frozen=True)
class CaptionMetadataRouteDependencies:
    item_labels: tuple[str, ...]
    clean_text: Callable[[Any], str]
    row_value: Callable[[dict[str, Any], str, tuple[str, ...]], str]
    field_row_value: Callable[[dict[str, Any], str], str]
    clean_clothing: Callable[[str], str]
    normalize_composition: Callable[[str], str]
    expression_disabled: Callable[[dict[str, Any]], bool]
    detail_allows: Callable[..., bool]
    join_sentences: Callable[[list[str]], str]
    human_join: Callable[[list[str]], str]
    article: Callable[[str], str]
    cap_first: Callable[[str], str]
    body_phrase: Callable[[Any, Any], str]
    single_caption_front: Callable[[dict[str, Any]], dict[str, str]]
    pose_clause: Callable[[str], str]
    age_subject: Callable[[str, str], str]
    clean_age_phrase: Callable[[str], str]
    subject_phrase_from_counts: Callable[[dict[str, Any]], str]
    verb_for_row: Callable[[dict[str, Any]], str]
    metadata_action_label: Callable[[dict[str, Any]], str]
    item_axis_detail_text: Callable[[dict[str, Any], str], str]
    natural_cast_descriptor_text: Callable[[str], str]
    cast_labels: Callable[[str], list[str]]
    natural_label_text: Callable[[Any, list[str]], str]
    softcore_caption_setup_phrase: Callable[..., str]
    metadata_to_prose: Callable[..., tuple[str, str]]


def pronoun(subject: str) -> str:
    return "She" if subject == "woman" else "He"


def possessive_pronoun(subject: str) -> str:
    return "Her" if subject == "woman" else "His"


def couple_clothing_sentence(clothing: str, clean_text: Callable[[Any], str]) -> str:
    clothing = clean_text(clothing)
    lower = clothing.lower()
    partner_text = re.sub(r"\bPartner ([AB]) wears\b", r"Partner \1 wearing", clothing)
    partner_text = re.sub(r"\bPartner ([AB]) has\b", r"Partner \1 with", partner_text)
    if lower.startswith("partner a "):
        return f"The outfits show {partner_text}"
    if lower.startswith(("two ", "paired ", "coordinated ")):
        return f"The outfits are {partner_text}"
    return f"They wear {clothing}"


def couple_subject_sentence(
    subject: str,
    ages: str,
    cap_first: Callable[[str], str],
    clean_age_phrase: Callable[[str], str],
) -> str:
    subject = cap_first(subject or "adult couple")
    ages = clean_age_phrase(ages)
    if ages:
        return f"{subject}, {ages}"
    if subject.lower() == "adult couple":
        return subject
    return f"{subject} are adults"


def expression_detail(expression: Any, clean_text: Callable[[Any], str]) -> tuple[str, bool]:
    text = clean_text(expression)
    if not text:
        return "", False
    has_character_labels = bool(
        re.search(
            r"\b(?:Woman|Man) [A-Z] has\b|\bthe (?:woman|man) has\b",
            text,
            flags=re.IGNORECASE,
        )
    )
    text = re.sub(
        r"\b((?:Woman|Man) [A-Z]|the (?:woman|man)) has\b",
        r"\1 with",
        text,
        flags=re.IGNORECASE,
    )
    return text, has_character_labels


def single_from_row_result(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> CaptionMetadataRoute | None:
    row = request.row
    detail_level = request.detail_level
    keep_style = request.keep_style
    subject = deps.clean_text(row.get("primary_subject") or row.get("subject") or "")
    if subject not in ("woman", "man"):
        return None

    caption_front = deps.single_caption_front(row)
    age = deps.clean_text(row.get("age") or row.get("age_band") or caption_front.get("caption_age") or "")
    body_phrase = deps.field_row_value(row, "body_phrase") or caption_front.get("caption_body_phrase", "")
    if not body_phrase:
        body = deps.clean_text(row.get("body_type") or row.get("body") or "")
        figure = deps.clean_text(row.get("figure"))
        body_phrase = deps.body_phrase(body, figure)

    skin = deps.field_row_value(row, "skin") or caption_front.get("caption_skin", "")
    hair = deps.field_row_value(row, "hair") or caption_front.get("caption_hair", "")
    eyes = deps.field_row_value(row, "eyes") or caption_front.get("caption_eyes", "")
    item = deps.row_value(row, "item", deps.item_labels)
    if item:
        item = deps.clean_clothing(item)
    if not item:
        item = deps.clean_clothing(deps.row_value(row, "clothing", ("Clothing", "Erotic outfit")))
    scene = deps.row_value(row, "scene_text", ("Scene", "Setting"))
    pose = deps.row_value(row, "pose", ("Pose",))
    expression = "" if deps.expression_disabled(row) else deps.row_value(row, "expression", ("Facial expression", "Facial expressions"))
    composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
    camera_scene = deps.clean_text(row.get("camera_scene_directive"))
    prop = deps.row_value(row, "prop", ("Prop/detail",))
    style = deps.field_row_value(row, "style") if keep_style else ""

    parts = []
    opener = deps.age_subject(age, subject)
    appearance_details = [piece for piece in (skin, hair, eyes) if piece]
    if body_phrase:
        parts.append(f"{opener} has {deps.article(body_phrase)} {body_phrase}")
    elif appearance_details:
        parts.append(f"{opener} has {deps.human_join(appearance_details)}")
    else:
        parts.append(opener)
    if body_phrase and appearance_details:
        parts.append(f"{pronoun(subject)} has {deps.human_join(appearance_details)}")
    if item:
        verb = "wears" if subject == "woman" else "is dressed in"
        parts.append(f"{pronoun(subject)} {verb} {item}")
    if prop:
        parts.append(f"{pronoun(subject)} is {prop}")
    if pose:
        parts.append(f"{pronoun(subject)} is {deps.pose_clause(pose)}")
    if expression:
        expression, labeled_expression = expression_detail(expression, deps.clean_text)
        if labeled_expression:
            parts.append(f"The expression detail shows {expression}")
        else:
            parts.append(f"{possessive_pronoun(subject)} expression is {expression}")
    if scene:
        parts.append(f"The setting is {scene}")
    if deps.detail_allows(detail_level) and camera_scene:
        parts.append(camera_scene)
    if deps.detail_allows(detail_level) and composition:
        parts.append(f"The composition is {composition}")
    if keep_style and style:
        parts.append(f"The visual style is {style}")
    return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(single)")


def couple_from_row_result(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> CaptionMetadataRoute | None:
    row = request.row
    detail_level = request.detail_level
    keep_style = request.keep_style
    subject = deps.clean_text(row.get("subject_phrase") or row.get("primary_subject"))
    primary = deps.clean_text(row.get("primary_subject"))
    if "couple" not in primary and subject not in ("two women", "two men", "a woman and a man"):
        if not primary.startswith("two ") and " and " not in subject:
            return None
    if subject == "woman and man":
        subject = "a woman and a man"

    ages = deps.row_value(row, "age", ("Ages",)) or deps.clean_text(row.get("age_band"))
    body = deps.row_value(row, "body", ("Body types",)) or deps.clean_text(row.get("body_type"))
    pose = deps.row_value(row, "pose", ("Pose",))
    pose = pose.replace(", affectionate and flirtatious but non-explicit", "")
    clothing = deps.clean_clothing(deps.row_value(row, "item", deps.item_labels) or deps.row_value(row, "clothing", ("Clothing",)))
    scene = deps.row_value(row, "scene_text", ("Scene", "Setting"))
    expression = ""
    if not deps.expression_disabled(row):
        expression = deps.row_value(row, "character_expression_text") or deps.row_value(
            row,
            "expression",
            ("Facial expressions", "Facial expression"),
        )
    composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
    camera_scene = deps.clean_text(row.get("camera_scene_directive"))
    style = deps.field_row_value(row, "style") if keep_style else ""

    parts = [couple_subject_sentence(subject, ages, deps.cap_first, deps.clean_age_phrase)]
    if body:
        parts.append(f"Their body types are {body}")
    if clothing:
        parts.append(couple_clothing_sentence(clothing, deps.clean_text))
    if pose:
        parts.append(f"The pose is {pose}")
    if scene:
        parts.append(f"The setting is {scene}")
    if deps.detail_allows(detail_level) and camera_scene:
        parts.append(camera_scene)
    if expression:
        expression, labeled_expression = expression_detail(expression, deps.clean_text)
        if labeled_expression:
            parts.append(f"The expression details show {expression}")
        else:
            parts.append(f"Their expressions are {expression}")
    if deps.detail_allows(detail_level) and composition:
        parts.append(f"The composition is {composition}")
    if keep_style and style:
        parts.append(f"The visual style is {style}")
    return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(couple)")


def configured_cast_from_row_result(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> CaptionMetadataRoute | None:
    row = request.row
    detail_level = request.detail_level
    keep_style = request.keep_style
    if deps.clean_text(row.get("subject_type")) != "configured_cast":
        if "hardcore sexual poses" not in deps.clean_text(row.get("main_category")).lower():
            return None

    subject = deps.subject_phrase_from_counts(row)
    verb = deps.verb_for_row(row)
    cast = deps.row_value(row, "cast_summary", ("Cast",))
    role_graph = deps.row_value(row, "role_graph", ("Role graph",))
    item = deps.row_value(row, "item", deps.item_labels)
    axis_detail = deps.item_axis_detail_text(row, " ".join(part for part in (role_graph, item) if part))
    scene = deps.row_value(row, "scene_text", ("Setting", "Scene"))
    expression = ""
    if not deps.expression_disabled(row):
        expression = deps.row_value(row, "character_expression_text") or deps.row_value(
            row,
            "expression",
            ("Facial expressions", "Facial expression"),
        )
    composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
    camera_scene = deps.clean_text(row.get("camera_scene_directive"))
    cast_descriptor_text = deps.row_value(row, "cast_descriptor_text", ("Characters", "Cast descriptors"))
    scene_kind = deps.field_row_value(row, "scene_kind") or "explicit adult sex scene"
    style = deps.field_row_value(row, "style") if keep_style else ""

    parts = [f"{deps.cap_first(subject)} {verb} shown as a consensual {scene_kind}"]
    if cast_descriptor_text:
        parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text))
    if cast and not cast_descriptor_text:
        parts.append(f"The cast is {cast}")
    if role_graph:
        parts.append(role_graph)
    if item:
        parts.append(f"The {deps.metadata_action_label(row)} is {item}")
    if axis_detail:
        parts.append(f"Selected action details include {axis_detail}")
    scene_bits = []
    if scene:
        scene_bits.append(f"set in {scene}")
    if expression:
        expression, labeled_expression = expression_detail(expression, deps.clean_text)
        if labeled_expression:
            scene_bits.append(f"showing {expression}")
        else:
            scene_bits.append(f"with {expression}")
    if composition:
        scene_bits.append(f"framed as {composition}")
    if scene_bits and deps.detail_allows(detail_level):
        parts.append(", ".join(scene_bits))
    if deps.detail_allows(detail_level) and camera_scene:
        parts.append(camera_scene)
    if keep_style and style:
        parts.append(f"The visual style is {style}")
    return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(configured_cast)")


def group_or_layout_from_row_result(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> CaptionMetadataRoute | None:
    row = request.row
    detail_level = request.detail_level
    keep_style = request.keep_style
    primary = deps.clean_text(row.get("primary_subject"))
    if "group" not in primary and primary != "layout scene":
        return None

    subject = deps.field_row_value(row, "subject_phrase") or primary
    age = deps.row_value(row, "age", ("Ages",)) or deps.clean_text(row.get("age_band"))
    item = deps.clean_clothing(deps.row_value(row, "item", deps.item_labels) or deps.row_value(row, "clothing", ("Clothing",)))
    scene = deps.row_value(row, "scene_text", ("Scene", "Setting"))
    expression = ""
    if not deps.expression_disabled(row):
        expression = deps.row_value(row, "character_expression_text") or deps.row_value(
            row,
            "expression",
            ("Facial expressions", "Facial expression"),
        )
    composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
    camera_scene = deps.clean_text(row.get("camera_scene_directive"))
    style = deps.field_row_value(row, "style") if keep_style else ""

    if primary == "layout scene":
        parts = [f"{deps.cap_first(subject)} is arranged as an adults-only designed illustration layout"]
        if expression:
            expression, labeled_expression = expression_detail(expression, deps.clean_text)
            if labeled_expression:
                parts.append(f"The featured expression details show {expression}")
            else:
                parts.append(f"The featured expression is {expression}")
    else:
        parts = [f"{deps.cap_first(subject)} includes adults"]
        if age:
            parts[0] += f" ages {age}"
        if item:
            parts.append(f"They wear {item}")
        if expression:
            expression, labeled_expression = expression_detail(expression, deps.clean_text)
            if labeled_expression:
                parts.append(f"Their expressions show {expression}")
            else:
                parts.append(f"They show {expression}")
    if scene:
        parts.append(f"The setting is {scene}")
    if deps.detail_allows(detail_level) and camera_scene:
        parts.append(camera_scene)
    if deps.detail_allows(detail_level) and composition:
        parts.append(f"The composition is {composition}")
    if keep_style and style:
        parts.append(f"The visual style is {style}")
    return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(group_layout)")


def insta_of_pair_from_row_result(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> CaptionMetadataRoute | None:
    row = request.row
    detail_level = request.detail_level
    keep_style = request.keep_style
    pair_target = target_policy.pair_policy(request.target)
    target = pair_target.pair_target
    if not input_policy.is_pair_metadata(row):
        return None
    soft_row = row.get("softcore_row")
    hard_row = row.get("hardcore_row")
    if not isinstance(soft_row, dict) or not isinstance(hard_row, dict):
        return None

    hard_row_for_text = dict(hard_row)
    options = row.get("options")
    if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room":
        if not hard_row_for_text.get("scene_text") and soft_row.get("scene_text"):
            hard_row_for_text["scene_text"] = soft_row["scene_text"]
        if not hard_row_for_text.get("composition") and soft_row.get("composition"):
            hard_row_for_text["composition"] = soft_row["composition"]

    include_soft = pair_target.include_softcore
    include_hard = pair_target.include_hardcore
    soft_text = ""
    hard_text = ""
    if include_soft:
        soft_text, _soft_method = deps.metadata_to_prose(soft_row, detail_level, keep_style, "single")
    if include_hard:
        hard_text, _hard_method = deps.metadata_to_prose(hard_row_for_text, detail_level, keep_style, "single")
    descriptor = deps.clean_text(row.get("shared_descriptor"))
    options = row.get("options") if isinstance(row.get("options"), dict) else {}
    cast_descriptors = row.get("shared_cast_descriptors")
    if isinstance(cast_descriptors, list):
        cast_descriptor_text = "; ".join(deps.clean_text(item) for item in cast_descriptors if deps.clean_text(item))
    else:
        cast_descriptor_text = deps.clean_text(cast_descriptors)
    labels = deps.cast_labels(cast_descriptor_text)

    same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"

    parts = []
    if not soft_text and not hard_text:
        if cast_descriptor_text:
            parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text))
        elif descriptor:
            parts.append(f"A {descriptor}")
    if same_soft_cast and include_soft:
        parts.append(
            deps.softcore_caption_setup_phrase(
                same_cast=True,
                target_auto=target == "auto",
            )
        )
        partner_styling = row.get("softcore_partner_styling")
        if isinstance(partner_styling, dict):
            outfits = partner_styling.get("outfits")
            if isinstance(outfits, list):
                outfit_text = deps.human_join([deps.clean_text(item) for item in outfits if deps.clean_text(item)])
                outfit_text = deps.natural_label_text(outfit_text, labels)
                if outfit_text:
                    parts.append(f"Softcore partner styling: {outfit_text}")
            pose = deps.clean_text(partner_styling.get("pose"))
            if pose:
                parts.append(f"The shared softcore cast pose is {pose}")
    if soft_text:
        parts.append(f"Softcore side: {soft_text}" if target == "auto" else soft_text)
    if hard_text:
        parts.append(f"Hardcore side: {hard_text}" if target == "auto" else hard_text)
    if not parts:
        return None
    return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(insta_of_pair)")


def single_from_row(request: CaptionMetadataRouteRequest, deps: CaptionMetadataRouteDependencies) -> tuple[str, str] | None:
    result = single_from_row_result(request, deps)
    return result.as_tuple() if result else None


def couple_from_row(request: CaptionMetadataRouteRequest, deps: CaptionMetadataRouteDependencies) -> tuple[str, str] | None:
    result = couple_from_row_result(request, deps)
    return result.as_tuple() if result else None


def configured_cast_from_row(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> tuple[str, str] | None:
    result = configured_cast_from_row_result(request, deps)
    return result.as_tuple() if result else None


def group_or_layout_from_row(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> tuple[str, str] | None:
    result = group_or_layout_from_row_result(request, deps)
    return result.as_tuple() if result else None


def insta_of_pair_from_row(
    request: CaptionMetadataRouteRequest,
    deps: CaptionMetadataRouteDependencies,
) -> tuple[str, str] | None:
    result = insta_of_pair_from_row_result(request, deps)
    return result.as_tuple() if result else None
