from __future__ import annotations

import json
import re
from typing import Any


IMPORTANCE_SCORE = {
    "Critical": 100,
    "Hard Canon": 100,  # legacy compatibility
    "Important": 70,
    "Relationship": 70,  # legacy compatibility
    "Normal": 45,
    "Long-Term": 45,  # legacy compatibility
    "Minor": 20,
    "Short-Term": 20,  # legacy compatibility
}
RELEVANCE_SCORE = {"active": 40, "resolved": 10, "superseded": -35, "archived": -1000}
WORLD_SECTION_ORDER = [
    "history_backstory",
    "background_facts",
    "characters",
    "relationship_history",
    "core_canon",
    "locations",
    "secrets_knowledge",
    "groups_factions",
    "important_objects",
    "world_rules",
    "current_state",
    "physical_continuity",
    "story_timeline",
    "custom_entries",
]


def _compact(value: Any, limit: int = 3500) -> str:
    text = json.dumps(value, ensure_ascii=False, indent=2)
    return text if len(text) <= limit else text[:limit] + "\n…"


def _maybe_json(v: Any, fallback: Any) -> Any:
    if isinstance(v, (list, dict)):
        return v
    try:
        return json.loads(v or "")
    except Exception:
        return fallback


def _tokens(text: str) -> set[str]:
    return {
        x
        for x in re.findall(r"[a-z0-9][a-z0-9'’-]{2,}", str(text or "").casefold())
        if x not in {
            "the", "and", "that", "with", "this", "from", "have", "has", "had", "was", "were", "are",
            "for", "but", "not", "you", "your", "his", "her", "their", "they", "them", "she", "him", "who",
            "what", "when", "where", "then", "than", "into", "out", "about", "just", "like", "would", "could",
        }
    }


def _memory_search_text(m: dict[str, Any]) -> str:
    return " ".join(
        str(x or "")
        for x in (
            m.get("title"),
            m.get("what_happened") or m.get("content"),
            m.get("story_when"),
            m.get("story_where"),
            " ".join(_maybe_json(m.get("who_remembers"), [])),
            " ".join(_maybe_json(m.get("who_involved"), [])),
        )
    )


def select_memories_for_context(memories: list[dict[str, Any]], query_text: str = "", limit: int = 28) -> list[dict[str, Any]]:
    """Choose memories the RP should actually receive.

    Active/current memories are favored, Critical/Important memories outrank minor
    ones, and lexical overlap with the current turn helps pull older relevant
    memories back into context. Superseded memories remain historical evidence but
    are strongly deprioritized so they do not masquerade as current state.
    """
    q = _tokens(query_text)
    ranked: list[tuple[float, int, dict[str, Any]]] = []
    for idx, m in enumerate(memories or []):
        if not isinstance(m, dict) or not m.get("active", 1):
            continue
        relevance = str(m.get("current_relevance") or "active").lower()
        if relevance == "archived":
            continue
        importance = str(m.get("importance") or "Normal")
        score = float(IMPORTANCE_SCORE.get(importance, 35) + RELEVANCE_SCORE.get(relevance, 0))
        if q:
            overlap = len(q & _tokens(_memory_search_text(m)))
            score += min(30, overlap * 7)
        # Candidate rows arrive newest-first within similar priority buckets.
        score += max(0.0, 8.0 - idx * 0.08)
        ranked.append((score, -idx, m))
    ranked.sort(key=lambda x: (x[0], x[1]), reverse=True)
    return [x[2] for x in ranked[: max(1, int(limit))]]


def _world_item_text(item: Any) -> str:
    if isinstance(item, str):
        return item
    if not isinstance(item, dict):
        return str(item or "")
    parts: list[str] = []
    for k in (
        "name", "title", "fact", "event", "rule", "relationship", "status", "value", "details", "description",
        "what_happened", "where", "when", "people", "known_by", "hidden_from", "participants",
    ):
        v = item.get(k)
        if isinstance(v, list):
            parts.extend(str(x) for x in v)
        elif isinstance(v, (str, int, float)):
            parts.append(str(v))
    return " ".join(parts)


def relevant_world_bible(world: dict[str, Any], query_text: str = "", max_items: int = 42) -> dict[str, Any]:
    """Build a bounded World Bible view for the RP prompt without losing structure.

    Overview scalar fields are retained. Array sections are ranked so user-authored
    and query-relevant entries survive context limits, while provenance/revision
    metadata stays out of the model prompt unless it changes the actual fact.
    """
    if not isinstance(world, dict):
        return {}
    q = _tokens(query_text)
    out: dict[str, Any] = {}
    for key in ("name", "title", "description", "overview", "summary", "schema_version"):
        if key in world and world.get(key) not in (None, "", [], {}):
            out[key] = world.get(key)

    legacy_aliases = {
        "story_timeline": "timeline",
        "groups_factions": "organizations",
        "important_objects": "vehicles",
        "custom_entries": "pets",
    }
    pool: list[tuple[float, str, int, Any]] = []
    for section_index, section in enumerate(WORLD_SECTION_ORDER):
        values = list(world.get(section) or []) if isinstance(world.get(section), list) else []
        legacy_key = legacy_aliases.get(section)
        legacy_values = world.get(legacy_key) if legacy_key else None
        if isinstance(legacy_values, list):
            existing_text = {_world_item_text(x).casefold() for x in values if _world_item_text(x)}
            for raw in legacy_values:
                if _world_item_text(raw).casefold() not in existing_text:
                    values.append(raw)
        if not values:
            continue
        for i, item in enumerate(values):
            text = _world_item_text(item)
            score = 0.0
            if isinstance(item, dict):
                source = str(item.get("source") or "").casefold()
                role = str(item.get("role") or "").casefold()
                if source.startswith("user") or source.startswith("manual"):
                    score += 65
                elif source.startswith("scenario-template-linked-character"):
                    score += 58
                elif source.startswith("scenario-template"):
                    score += 46
                elif source.startswith("auto-rp"):
                    score += 18
                if section == "core_canon":
                    score += 65
                elif section == "world_rules":
                    score += 58
                elif section == "current_state":
                    score += 55
                elif section == "physical_continuity":
                    score += 52
                elif section == "secrets_knowledge":
                    score += 50
                elif section == "characters":
                    score += 38
                elif section == "relationship_history":
                    score += 34
                if "main character" in role:
                    score += 90
                if item.get("canon_importance") in {"Critical", "High", "Important"}:
                    score += 30
            if q:
                score += min(35, len(q & _tokens(text)) * 8)
            # keep some recency while avoiding total domination by newest items
            score += min(10, i * 0.15)
            score -= section_index * 0.02
            pool.append((score, section, i, item))

    pool.sort(key=lambda x: (x[0], x[2]), reverse=True)
    chosen = pool[:max_items]
    chosen_by_section: dict[str, list[tuple[int, Any]]] = {}
    for _, section, i, item in chosen:
        chosen_by_section.setdefault(section, []).append((i, item))
    for section in WORLD_SECTION_ORDER:
        if section not in chosen_by_section:
            continue
        # Present selected entries in original story/canon order after ranking chose them.
        items = [item for _, item in sorted(chosen_by_section[section], key=lambda x: x[0])]
        cleaned: list[Any] = []
        for item in items:
            if not isinstance(item, dict):
                cleaned.append(item)
                continue
            cleaned.append({
                k: v for k, v in item.items()
                if k not in {"revision_history", "provenance", "updated_at", "captured_at"}
            })
        out[section] = cleaned
    return out


def build_system_prompt(
    conversation: dict[str, Any],
    state: dict[str, Any],
    memories: list[dict[str, Any]],
    query_text: str = "",
) -> str:
    scene = state.get("scene", {}) if isinstance(state.get("scene"), dict) else {}
    retrieval_query = "\n".join(
        str(x or "") for x in (
            query_text, conversation.get("scenario_name"), conversation.get("persona_name"),
            scene.get("location"), " ".join(str(x) for x in (scene.get("present") or [])),
            " ".join(str(x) for x in (scene.get("offscreen") or [])),
        ) if x
    )
    selected_memories = [
        {
            "type": m.get("memory_type"),
            "title": m.get("title"),
            "what_happened": m.get("what_happened") or m.get("content"),
            "who_remembers": _maybe_json(m.get("who_remembers"), []),
            "who_was_involved": _maybe_json(m.get("who_involved"), []),
            "when": m.get("story_when") or "",
            "where": m.get("story_where") or "",
            "importance": m.get("importance"),
            "emotional_weight": m.get("emotional_weight") or "",
            "current_relevance": m.get("current_relevance") or "active",
            "source": m.get("source_kind") or "",
        }
        for m in select_memories_for_context(memories, retrieval_query, 28)
    ]
    bible_context = relevant_world_bible(state.get("world_bible", {}), retrieval_query, 42)
    # Serialize high-authority sections first so a very large Bible cannot use the
    # entire prompt budget on background/history before the model sees the main
    # Character dossier, hard rules, current state or knowledge boundaries.
    prompt_world_order = [
        "overview", "characters", "core_canon", "world_rules", "current_state",
        "physical_continuity", "secrets_knowledge", "relationship_history",
        "locations", "groups_factions", "important_objects", "history_backstory",
        "background_facts", "story_timeline", "custom_entries",
    ]
    bible_prompt_context: dict[str, Any] = {}
    for key in prompt_world_order:
        if key in bible_context:
            bible_prompt_context[key] = bible_context[key]
    for key, value in bible_context.items():
        if key not in bible_prompt_context:
            bible_prompt_context[key] = value
    return f"""You are the creative dialogue/narration model inside a persistent roleplay world engine.

CORE CONTRACT
- The world engine decides what is true. Do not overwrite structured canon with improvisation.
- The Scenario template, linked Character dossiers, World Bible and retrieved Memories below are live RP inputs, not decorative notes. Use them when deciding identity, voice, knowledge, relationships, continuity and how the world works.
- Linked Character definitions are authoritative for identity, appearance, personality, relationships, RP behavior, skills and lore. Preserve their specific voice instead of flattening them into a generic assistant.
- Scenario-seeded facts are starting canon for this RP. The RP may evolve independently afterward; current state overrides an older starting state while history remains history.
- Never write dialogue, thoughts, feelings, consent, choices, or major actions for the currently user-controlled character unless explicitly allowed.
- Present/offscreen cast matters: do not teleport important characters into a scene.
- Only characters who heard/know something may act on it. World knowledge is not universal character knowledge. Respect each memory's who_remembers list; an unlisted character does not automatically know that memory.
- World Bible secret entries may contain known_by / hidden_from. Respect those fields literally; a true world fact is not automatically known by every character.
- Current relationship state overrides obsolete historical labels. Historical states remain history.
- A superseded or resolved Memory is historical context, not automatically the current truth. Current structured state wins.
- Major irreversible events (death, pregnancy, marriage, divorce, cheating, serious injury, arrest, deployment, major move/job loss, etc.) require user approval; do not enact them autonomously.
- Long reply means a richer whole scene: narration, movement, environment, reactions, pacing, side-character activity and subtext. Do not pad with one character talking nonstop.
- Preserve character-specific voice. Dominant/stoic/military characters are full humans: they may joke, laugh, love, cry, be tender, or lose composure when context warrants. Dominance is not automatic cruelty.
- Conflict should be proportional and human: neither instant abuse nor perfect therapist dialogue.
- Avoid repetitive AIisms: pet-name spam, endless smirks/chuckles, 'you're mine' loops, jaw-clench loops, faux-profound speeches, therapy-speak, rhetorical-question spam, and repeating the user's words.
- Physical continuity matters. Do not invent outfit/object/location changes without an in-story transition.
- Story date/time in CURRENT SCENE is authoritative. The engine anchors the first sent message to the user-chosen start time and advances conservatively. Do not randomly freeze time or make large unrequested time jumps.
- Silence is valid in group scenes. Not every present NPC must speak.

OUTPUT FORMAT — HARD RULE
- Wrap narration, actions, internal scene description, body language, and environmental prose in single asterisks: *narration*.
- Put spoken dialogue in plain double quotation marks: \"Dialogue.\"
- Keep narration and dialogue distinct. A dialogue line must not be wrapped in asterisks.
- Never emit visible formatting labels such as Narration:, Assistant:, AI:, or the character name followed by a colon.
- Do not use Markdown headings, bullet lists, code fences, or bold **text** in an RP reply.
- The client renders the asterisks as italic narration; write the source format exactly even though the user will see clean styled prose.
- Do not write dialogue, thoughts, feelings, choices, consent, or actions for the user-controlled persona.

ACTIVE CHAT SETTINGS
Persona: {conversation.get('persona_name')}
Scenario: {conversation.get('scenario_name')}
Canon mode: {conversation.get('canon_mode')}
Timeline mode: {conversation.get('timeline_mode')}
Reply length: {conversation.get('reply_length')}
Character initiative: {conversation.get('initiative')}
Relationship realism: {conversation.get('relationship_realism')}

HARD CANON
{_compact(state.get('hard_canon', {}))}

RELATIONSHIP STATE
{_compact(state.get('relationship', {}))}

KNOWLEDGE / SECRETS
{_compact(state.get('knowledge', {}))}

CURRENT SCENE
{_compact(state.get('scene', {}))}

PHYSICAL CONTINUITY
{_compact(state.get('physical', {}))}

WORLD BIBLE / LINKED CHARACTER DOSSIERS (live structured reference)
{_compact(bible_prompt_context, 9000)}

RETRIEVED MEMORIES (live character-aware history)
{_compact(selected_memories, 7500)}

Write only the next scene beat. Preserve uncertainty where the world state is uncertain. Do not invent a missing hard fact merely to fill a blank.
"""


def recent_messages_for_model(messages: list[dict[str, Any]], max_messages: int = 28) -> list[dict[str, str]]:
    selected = [m for m in messages if m.get("is_selected", 1)]
    selected = selected[-max_messages:]
    out = []
    for m in selected:
        if m["role"] not in {"user", "assistant"}:
            continue
        content = m["content"]
        speaker = str(m.get("speaker_name") or "").strip()
        if speaker and speaker.lower() not in {"ai", "assistant", "imported-user"}:
            content = f"[{speaker}] {content}"
        out.append({"role": m["role"], "content": content})
    return out
