from __future__ import annotations

import re
from typing import Any

try:
    from . import formatter_input as input_policy
    from . import item_axis_policy
    from . import route_metadata as route_metadata_policy
    from . import sdxl_presets as sdxl_policy
    from . import sdxl_tag_routes
    from . import softcore_text_policy
except ImportError:  # Allows local smoke tests with `python -c`.
    import formatter_input as input_policy
    import item_axis_policy
    import route_metadata as route_metadata_policy
    import sdxl_presets as sdxl_policy
    import sdxl_tag_routes
    import softcore_text_policy


PROMPT_FIELD_LABELS = input_policy.prompt_field_labels()

INCOMPATIBLE_ROUTE_TAGS = {
    "action:anal": ("oral sex", "outercourse", "manual stimulation"),
    "action:penetration": ("oral sex", "outercourse", "anal sex", "manual stimulation"),
    "action:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"),
    "action:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"),
    "action:manual": ("penetrative sex", "penetration", "oral sex", "anal sex", "outercourse"),
    "position:penetrative": ("oral sex", "outercourse", "anal sex", "manual stimulation"),
    "position:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"),
    "position:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"),
    "position:manual": ("penetrative sex", "penetration", "oral sex", "anal sex", "outercourse"),
    "position:anal": ("oral sex", "outercourse", "manual stimulation"),
}


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


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 split_tag_text(text: Any) -> list[str]:
    text = clean(text)
    if not text:
        return []
    text = input_policy.strip_prompt_field_labels(text, field_labels=PROMPT_FIELD_LABELS)
    text = re.sub(r"\bWoman [A-Z]'s\b", "woman's", text)
    text = re.sub(r"\bMan [A-Z]'s\b", "man's", text)
    text = re.sub(r"\bWoman [A-Z]\b", "woman", text)
    text = re.sub(r"\bMan [A-Z]\b", "man", text)
    text = re.sub(r"\b(?:the\s+)?(?:woman|man)\s+has\s+", "", text, flags=re.IGNORECASE)
    text = re.sub(
        r"\b(?:Clothing state|Visual clothing state|visible remaining styling|teaser outfit detail|softcore visual reference|Sexual scene|Role graph):\s*",
        "",
        text,
        flags=re.IGNORECASE,
    )
    text = re.sub(r"(?<=[A-Za-z0-9)])\.\s+(?=[A-Za-z])", ", ", text)
    text = re.sub(r"(?<!-)\b(?:and|with)\b(?!-)", ",", text, flags=re.IGNORECASE)
    text = re.sub(r"\b(woman|man),\s+(woman|man)\s+are\b", r"\1 and \2 are", text, flags=re.IGNORECASE)
    parts = re.split(r"\s*[,;]\s*", text)
    tags = []
    for part in parts:
        part = re.sub(r"^keep\s+", "", clean(part).strip(" ."), flags=re.IGNORECASE)
        if part:
            tags.append(part)
    return tags


def tag_key(tag: str) -> str:
    text = clean(tag).lower()
    text = re.sub(r"^\((.*?):[0-9.]+\)$", r"\1", text)
    text = text.strip("() ")
    return text


def add(tags: list[str], seen: set[str], value: Any) -> None:
    for tag in split_tag_text(value):
        key = tag_key(tag)
        if key and key not in seen:
            tags.append(tag)
            seen.add(key)


def add_one(tags: list[str], seen: set[str], tag: str) -> None:
    tag = clean(tag).strip(" ,")
    key = tag_key(tag)
    if tag and key and key not in seen:
        tags.append(tag)
        seen.add(key)


def metadata_family_tags(row: dict[str, Any]) -> list[str]:
    tags: list[str] = []
    action_family = route_metadata_policy.row_action_family(row)
    tags.extend(sdxl_policy.SDXL_ACTION_FAMILY_TAGS.get(action_family, ()))

    position_family = route_metadata_policy.row_position_family(row)
    tags.extend(sdxl_policy.SDXL_POSITION_FAMILY_TAGS.get(position_family, ()))

    for key in route_metadata_policy.row_position_keys(row, include_unknown=True):
        key_text = clean(key)
        if key_text:
            tags.append(key_text.replace("_", " "))
    return tags


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


def axis_value_tags(row: dict[str, Any]) -> list[str]:
    tags: list[str] = []
    seen: set[str] = set()
    for text in item_axis_policy.row_axis_value_texts(row):
        add(tags, seen, text)
    return tags


def combine_tags(*parts: Any) -> str:
    tags: list[str] = []
    seen: set[str] = set()
    for part in parts:
        add(tags, seen, part)
    return ", ".join(tags)


def combine_negative(*parts: Any) -> str:
    return combine_tags(*(part for part in parts if clean(part)))


def count_tag(women_count: int = 0, men_count: int = 0) -> list[str]:
    tags = []
    if women_count > 0:
        tags.append(f"{women_count}woman" if women_count == 1 else f"{women_count}women")
    if men_count > 0:
        tags.append(f"{men_count}man" if men_count == 1 else f"{men_count}men")
    return tags


def infer_counts(row: dict[str, Any]) -> tuple[int, int]:
    try:
        women = int(row.get("women_count") or 0)
        men = int(row.get("men_count") or 0)
    except (TypeError, ValueError):
        women = men = 0
    if women or men:
        return women, men
    subject = clean(row.get("subject_type") or row.get("primary_subject")).lower()
    phrase = clean(row.get("subject_phrase")).lower()
    text = f"{subject} {phrase}"
    if "two women" in text:
        return 2, 0
    if "two men" in text:
        return 0, 2
    if "woman and" in text or "woman a" in text and "man a" in text:
        return 1, 1
    if "group" in text:
        return 2, 2
    if "man" in text and "woman" not in text:
        return 0, 1
    return 1, 0


def character_tags_from_descriptor(descriptor: Any) -> list[str]:
    text = clean(descriptor)
    text = re.sub(r"\bWoman [A-Z]\s*/\s*primary creator:\s*", "", text)
    text = re.sub(r"\b(?:Woman|Man) [A-Z]:\s*", "", text)
    text = re.sub(r"\balongside\b", ",", text, flags=re.IGNORECASE)
    parts = split_tag_text(text)
    cleaned = []
    for part in parts:
        part = re.sub(r"\bfigure\b", "build", part, flags=re.IGNORECASE)
        part = part.replace("adult adult", "adult")
        cleaned.append(part)
    return cleaned


def normal_character_tags(row: dict[str, Any]) -> list[str]:
    descriptor = clean(row.get("cast_descriptor_text"))
    if descriptor:
        return character_tags_from_descriptor(descriptor)

    parts = [
        clean(row.get("age") or row.get("age_band")),
        clean(row.get("subject_phrase") or row.get("subject_type") or row.get("primary_subject")),
        clean(row.get("body_phrase") or row.get("body") or row.get("body_type")),
        clean(row.get("skin")),
        clean(row.get("hair")),
        clean(row.get("eyes")),
    ]
    return [part for part in parts if part and part not in ("woman", "man", "single_any")]


def camera_tags_from_config(config: Any) -> list[str]:
    if not isinstance(config, dict):
        return []
    if clean(config.get("camera_detail")) == "off" or clean(config.get("camera_mode")) == "disabled":
        return []
    custom = clean(config.get("custom_camera_prompt"))
    tags = split_tag_text(custom)
    direction = clean(config.get("orbit_direction"))
    elevation = clean(config.get("orbit_elevation_label"))
    distance = clean(config.get("orbit_distance_label"))
    for value in (direction, elevation, distance):
        if value and value != "auto":
            tags.extend(split_tag_text(value))
    for key in ("angle", "shot_size", "distance", "lens", "orientation", "subject_focus"):
        value = clean(config.get(key)).replace("_", " ")
        if value and value != "auto":
            tags.append(value)
    return tags


def camera_tags(row: dict[str, Any], directive: Any = "", config: Any = None) -> list[str]:
    tags = split_tag_text(directive)
    tags.extend(camera_tags_from_config(config if config is not None else row.get("camera_config")))
    camera_directive = clean(row.get("camera_directive"))
    if camera_directive:
        tags.extend(split_tag_text(camera_directive))
    out = []
    for tag in tags:
        tag = tag.replace("0-degree front view", "(front facing:1.15)")
        tag = tag.replace("front view", "(front facing:1.15)")
        tag = tag.replace("right side view", "side view")
        tag = tag.replace("left side view", "side view")
        out.append(tag)
    return out


def explicit_tags(text: str, nude_weight: float) -> list[str]:
    lower = text.lower()
    tags: list[str] = []
    if any(token in lower for token in ("fully nude", "fully exposed", "naked", "bare skin unobstructed", "explicit_nude")):
        tags.append(f"(naked:{nude_weight:.2f})")
    if any(token in lower for token in ("nipples", "breasts exposed", "bare breasts", "nipple")):
        tags.append("nipples")
    if any(token in lower for token in ("pussy", "vulva", "genitals")):
        tags.append("pussy")
    if any(token in lower for token in ("penis", "cock")):
        tags.append("penis")
    if "penetration" in lower or "thrust" in lower:
        tags.append("penetration")
    if "vaginal" in lower:
        tags.append("pussy")
    oral_terms = (
        "oral sex",
        "oral-sex",
        "blowjob",
        "deepthroat",
        "fellatio",
        "cunnilingus",
        "pussy licking",
        "mouth on",
        "mouth pressed",
        "mouth contact",
        "mouth around",
        "lips wrapped",
        "takes the penis in her mouth",
        "takes the man's penis",
        "takes the viewer's penis",
        "penis in her mouth",
        "tongue on pussy",
        "tongue along the penis",
        "tongue along the penis shaft",
        "tongue touches the underside",
        "licking the penis",
        "testicle sucking",
        "balls licking",
        "balls-licking",
    )
    if any(token in lower for token in oral_terms):
        tags.append("oral sex")
    if "anal" in lower:
        tags.append("anal sex")
    if any(token in lower for token in ("semen", "ejaculates", "cum ")):
        tags.append("semen")
    return tags


def filter_incompatible_route_tags(tags: list[str], row: dict[str, Any]) -> list[str]:
    action_family = route_metadata_policy.row_action_family(row)
    position_family = route_metadata_policy.row_position_family(row)
    blocked: set[str] = set()
    for scope, family in (("action", action_family), ("position", position_family)):
        for tag in INCOMPATIBLE_ROUTE_TAGS.get(f"{scope}:{family}", ()):
            blocked.add(tag_key(tag))
    if not blocked:
        return tags
    return [tag for tag in tags if tag_key(tag) not in blocked]


def softcore_pair_tags(row: dict[str, Any], root: dict[str, Any]) -> list[str]:
    tags = ["softcore teaser", softcore_text_policy.softcore_style_tag()]
    options = root.get("options") if isinstance(root.get("options"), dict) else {}
    cast_mode = clean(options.get("softcore_cast")).lower()
    if cast_mode == "same_as_hardcore" or root.get("shared_cast_descriptors"):
        tags.append("same-cast creator frame")
    elif "solo" in clean(row.get("subject_type") or row.get("primary_subject")).lower():
        tags.append("solo creator frame")
    return tags


def tag_route_dependencies() -> sdxl_tag_routes.SDXLTagRouteDependencies:
    return sdxl_tag_routes.SDXLTagRouteDependencies(
        clean=clean,
        row_value=row_value,
        tag_key=tag_key,
        add=add,
        add_one=add_one,
        count_tag=count_tag,
        infer_counts=infer_counts,
        normal_character_tags=normal_character_tags,
        character_tags_from_descriptor=character_tags_from_descriptor,
        metadata_family_tags=metadata_family_tags,
        formatter_hint_tags=formatter_hint_tags,
        axis_value_tags=axis_value_tags,
        camera_tags=camera_tags,
        explicit_tags=explicit_tags,
        filter_incompatible_route_tags=filter_incompatible_route_tags,
        softcore_pair_tags=softcore_pair_tags,
    )
