from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

try:
    from . import character_config as character_policy
except ImportError:  # Allows local smoke tests from the repository root.
    import character_config as character_policy


ROOT_DIR = Path(__file__).resolve().parent
PROFILE_DIR = ROOT_DIR / "profiles"
CHARACTER_MANUAL_FIELDS = (
    "manual_age",
    "manual_body",
    "body_phrase",
    "skin",
    "hair",
    "eyes",
    "softcore_outfit",
    "hardcore_clothing",
)


def body_phrase(body: Any, figure_note: Any = "") -> str:
    body = str(body or "").strip()
    figure_note = str(figure_note or "").strip()
    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 safe_profile_name(profile_name: str) -> str:
    profile_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(profile_name or "").strip()).strip("_")
    return profile_name[:64] or "profile"


def profile_path(profile_name: str) -> Path:
    return PROFILE_DIR / f"{safe_profile_name(profile_name)}.json"


def character_profile_choices() -> list[str]:
    if not PROFILE_DIR.exists():
        return ["manual"]
    names = sorted(path.stem for path in PROFILE_DIR.glob("*.json") if path.is_file())
    return ["manual"] + names


def load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[str, Any]:
    if not value:
        return {}
    if isinstance(value, dict):
        return value
    try:
        raw = json.loads(str(value))
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid {label} JSON: {exc}") from exc
    if not isinstance(raw, dict):
        raise ValueError(f"{label} must be a JSON object.")
    return raw


def parse_character_manual_config(value: str | dict[str, Any] | None) -> dict[str, str]:
    if not value:
        return {}
    if isinstance(value, dict):
        raw = value
    else:
        try:
            raw = json.loads(str(value))
        except json.JSONDecodeError:
            return {}
    if not isinstance(raw, dict):
        return {}
    return {
        key: str(raw.get(key) or "").strip()
        for key in CHARACTER_MANUAL_FIELDS
        if str(raw.get(key) or "").strip()
    }


def character_manual_summary(config: dict[str, str]) -> str:
    parts = [f"{key}={value}" for key, value in config.items() if value]
    return "; ".join(parts) if parts else "manual unrestricted"


def build_character_manual_config_json(
    manual: str | dict[str, Any] | None = "",
    combine_mode: str = "merge_nonempty",
    manual_age: str = "",
    manual_body: str = "",
    body_phrase: str = "",
    skin: str = "",
    hair: str = "",
    eyes: str = "",
    softcore_outfit: str = "",
    hardcore_clothing: str = "",
) -> str:
    base = {} if combine_mode == "replace_all" else parse_character_manual_config(manual)
    updates = {
        "manual_age": manual_age,
        "manual_body": manual_body,
        "body_phrase": body_phrase,
        "skin": skin,
        "hair": hair,
        "eyes": eyes,
        "softcore_outfit": softcore_outfit,
        "hardcore_clothing": hardcore_clothing,
    }
    for key, value in updates.items():
        value = str(value or "").strip()
        if value:
            base[key] = value
    result = {"config_type": "character_manual", **base}
    result["summary"] = character_manual_summary(base)
    return json.dumps(result, ensure_ascii=True, sort_keys=True)


def descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
    detail = character_policy.normalize_descriptor_detail(descriptor_detail)
    if detail != "auto":
        return detail
    return "compact" if str(subject or "").strip().lower() == "man" else "full"


def descriptor_from_parts(
    subject: Any,
    age: Any,
    body_phrase_value: Any,
    skin: Any,
    hair: Any,
    eyes: Any,
    descriptor_detail: Any = "auto",
) -> str:
    subject = str(subject or "person").strip() or "person"
    age_text = " ".join(str(age or "").strip().split())
    age_text = age_text.removesuffix(" adults").removesuffix(" adult").strip()
    if age_text in ("adult", "adults"):
        age_text = ""
    subject_phrase = f"{age_text} adult {subject}".strip() if age_text else f"adult {subject}"
    detail = descriptor_detail_for_subject(subject, descriptor_detail)
    detail_map = {
        "minimal": (body_phrase_value,),
        "compact": (body_phrase_value, skin),
        "medium": (body_phrase_value, skin, hair),
        "full": (body_phrase_value, skin, hair, eyes),
    }
    pieces = [subject_phrase, *detail_map.get(detail, detail_map["full"])]
    return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())


def row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
    row = load_json_object(metadata_json, "metadata_json")
    if isinstance(row.get("softcore_row"), dict):
        return row["softcore_row"]
    return row


def character_profile_descriptor(profile: dict[str, Any]) -> str:
    subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
    return descriptor_from_parts(
        subject,
        profile.get("age"),
        profile.get("body_phrase") or body_phrase(profile.get("body"), profile.get("figure")),
        profile.get("skin"),
        profile.get("hair"),
        profile.get("eyes"),
        profile.get("descriptor_detail"),
    )


def normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
    subject_type = str(profile.get("subject_type") or profile.get("primary_subject") or profile.get("subject") or "").strip()
    if subject_type not in ("woman", "man"):
        subject_type = "woman"
    body = str(profile.get("body") or profile.get("body_type") or "").strip()
    figure = str(profile.get("figure") or "").strip()
    normalized_body_phrase = str(profile.get("body_phrase") or "").strip() or body_phrase(body, figure)
    normalized = {
        "profile_type": "character",
        "profile_name": safe_profile_name(profile_name or str(profile.get("profile_name") or "")),
        "subject_type": subject_type,
        "subject": subject_type,
        "subject_phrase": subject_type,
        "age": str(profile.get("age") or profile.get("age_band") or "").strip(),
        "body": body,
        "body_phrase": normalized_body_phrase,
        "skin": str(profile.get("skin") or "").strip(),
        "hair": str(profile.get("hair") or "").strip(),
        "eyes": str(profile.get("eyes") or "").strip(),
        "figure": figure,
        "descriptor_detail": character_policy.normalize_descriptor_detail(profile.get("descriptor_detail")),
    }
    normalized["descriptor"] = character_profile_descriptor(normalized)
    return normalized


def build_character_profile_json(
    profile_name: str = "",
    source: str = "metadata_json",
    metadata_json: str | dict[str, Any] | None = "",
    character_slot_row: dict[str, Any] | None = None,
    subject_type: str = "woman",
    age: str = "",
    body: str = "",
    body_phrase_value: str = "",
    skin: str = "",
    hair: str = "",
    eyes: str = "",
    figure: str = "",
    save_now: bool = False,
) -> dict[str, str]:
    if source == "character_slot":
        row = character_slot_row or {}
        raw_profile = {
            "profile_name": profile_name,
            "subject_type": row.get("subject_type") or subject_type,
            "age": row.get("age") or age,
            "body": row.get("body") or body,
            "body_phrase": row.get("body_phrase") or body_phrase_value,
            "skin": row.get("skin") or skin,
            "hair": row.get("hair") or hair,
            "eyes": row.get("eyes") or eyes,
            "figure": row.get("figure") or figure,
            "descriptor_detail": row.get("descriptor_detail") or "auto",
        }
    elif source == "metadata_json":
        row = row_from_profile_metadata(metadata_json)
        raw_profile = {
            "profile_name": profile_name,
            "subject_type": row.get("subject_type") or row.get("primary_subject") or subject_type,
            "age": row.get("age") or row.get("age_band") or age,
            "body": row.get("body") or row.get("body_type") or body,
            "body_phrase": row.get("body_phrase") or body_phrase_value,
            "skin": row.get("skin") or skin,
            "hair": row.get("hair") or hair,
            "eyes": row.get("eyes") or eyes,
            "figure": row.get("figure") or figure,
            "descriptor_detail": row.get("descriptor_detail") or "auto",
        }
    else:
        raw_profile = {
            "profile_name": profile_name,
            "subject_type": subject_type,
            "age": age,
            "body": body,
            "body_phrase": body_phrase_value,
            "skin": skin,
            "hair": hair,
            "eyes": eyes,
            "figure": figure,
            "descriptor_detail": "auto",
        }
    profile = normalize_character_profile(raw_profile, profile_name)
    saved_path = ""
    status = "not_saved"
    if save_now:
        PROFILE_DIR.mkdir(parents=True, exist_ok=True)
        path = profile_path(profile["profile_name"])
        path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
        saved_path = str(path)
        status = "saved"
    return {
        "profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
        "profile_name": profile["profile_name"],
        "descriptor": profile["descriptor"],
        "saved_path": saved_path,
        "status": status,
    }


def save_character_profile_payload(profile_name: str = "", profile_json: str | dict[str, Any] | None = "") -> dict[str, str]:
    raw_profile = load_json_object(profile_json, "profile_json")
    if not raw_profile:
        raise ValueError("No cached character profile is available to save.")
    profile = normalize_character_profile(raw_profile, profile_name or str(raw_profile.get("profile_name") or ""))
    PROFILE_DIR.mkdir(parents=True, exist_ok=True)
    path = profile_path(profile["profile_name"])
    path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    return {
        "profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
        "profile_name": profile["profile_name"],
        "descriptor": profile["descriptor"],
        "saved_path": str(path),
        "status": "saved",
    }


def empty_profile_result(status: str = "empty") -> dict[str, str]:
    return {
        "profile_json": "",
        "profile_name": "",
        "descriptor": "",
        "saved_path": "",
        "status": status,
    }


def apply_character_profile_overrides(
    profile: dict[str, Any],
    override_subject_type: str = "",
    override_age: str = "",
    override_body: str = "",
    override_body_phrase: str = "",
    override_skin: str = "",
    override_hair: str = "",
    override_eyes: str = "",
    override_figure: str = "",
    override_descriptor_detail: str = "",
) -> dict[str, Any]:
    updated = dict(profile)
    subject_type = str(override_subject_type or "").strip()
    if subject_type in ("woman", "man"):
        updated["subject_type"] = subject_type
        updated["subject"] = subject_type
        updated["subject_phrase"] = subject_type
    for key, value in (
        ("age", override_age),
        ("body", override_body),
        ("body_phrase", override_body_phrase),
        ("skin", override_skin),
        ("hair", override_hair),
        ("eyes", override_eyes),
        ("figure", override_figure),
    ):
        text = str(value or "").strip()
        if text:
            updated[key] = text
    descriptor_detail = str(override_descriptor_detail or "").strip()
    if descriptor_detail and descriptor_detail != "keep_profile":
        updated["descriptor_detail"] = character_policy.normalize_descriptor_detail(descriptor_detail)
    if not str(updated.get("body_phrase") or "").strip():
        updated["body_phrase"] = body_phrase(updated.get("body"), updated.get("figure"))
    updated["descriptor"] = character_profile_descriptor(updated)
    return updated


def load_character_profile_json(
    profile_name: str = "",
    fallback_profile_json: str | dict[str, Any] | None = "",
    enabled: bool = True,
    delete_now: bool = False,
    rename_now: bool = False,
    rename_to: str = "",
    override_subject_type: str = "",
    override_age: str = "",
    override_body: str = "",
    override_body_phrase: str = "",
    override_skin: str = "",
    override_hair: str = "",
    override_eyes: str = "",
    override_figure: str = "",
    override_descriptor_detail: str = "",
) -> dict[str, str]:
    if not enabled:
        return empty_profile_result("disabled")
    if delete_now and rename_now:
        return empty_profile_result("choose_delete_or_rename")

    raw_profile = load_json_object(fallback_profile_json, "fallback_profile_json")
    saved_path = ""
    if profile_name and profile_name != "manual":
        path = profile_path(profile_name)
        if delete_now:
            if path.exists():
                path.unlink()
                return empty_profile_result(f"deleted:{path.stem}")
            return empty_profile_result(f"delete_missing:{safe_profile_name(profile_name)}")
        if rename_now:
            new_name = safe_profile_name(rename_to)
            if not rename_to.strip():
                return empty_profile_result("rename_missing_name")
            if not path.exists():
                return empty_profile_result(f"rename_missing:{safe_profile_name(profile_name)}")
            target = profile_path(new_name)
            if target.exists() and target != path:
                return empty_profile_result(f"rename_target_exists:{target.stem}")
            raw_profile = load_json_object(path.read_text(encoding="utf-8"), "character_profile")
            profile = normalize_character_profile(raw_profile, new_name)
            target.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
            if target != path:
                path.unlink()
            return {
                "profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
                "profile_name": profile["profile_name"],
                "descriptor": profile["descriptor"],
                "saved_path": str(target),
                "status": f"renamed:{path.stem}->{target.stem}",
            }
        if path.exists():
            raw_profile = load_json_object(path.read_text(encoding="utf-8"), "character_profile")
            saved_path = str(path)
    if not raw_profile:
        return empty_profile_result("empty")
    profile = normalize_character_profile(raw_profile, profile_name or raw_profile.get("profile_name", ""))
    profile = apply_character_profile_overrides(
        profile,
        override_subject_type=override_subject_type,
        override_age=override_age,
        override_body=override_body,
        override_body_phrase=override_body_phrase,
        override_skin=override_skin,
        override_hair=override_hair,
        override_eyes=override_eyes,
        override_figure=override_figure,
        override_descriptor_detail=override_descriptor_detail,
    )
    return {
        "profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
        "profile_name": profile["profile_name"],
        "descriptor": profile["descriptor"],
        "saved_path": saved_path,
        "status": "loaded" if saved_path else "fallback",
    }


def parse_character_profile(character_profile: str | dict[str, Any] | None) -> dict[str, Any]:
    raw = load_json_object(character_profile, "character_profile")
    if not raw:
        return {}
    if raw.get("profile_type") == "character" or any(key in raw for key in ("age", "age_band", "skin", "hair", "eyes")):
        return normalize_character_profile(raw, str(raw.get("profile_name") or ""))
    return {}


def apply_character_profile_to_context(
    context: dict[str, Any],
    character_profile: str | dict[str, Any] | None,
) -> tuple[dict[str, Any], dict[str, Any], str]:
    profile = parse_character_profile(character_profile)
    if not profile:
        return context, {}, "none"
    if context.get("subject_type") not in ("woman", "man"):
        return context, profile, "skipped_non_single_subject"
    if profile["subject_type"] != context.get("subject_type"):
        return context, profile, "skipped_subject_mismatch"
    updated = dict(context)
    for key in (
        "subject_type",
        "subject",
        "subject_phrase",
        "age",
        "body",
        "body_phrase",
        "skin",
        "hair",
        "eyes",
        "figure",
        "descriptor_detail",
    ):
        value = profile.get(key)
        if value:
            updated[key] = value
    updated["subject"] = profile["subject_type"]
    updated["subject_phrase"] = profile["subject_type"]
    return updated, profile, "applied"


_body_phrase = body_phrase
_safe_profile_name = safe_profile_name
_profile_path = profile_path
_load_json_object = load_json_object
_parse_character_manual_config = parse_character_manual_config
_character_manual_summary = character_manual_summary
_descriptor_detail_for_subject = descriptor_detail_for_subject
_descriptor_from_parts = descriptor_from_parts
_row_from_profile_metadata = row_from_profile_metadata
_character_profile_descriptor = character_profile_descriptor
_normalize_character_profile = normalize_character_profile
_empty_profile_result = empty_profile_result
_apply_character_profile_overrides = apply_character_profile_overrides
_parse_character_profile = parse_character_profile
_apply_character_profile_to_context = apply_character_profile_to_context
