from __future__ import annotations

import re
from typing import Any

try:
    from . import formatter_input as input_policy
    from . import krea_format_route
    from . import route_metadata as route_metadata_policy
    from . import softcore_text_policy
    from .krea_action_context import (
        is_close_foreplay_text as _is_close_foreplay_text,
        is_outercourse_text as _is_outercourse_text,
        normalize_hardcore_detail_density as _normalize_hardcore_detail_density,
    )
    from . import krea_configured_cast_formatter
    from . import krea_normal_formatter
    from . import krea_pair_formatter
    from . import krea_row_fields
    from .hardcore_text_cleanup import (
        sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
        sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
    )
    from .krea_cast import (
        cast_prose as _cast_prose,
        label_join as _label_join,
        lowercase_for_inline_join as _lowercase_for_inline_join,
        natural_label_text as _natural_label_text,
        prompt_cast_descriptors as _prompt_cast_descriptors,
    )
    from .krea_clothing import natural_clothing_state as _natural_clothing_state
    from .krea_action_positions import action_position_phrase as _action_position_phrase
    from .krea_actions import hardcore_action_sentence as _hardcore_action_sentence
    from .krea_pov import (
        filter_pov_labeled_clauses as _filter_pov_labeled_clauses,
        merge_labels as _merge_labels,
        pov_camera_phrase as _pov_camera_phrase,
        pov_composition_text as _pov_composition_text,
        pov_labels_from_value as _pov_labels_from_value,
    )
    from .krea_pov_actions import pov_action_phrase as _pov_action_phrase
    from .prompt_hygiene import combine_negative_text, sanitize_negative_text, sanitize_prose_text
except ImportError:  # Allows local smoke tests with `python -c`.
    import formatter_input as input_policy
    import krea_format_route
    import route_metadata as route_metadata_policy
    import softcore_text_policy
    from krea_action_context import (
        is_close_foreplay_text as _is_close_foreplay_text,
        is_outercourse_text as _is_outercourse_text,
        normalize_hardcore_detail_density as _normalize_hardcore_detail_density,
    )
    import krea_configured_cast_formatter
    import krea_normal_formatter
    import krea_pair_formatter
    import krea_row_fields
    from hardcore_text_cleanup import (
        sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
        sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
    )
    from krea_cast import (
        cast_prose as _cast_prose,
        label_join as _label_join,
        lowercase_for_inline_join as _lowercase_for_inline_join,
        natural_label_text as _natural_label_text,
        prompt_cast_descriptors as _prompt_cast_descriptors,
    )
    from krea_clothing import natural_clothing_state as _natural_clothing_state
    from krea_action_positions import action_position_phrase as _action_position_phrase
    from krea_actions import hardcore_action_sentence as _hardcore_action_sentence
    from krea_pov import (
        filter_pov_labeled_clauses as _filter_pov_labeled_clauses,
        merge_labels as _merge_labels,
        pov_camera_phrase as _pov_camera_phrase,
        pov_composition_text as _pov_composition_text,
        pov_labels_from_value as _pov_labels_from_value,
    )
    from krea_pov_actions import pov_action_phrase as _pov_action_phrase
    from prompt_hygiene import combine_negative_text, sanitize_negative_text, sanitize_prose_text


TRIGGER_CANDIDATES = (
    "sxcpinup_coloredpencil",
    "sxcppnl7",
)
PROMPT_FIELD_LABELS = input_policy.prompt_field_labels()


def _clean(value: Any) -> str:
    return input_policy.clean_text(value)


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 _expression_disabled(row: dict[str, Any]) -> bool:
    return bool(row.get("expression_disabled")) or _is_false(row.get("expression_enabled", True))


def _sentence(text: str) -> str:
    text = _clean(text).strip(" ,;")
    if not text:
        return ""
    text = text[:1].upper() + text[1:]
    if text[-1] not in ".!?":
        text += "."
    return text


def _paragraph(parts: list[str]) -> str:
    return " ".join(part for part in (_sentence(part) for part in parts) if part)


def _formatter_hint_parts(*rows: dict[str, Any]) -> list[str]:
    hints: list[str] = []
    for row in rows:
        if not isinstance(row, dict):
            continue
        for hint in route_metadata_policy.row_formatter_hints(row, "krea"):
            hint = _clean(hint).strip(" .")
            if hint and hint not in hints:
                hints.append(hint)
    return hints


def _append_formatter_hints(prompt: str, *rows: dict[str, Any]) -> str:
    hints = _formatter_hint_parts(*rows)
    if not hints:
        return prompt
    return _paragraph([prompt, *hints])


def _with_indefinite_article(text: str) -> str:
    text = _clean(text)
    if not text or text.lower().startswith(("a ", "an ")):
        return text
    article = "an" if text[:1].lower() in "aeiou" else "a"
    return f"{article} {text}"


def _maybe_json(text: str) -> dict[str, Any] | None:
    return input_policy.maybe_json(text)


def _row_from_inputs(source_text: str, metadata_json: str, input_hint: str) -> tuple[dict[str, Any] | None, str]:
    return input_policy.row_from_inputs(source_text, metadata_json, input_hint)


def _strip_trigger(text: str, preserve_trigger: bool) -> str:
    return input_policy.strip_trigger_prefix(text, TRIGGER_CANDIDATES, preserve_trigger=preserve_trigger)


def _split_avoid(text: str) -> tuple[str, str]:
    return input_policy.split_avoid(text)


def _prompt_field(text: str, label: str) -> str:
    return input_policy.prompt_field(text, label, field_labels=PROMPT_FIELD_LABELS)


def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str:
    return input_policy.row_value(row, key, labels, field_labels=PROMPT_FIELD_LABELS)


def _body_phrase(body: Any, figure_note: Any = "") -> str:
    body = _clean(body)
    figure_note = _clean(figure_note)
    if not body:
        return figure_note
    if not figure_note:
        return f"{body} figure"
    if "figure" in figure_note.lower():
        return f"{body} build and {figure_note}"
    return f"{body} figure with {figure_note}"


def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
    caption = _strip_trigger(_clean(row.get("caption")), False)
    if not caption:
        return {}
    subject = _clean(row.get("primary_subject"))
    age = _clean(row.get("age_band") or row.get("age"))
    body = _clean(row.get("body_phrase"))
    if not body:
        body_type = _clean(row.get("body_type") or row.get("body"))
        figure = _clean(row.get("figure"))
        body = _body_phrase(body_type, figure)
    front = f"{subject}, {age}, {body}, "
    if subject in ("woman", "man") and age and body and caption.startswith(front):
        try:
            skin, hair, eyes, _rest = caption[len(front) :].split(", ", 3)
        except ValueError:
            return {}
        return {"body_phrase": body, "skin": skin, "hair": hair, "eyes": eyes}
    return {}


def _combine_negative(*parts: str) -> str:
    return combine_negative_text(*parts)


def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
    text = _clean(text)
    if not text:
        return ""
    if len(labels) < 3:
        text = re.sub(r"\s*(?:while|as)\s+another partner watches\b", "", text, flags=re.IGNORECASE)
        text = re.sub(r"\banother partner watches\b", "", text, flags=re.IGNORECASE)
        text = re.sub(r",?\s*\bone partner held between two bodies\b", "", text, flags=re.IGNORECASE)
        text = re.sub(r",?\s*\bthree bodies locked together\b", "", text, flags=re.IGNORECASE)
        text = re.sub(r",?\s*\bthree bodies\b", "", text, flags=re.IGNORECASE)
        text = re.sub(r"\bwith\s*,\s*", "with ", text, flags=re.IGNORECASE)
    text = re.sub(r"\bwhile blowjob\b", "during a blowjob", text, flags=re.IGNORECASE)
    text = re.sub(r"\bfeaturing blowjob\b", "featuring a blowjob", text, flags=re.IGNORECASE)
    text = re.sub(r"\s+,", ",", text)
    text = re.sub(r",\s*,", ",", text)
    text = re.sub(r"\s{2,}", " ", text).strip(" ,")
    return text


def _composition_phrase(
    composition: Any,
    action: str = "",
    prefix: str = "framed as",
    detail_density: str = "balanced",
) -> str:
    composition = _clean(composition)
    if not composition:
        return ""
    composition = re.sub(r"\s+composition$", "", composition, flags=re.IGNORECASE)
    composition = re.sub(r"\bcomposition\b", "frame", composition, flags=re.IGNORECASE).strip(" ,")
    if not composition:
        return ""
    action_lower = _clean(action).lower()
    composition_lower = composition.lower()
    detail_density = _normalize_hardcore_detail_density(detail_density)
    if "first-person underview" in action_lower or "straddling the viewer's face" in action_lower:
        if any(token in composition_lower for token in ("mirror-reflected", "oral scene", "face and body visible")):
            return (
                f"{prefix} close first-person underview with the woman's thighs framing the camera and the oral contact centered"
            )
    if "pov viewer" in action_lower and any(
        token in action_lower
        for token in ("ass raised", "seen from behind", "on all fours", "bent forward", "face-down")
    ):
        return (
            f"{prefix} first-person rear-view frame looking down at the woman's raised ass, with foreground hands and rear-entry contact readable"
        )
    oral_pose_tokens = (
        "kneeling oral",
        "side-lying oral",
        "spread-leg oral",
        "standing oral",
        "edge-of-bed oral",
        "face-sitting",
        "sixty-nine",
        "reclining cunnilingus",
        "straddled oral",
        "chair oral",
    )
    if "oral" in action_lower:
        composition_oral_tokens = [token for token in oral_pose_tokens if token in composition_lower]
        if composition_oral_tokens and not any(token in action_lower for token in composition_oral_tokens):
            match = re.search(r"\bwith\s+(.+)$", composition, flags=re.IGNORECASE)
            return f"framed with {match.group(1)}" if match else ""
    if _is_outercourse_text(action_lower):
        return f"{prefix} {composition}"
    position = _action_position_phrase(action)
    close_or_aftermath = any(
        token in composition_lower
        for token in ("close-up", "close crop", "tight", "direct-flash", "subscriber-view", "post-ejaculation", "aftermath")
    )
    if _is_close_foreplay_text(action_lower) and close_or_aftermath:
        return f"{prefix} {composition}, in one continuous first-person close-body frame"
    if position and close_or_aftermath:
        if detail_density == "compact":
            return f"{prefix} {composition}, with the {position} still readable"
        return f"{prefix} {composition}, with the {position} still readable and the body contact clear"
    return f"{prefix} {composition}"


def _clean_age(age: Any) -> str:
    return _clean(age)


def _age_detail_phrase(age: Any) -> str:
    text = _clean(age)
    text = re.sub(r"\s+adults?$", "", text).strip()
    return text.replace("-year-old", " years old")


def _age_subject(row: dict[str, Any], fallback_subject: str = "adult person") -> str:
    subject = _clean(row.get("subject_phrase") or row.get("primary_subject") or row.get("subject") or fallback_subject)
    age = _clean_age(row.get("age_band") or row.get("age"))
    if row.get("subject_type") == "configured_cast":
        return _clean(row.get("subject_phrase") or subject)
    if subject in ("woman", "man"):
        if age:
            return f"{age} {subject}" if "adult" in age.lower() else f"{age} adult {subject}"
        return f"adult {subject}"
    if age and "adult" not in subject.lower():
        return f"{age} {subject}"
    return subject or fallback_subject


def _appearance_phrase(row: dict[str, Any]) -> str:
    front = _single_caption_front(row)
    parts = [
        _row_value(row, "body_phrase") or front.get("body_phrase"),
        _row_value(row, "skin") or front.get("skin"),
        _row_value(row, "hair") or front.get("hair"),
        _row_value(row, "eyes") or front.get("eyes"),
    ]
    return ", ".join(_clean(part) for part in parts if _clean(part))


def _expression_phrase(expression: Any) -> str:
    expression = _clean(expression)
    if not expression:
        return ""
    if ";" in expression or re.search(
        r"\b(?:Woman|Man) [A-Z] has\b|\bthe (?:woman|man) has\b",
        expression,
        flags=re.IGNORECASE,
    ):
        expression = re.sub(
            r"\b((?:Woman|Man) [A-Z]|the (?:woman|man)) has\b",
            r"\1 showing",
            expression,
            flags=re.IGNORECASE,
        )
        return f"With {expression[:1].lower()}{expression[1:]}"
    return f"with {expression}"


def _natural_camera_phrase(value: str) -> str:
    value = _clean(value)
    if not value:
        return ""
    value = re.sub(r"^camera:\s*", "", value, flags=re.IGNORECASE).strip()
    if not value:
        return ""
    return f"Captured with {value}"


def _camera_phrase(row: dict[str, Any]) -> str:
    directive = _clean(row.get("camera_directive"))
    if directive:
        return _natural_camera_phrase(directive)
    config = row.get("camera_config")
    if isinstance(config, dict):
        detail = _clean(config.get("camera_detail"))
        if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
            return ""
        custom = _clean(config.get("custom_camera_prompt"))
        if custom:
            base = _clean(config.get("camera_mode")).replace("_", " ")
            pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
            return _natural_camera_phrase(", ".join(pieces))
        mode = _clean(config.get("camera_mode")).replace("_", " ")
        shot = _clean(config.get("shot_size")).replace("_", " ")
        angle = _clean(config.get("angle")).replace("_", " ")
        pieces = [piece for piece in (mode, shot, angle) if piece and piece != "auto" and piece != "standard"]
        if pieces:
            return _natural_camera_phrase(", ".join(pieces))
    return ""


def _camera_scene_phrase(row: dict[str, Any]) -> str:
    return _clean(row.get("camera_scene_directive"))


def _camera_phrase_from_config(config: Any) -> str:
    if not isinstance(config, dict):
        return ""
    detail = _clean(config.get("camera_detail"))
    if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
        return ""
    custom = _clean(config.get("custom_camera_prompt"))
    if custom:
        base = _clean(config.get("camera_mode")).replace("_", " ")
        pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
        return _natural_camera_phrase(", ".join(pieces))
    values = [
        _clean(config.get("camera_mode")).replace("_", " "),
        _clean(config.get("shot_size")).replace("_", " "),
        _clean(config.get("angle")).replace("_", " "),
        _clean(config.get("lens")).replace("_", " "),
        _clean(config.get("distance")).replace("_", " "),
        _clean(config.get("orientation")).replace("_", " "),
        _clean(config.get("phone_visibility")).replace("_", " "),
    ]
    pieces = [value for value in values if value and value not in ("auto", "standard")]
    if not pieces:
        return ""
    return _natural_camera_phrase(", ".join(pieces))


def _pair_camera_phrase(directive: Any, config: Any, row: dict[str, Any]) -> str:
    directive_text = _clean(directive)
    if directive_text:
        return _natural_camera_phrase(directive_text)
    if isinstance(config, dict) and (
        _clean(config.get("camera_detail")) == "off" or _clean(config.get("camera_mode")) == "disabled"
    ):
        return ""
    return _camera_phrase_from_config(config) or _camera_phrase(row)


def _style_phrase(row: dict[str, Any], style_mode: str) -> str:
    if style_mode == "minimal":
        return ""
    if style_mode == "photographic":
        return "realistic creator-shot photography with natural lighting, tactile skin and fabric detail, and clean social-media composition"
    style = _clean(row.get("style"))
    suffix = _clean(row.get("positive_suffix"))
    if style and suffix:
        return f"{style}; {suffix}"
    return style or suffix


def _krea_row_field_dependencies() -> krea_row_fields.KreaRowFieldDependencies:
    return krea_row_fields.KreaRowFieldDependencies(
        clean=_clean,
        row_value=_row_value,
        camera_phrase=_camera_phrase,
        camera_scene_phrase=_camera_scene_phrase,
        style_phrase=_style_phrase,
        expression_disabled=_expression_disabled,
    )


def _krea_normal_row_dependencies() -> krea_normal_formatter.KreaNormalRowDependencies:
    return krea_normal_formatter.KreaNormalRowDependencies(
        clean=_clean,
        row_value=_row_value,
        age_subject=_age_subject,
        age_detail_phrase=_age_detail_phrase,
        appearance_phrase=_appearance_phrase,
        with_indefinite_article=_with_indefinite_article,
        paragraph=_paragraph,
    )


def _krea_normal_row_request_from_row(
    row: dict[str, Any],
    detail_level: str,
    style_mode: str,
) -> krea_normal_formatter.KreaNormalRowRequest:
    fields = krea_row_fields.extract_krea_row_fields(
        row,
        style_mode,
        _krea_row_field_dependencies(),
    )
    return krea_normal_formatter.KreaNormalRowRequest(
        row=row,
        detail_level=detail_level,
        style_mode=style_mode,
        subject_type=fields.subject_type,
        primary=fields.primary,
        item=fields.item,
        scene=fields.scene,
        pose=fields.pose,
        expression=fields.expression,
        composition=fields.composition,
        camera=fields.camera,
        camera_scene=fields.camera_scene,
        style=fields.style,
    )


def _krea_configured_cast_dependencies() -> krea_configured_cast_formatter.KreaConfiguredCastDependencies:
    return krea_configured_cast_formatter.KreaConfiguredCastDependencies(
        clean=_clean,
        sanitize_hardcore_environment_anchors=_sanitize_hardcore_environment_anchors,
        sanitize_hardcore_axis_values=_sanitize_hardcore_axis_values,
        sanitize_scene_text_for_cast=_sanitize_scene_text_for_cast,
        normalize_hardcore_detail_density=_normalize_hardcore_detail_density,
        row_action_family=route_metadata_policy.row_action_family,
        hardcore_action_sentence=_hardcore_action_sentence,
        pov_action_phrase=_pov_action_phrase,
        pov_labels_from_value=_pov_labels_from_value,
        merge_labels=_merge_labels,
        cast_prose_omit=lambda text, omit_labels: _cast_prose(text, omit_labels=omit_labels),
        filter_pov_labeled_clauses=_filter_pov_labeled_clauses,
        natural_label_text=_natural_label_text,
        pov_composition_text=_pov_composition_text,
        pov_camera_phrase=lambda labels: _pov_camera_phrase(labels),
        expression_phrase=_expression_phrase,
        composition_phrase=_composition_phrase,
        paragraph=_paragraph,
    )


def _krea_configured_cast_request(
    row: dict[str, Any],
    detail_level: str,
    style_mode: str,
    primary: str,
    item: str,
    scene: str,
    expression: str,
    composition: str,
    source_composition: str,
    camera: str,
    camera_scene: str,
    style: str,
) -> krea_configured_cast_formatter.KreaConfiguredCastRequest:
    return krea_configured_cast_formatter.KreaConfiguredCastRequest(
        row=row,
        detail_level=detail_level,
        style_mode=style_mode,
        primary=primary,
        item=item,
        scene=scene,
        expression=expression,
        composition=composition,
        source_composition=source_composition,
        camera=camera,
        camera_scene=camera_scene,
        style=style,
    )


def _krea_configured_cast_request_from_row(
    row: dict[str, Any],
    detail_level: str,
    style_mode: str,
) -> krea_configured_cast_formatter.KreaConfiguredCastRequest:
    fields = krea_row_fields.extract_krea_row_fields(
        row,
        style_mode,
        _krea_row_field_dependencies(),
    )
    return _krea_configured_cast_request(
        row,
        detail_level,
        style_mode,
        fields.primary,
        fields.item,
        fields.scene,
        fields.expression,
        fields.composition,
        fields.source_composition,
        fields.camera,
        fields.camera_scene,
        fields.style,
    )


def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str]:
    subject_type = _clean(row.get("subject_type"))
    if subject_type == "configured_cast" or _clean(row.get("cast_summary")):
        return krea_configured_cast_formatter.format_configured_cast(
            _krea_configured_cast_request_from_row(row, detail_level, style_mode),
            _krea_configured_cast_dependencies(),
        )
    return krea_normal_formatter.format_normal_row(
        _krea_normal_row_request_from_row(row, detail_level, style_mode),
        _krea_normal_row_dependencies(),
    )


def _krea_pair_format_dependencies() -> krea_pair_formatter.KreaPairFormatDependencies:
    return krea_pair_formatter.KreaPairFormatDependencies(
        clean=_clean,
        prompt_cast_descriptors=_prompt_cast_descriptors,
        pair_camera_phrase=_pair_camera_phrase,
        camera_scene_phrase=_camera_scene_phrase,
        style_phrase=_style_phrase,
        sanitize_hardcore_environment_anchors=_sanitize_hardcore_environment_anchors,
        sanitize_hardcore_axis_values=_sanitize_hardcore_axis_values,
        sanitize_scene_text_for_cast=_sanitize_scene_text_for_cast,
        normalize_hardcore_detail_density=_normalize_hardcore_detail_density,
        row_action_family=route_metadata_policy.row_action_family,
        hardcore_action_sentence=_hardcore_action_sentence,
        pov_action_phrase=_pov_action_phrase,
        pov_labels_from_value=_pov_labels_from_value,
        merge_labels=_merge_labels,
        cast_prose_omit=lambda text, omit_labels: _cast_prose(text, omit_labels=omit_labels),
        label_join=_label_join,
        filter_pov_labeled_clauses=_filter_pov_labeled_clauses,
        natural_label_text=_natural_label_text,
        expression_disabled=_expression_disabled,
        expression_phrase=_expression_phrase,
        pov_camera_phrase=lambda labels: _pov_camera_phrase(labels),
        pov_soft_camera_phrase=lambda labels: _pov_camera_phrase(labels, softcore=True),
        pov_composition_text=_pov_composition_text,
        softcore_cast_presence_phrase=softcore_text_policy.softcore_cast_presence_phrase,
        natural_clothing_state=_natural_clothing_state,
        composition_phrase=_composition_phrase,
        paragraph=_paragraph,
        combine_negative=_combine_negative,
    )


def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str, str, str]:
    return krea_pair_formatter.format_insta_pair(
        krea_pair_formatter.KreaPairFormatRequest(
            row=row,
            detail_level=detail_level,
            style_mode=style_mode,
        ),
        _krea_pair_format_dependencies(),
    )


def _fallback_text_to_krea(
    source_text: str,
    preserve_trigger: bool,
    detail_level: str,
    style_mode: str,
) -> tuple[str, str, str]:
    positive, negative = _split_avoid(_strip_trigger(source_text, preserve_trigger))
    positive = re.sub(r"\b(?:Scene|Setting):", "The setting is", positive)
    positive = re.sub(r"\b(?:Pose|Sexual pose):", "The pose is", positive)
    positive = re.sub(r"\bFacial expressions?:", "The facial expression is", positive)
    positive = re.sub(r"\bComposition:", "The composition is", positive)
    positive = re.sub(r"\bRole graph:", "The role choreography is", positive)
    positive = re.sub(r"\bUse\b", "Use", positive)
    positive = _clean(positive)
    return _paragraph([positive]), negative, "text(fallback)"


def _krea_format_dependencies() -> krea_format_route.KreaFormatDependencies:
    return krea_format_route.KreaFormatDependencies(
        trigger_candidates=TRIGGER_CANDIDATES,
        clean=_clean,
        row_from_inputs=_row_from_inputs,
        normal_row_to_krea=_normal_row_to_krea,
        insta_pair_to_krea=_insta_pair_to_krea,
        fallback_text_to_krea=_fallback_text_to_krea,
        append_formatter_hints=_append_formatter_hints,
        combine_negative=_combine_negative,
        sanitize_prose_text=sanitize_prose_text,
        sanitize_negative_text=sanitize_negative_text,
    )


def format_krea2_prompt(
    source_text: str,
    metadata_json: str = "",
    negative_prompt: str = "",
    input_hint: str = "auto",
    target: str = "auto",
    detail_level: str = "balanced",
    style_mode: str = "preserve",
    preserve_trigger: bool = False,
    extra_positive: str = "",
    extra_negative: str = "",
) -> dict[str, str]:
    return krea_format_route.format_krea2_prompt(
        krea_format_route.KreaFormatRequest(
            source_text=source_text,
            metadata_json=metadata_json,
            negative_prompt=negative_prompt,
            input_hint=input_hint,
            target=target,
            detail_level=detail_level,
            style_mode=style_mode,
            preserve_trigger=preserve_trigger,
            extra_positive=extra_positive,
            extra_negative=extra_negative,
        ),
        _krea_format_dependencies(),
    )
