"""Prompt assembler -- builds system prompts for roleplay conversations.

Extracted from app/services/prompt_assembler.py.
No sqlalchemy/fastapi/redis imports allowed.

Assembly order (V2):
  1. [Platform rules]      <- system.yaml
  2. [Character settings]  <- description (V2 merged) / fallback to personality/background/appearance
  3. [Personality summary]  <- personality_summary (V2)
  4. [Speech patterns]      <- speech_patterns_v2[mood] / fallback to speech_patterns
  5. [Conversation examples] <- mes_example
  6. [Anti-patterns]        <- anti_patterns (V2)
  7. [Relationship status]  <- affection_overrides[level] / fallback to default behaviors
  8. [Reply mode]           <- chat_modes
  9. [Scene]                <- scenario (V2) / scene param
  10. [Lorebook entries]    <- character_book (V2)
  11. [Dynamic info]        <- memories/events/memos
  12. [post_history]        <- post_history_by_level[level] / fallback to post_history_instructions
"""
from __future__ import annotations

import logging
import pathlib
from datetime import datetime, timezone
from typing import Optional

import yaml

from kokoro_chat.attitude.levels import (
    AFFECTION_BEHAVIORS,
    DEFAULT_AFFECTION_LEVELS,
    get_affection_level,
)
from kokoro_chat.character.model import Character
from kokoro_chat.lorebook.engine import LorebookEngine, LorebookEntry
from kokoro_chat.memory.memo import STALE_DAYS, STALE_KEYS, _filter_stale_memos
from kokoro_chat.prompt.budget import (
    DEFAULT_MAX_MEMO_TOKENS,
    DEFAULT_MAX_MES_EXAMPLES,
    DEFAULT_MAX_ROLLING_SUMMARY_TOKENS,
    DEFAULT_SYSTEM_PROMPT_TOKEN_BUDGET,
    _estimate_tokens,
)
from kokoro_chat.prompt.sections import (
    _select_speech_pattern,
    build_core_character_sections,
)

logger = logging.getLogger(__name__)

# ---- Prompt YAML loading ----------------------------------------------------

_DEFAULT_PROMPTS_PATH = pathlib.Path(__file__).resolve().parent.parent.parent / "prompts" / "system.yaml"

_DEFAULT_PLATFORM_RULES = """你是一個角色扮演 AI。請嚴格遵守以下規則：
1. 始終保持角色設定，不要跳出角色
2. 用角色的語氣和風格說話
3. 根據當前的關係等級調整親密程度
4. 記住之前的對話內容和事件
5. 回覆時使用繁體中文"""

_DEFAULT_AFFECTION_BEHAVIORS = {
    "stranger": "保持禮貌但有距離感，不會主動表達情感，用敬語或較正式的方式說話。",
    "friend": "輕鬆友善，會開玩笑，偶爾關心對方，但不會有曖昧行為。",
    "crush": "開始有些害羞和在意，說話時偶爾會臉紅，會找理由接近對方，語氣變得溫柔。",
    "lover": "親密自然，會撒嬌、吃醋、表達想念，使用親暱稱呼，會有肢體接觸描寫。",
    "married": "極度親密，會用最親暱的方式說話，表現出深厚的默契和信任，偶爾會討論未來。",
}

_DEFAULT_CHAT_MODES = {
    "short": "回覆保持簡短，1-3 句話，像日常 LINE 對話。不需要場景描寫。",
    "long": "回覆 200 字以上，包含場景描寫、動作描寫（用 * 包裹）、心理活動。",
    "story": "以小說形式回覆，包含第三人稱敘事、環境描寫、NPC 互動。回覆 300 字以上。",
}


def _load_prompts(prompts_path: pathlib.Path | str | None = None) -> dict:
    """Load prompt templates from YAML, fallback to defaults on error."""
    path = pathlib.Path(prompts_path) if prompts_path else _DEFAULT_PROMPTS_PATH
    try:
        with open(path, encoding="utf-8") as f:
            data = yaml.safe_load(f)
            if isinstance(data, dict):
                logger.info("Loaded prompt templates from %s", path)
                return data
    except FileNotFoundError:
        logger.info("Prompt YAML not found at %s, using defaults", path)
    except Exception as e:
        logger.warning("Failed to load prompt YAML: %s, using defaults", e)
    return {}


_PROMPTS = _load_prompts()

PLATFORM_RULES: str = _PROMPTS.get("platform_rules", _DEFAULT_PLATFORM_RULES).rstrip()
PROMPT_AFFECTION_BEHAVIORS: dict[str, str] = _PROMPTS.get("affection_behaviors", _DEFAULT_AFFECTION_BEHAVIORS)
CHAT_MODES: dict[str, str] = _PROMPTS.get("chat_modes", _DEFAULT_CHAT_MODES)


# ---- Lorebook helpers -------------------------------------------------------

def _build_lorebook_engine(character_book: dict) -> LorebookEngine | None:
    """Parse character_book dict into a LorebookEngine. Returns None if empty."""
    if not character_book:
        return None
    raw_entries = character_book.get("entries", [])
    if not raw_entries:
        return None

    entries: list[LorebookEntry] = []
    for raw in raw_entries:
        entries.append(LorebookEntry(
            keys=raw.get("keys", []),
            secondary_keys=raw.get("secondary_keys", []),
            content=raw.get("content", ""),
            enabled=raw.get("enabled", True),
            constant=raw.get("constant", False),
            selective=raw.get("selective", False),
            priority=raw.get("priority", 5),
            position=raw.get("position", "after_char"),
            insertion_order=raw.get("insertion_order", 100),
        ))

    return LorebookEngine(
        entries=entries,
        scan_depth=character_book.get("scan_depth", 10),
        token_budget=character_book.get("token_budget", 500),
    )


def _select_post_history(
    by_level: dict[str, str],
    legacy_instructions: str,
    level: str,
) -> str:
    """Select post_history: V2 level-based > legacy static."""
    if by_level:
        text = by_level.get(level, "")
        return text.strip() if text else ""
    return legacy_instructions.strip() if legacy_instructions else ""


def _format_lorebook_section(triggered: list[LorebookEntry]) -> str:
    """Format triggered lorebook entries into a prompt section."""
    if not triggered:
        return ""
    lines = [e.content.strip() for e in triggered if e.content.strip()]
    if not lines:
        return ""
    return "[角色世界觀]\n" + "\n---\n".join(lines)


# ---- Secret unlocking -------------------------------------------------------

def _get_unlocked_secrets(secrets: list[dict], affection: int, trust: int) -> list[str]:
    """Return content of secrets whose conditions are met."""
    unlocked = []
    for s in secrets:
        cond = s.get("reveal_condition", {})
        min_aff = cond.get("min_affection", 0)
        min_trust = cond.get("min_trust", 0)
        if affection >= min_aff and trust >= min_trust:
            content = s.get("content", "")
            if content:
                unlocked.append(content)
    return unlocked


# ---- Memory formatting helpers ----------------------------------------------

def _format_memory_section(
    memories: list[dict],
    character_id: str,
    character_name_lookup: dict[str, str] | None = None,
) -> str:
    """Format memories into prompt text, splitting own vs room memories.

    When memories carry *source_character_id*, they are split into:
    - own memories (source matches character_id)
    - room memories (source differs), annotated with source character name.

    Falls back to a flat "[相關記憶]" section when no source info is present.
    """
    if not memories:
        return ""

    lookup = character_name_lookup or {}
    has_source = any("source_character_id" in m for m in memories)

    if not has_source or not character_id:
        mem_texts = [
            f"- {m['text']}（相關度：{m['relevance']:.0%}）"
            for m in memories
        ]
        return (
            "[相關記憶]\n以下是與當前話題相關的過往記憶：\n"
            + "\n".join(mem_texts)
        )

    own: list[str] = []
    room: list[str] = []

    for m in memories:
        source_id = m.get("source_character_id", character_id)
        if source_id == character_id:
            own.append(f"- {m['text']}（相關度：{m['relevance']:.0%}）")
        else:
            source_name = lookup.get(source_id, source_id)
            room.append(
                f"- {m['text']}（來源：{source_name}，相關度：{m['relevance']:.0%}）"
            )

    sections: list[str] = []
    if own:
        sections.append("[你對用戶的記憶]\n" + "\n".join(own))
    if room:
        sections.append("[同房間其他角色得知的資訊]\n" + "\n".join(room))

    return "\n\n".join(sections)


# ---- System prompt assembly -------------------------------------------------

def assemble_static_system_prompt(
    character: Character,
    chat_mode: str,
    scene: Optional[str] = None,
    current_mood: Optional[str] = None,
) -> str:
    """Assemble a *slim* system prompt containing only static context.

    Dynamic context (attitude, memories, events, memos, recent chat) is
    excluded because the LLM can fetch it on demand via tool calls.
    This dramatically reduces per-turn token usage.

    Included sections:
      - Platform rules
      - Character settings (V2 description or legacy fields)
      - Personality summary (V2)
      - Traits, speech patterns (V2 mood-based), mes_example
      - Anti-patterns (V2)
      - Chat mode instructions
      - Scene / scenario
      - Tool usage instructions
    """
    sections: list[str] = []

    # 1. Platform rules
    sections.append(f"[平台規則]\n{PLATFORM_RULES}")

    # 2-8. Core character sections (shared with assemble_system_prompt)
    sections.extend(build_core_character_sections(
        character, chat_mode, CHAT_MODES, scene, current_mood,
    ))

    # 9. Tool usage instructions (static-only)
    sections.append(
        "[動態資訊查詢]\n"
        "你可以使用以下工具來取得需要的資訊，不需要每次都查詢，根據對話需要決定：\n"
        "- get_attitude: 查詢你對用戶的好感度、信任度和當前心情，用於調整回覆的親密程度\n"
        "- search_memories: 搜尋過往記憶，當用戶提到之前的話題或你需要回憶相關事件時使用\n"
        "- get_active_events: 查詢目前進行中的重要事件\n"
        "- get_memos: 查詢用戶的個人資訊備忘（名字、喜好等）\n"
        "- get_recent_chat: 查詢最近的對話歷史\n"
        "請在回覆前先判斷是否需要查詢這些資訊，然後再以角色身份回覆。"
    )

    return "\n\n".join(sections)


def assemble_static_system_prompt_blocks(
    character: Character,
    chat_mode: str = "short",
    scene: Optional[str] = None,
    current_mood: Optional[str] = None,
) -> list[dict]:
    """Return static system prompt as Anthropic content blocks with cache_control."""
    text = assemble_static_system_prompt(character=character, chat_mode=chat_mode, scene=scene, current_mood=current_mood)
    return [{"type": "text", "text": text, "cache_control": {"type": "ephemeral"}}]


def assemble_system_prompt(
    character: Character,
    affection_score: int,
    chat_mode: str,
    events: list[dict],
    memories: list[dict],
    memos: list[dict],
    scene: Optional[str] = None,
    attitude_section: Optional[str] = None,
    rolling_summary: Optional[str] = None,
    trust_score: int = 50,
    story_context: Optional[str] = None,
    max_tokens: int = DEFAULT_SYSTEM_PROMPT_TOKEN_BUDGET,
    current_mood: Optional[str] = None,
    recent_messages: Optional[list[str]] = None,
    *,
    affection_levels: dict[str, tuple[int, int]] | None = None,
    max_memo_tokens: int = DEFAULT_MAX_MEMO_TOKENS,
    max_mes_examples: int = DEFAULT_MAX_MES_EXAMPLES,
    max_rolling_summary_tokens: int = DEFAULT_MAX_ROLLING_SUMMARY_TOKENS,
    character_name_lookup: dict[str, str] | None = None,
) -> str:
    """Assemble full system prompt with progressive degradation on budget overflow.

    V2 enhancements:
    - description replaces scattered personality/background/appearance
    - personality_summary standalone summary
    - speech_patterns_v2 supports mood-based selection
    - anti_patterns prevent character drift
    - affection_overrides character-specific affection behaviors
    - post_history_by_level dynamic post_history
    - character_book lorebook integration
    """

    # -- Core sections (never removed) ----------------------------------------

    core_sections: list[str] = []

    level = get_affection_level(affection_score, levels=affection_levels)

    # 1. Platform rules
    core_sections.append(f"[平台規則]\n{PLATFORM_RULES}")

    # 2-8. Core character sections (shared with assemble_static_system_prompt)
    core_sections.extend(build_core_character_sections(
        character, chat_mode, CHAT_MODES, scene, current_mood,
        max_mes_examples=max_mes_examples,
    ))

    # Unlocked secrets
    unlocked = _get_unlocked_secrets(character.secrets, affection_score, trust_score)
    if unlocked:
        secret_lines = [f"- {s}" for s in unlocked]
        core_sections.append(
            "[已解鎖的秘密]\n"
            "以下是角色願意透露的秘密，可以在對話中自然地提及或暗示：\n"
            + "\n".join(secret_lines)
        )

    # 7. Relationship status -- V2 uses affection_overrides, fallback to defaults
    level_names = {
        "stranger": "熟人", "friend": "朋友", "crush": "心動",
        "lover": "戀人", "married": "結婚",
    }

    if character.affection_overrides and level in character.affection_overrides:
        behavior_text = character.affection_overrides[level]
    else:
        behavior_text = PROMPT_AFFECTION_BEHAVIORS.get(level, "")

    rel_section = f"""[關係狀態]
關係等級：{level_names.get(level, level)}
行為指引：{behavior_text}"""

    if attitude_section:
        rel_section += f"\n\n[當前態度（動態）]\n{attitude_section}"

    core_sections.append(rel_section)

    # 10. Lorebook entries (V2) -- character_book
    lorebook_engine = _build_lorebook_engine(character.character_book)
    if lorebook_engine and recent_messages:
        triggered = lorebook_engine.scan(recent_messages)
        lorebook_section = _format_lorebook_section(triggered)
        if lorebook_section:
            core_sections.append(lorebook_section)

    # -- Degradable sections ---------------------------------------------------

    mem_items = list(memories)   # sorted by importance x relevance
    evt_items = list(events)     # chronological order, tail = oldest
    memo_items = _filter_stale_memos(memos)
    # Truncate rolling summary
    _raw_summary = rolling_summary or ""
    _max_summary_chars = max_rolling_summary_tokens * 2
    summary_text = _raw_summary[:_max_summary_chars] if len(_raw_summary) > _max_summary_chars else _raw_summary
    story_text = story_context or ""

    def _build_degradable() -> list[str]:
        parts: list[str] = []
        if evt_items:
            event_texts = []
            for e in evt_items:
                prefix = "【創作者事件】" if e.get("type") == "creator" else "【關係事件】"
                event_texts.append(f"{prefix} {e['content']}")
            parts.append("[重要事件]\n" + "\n".join(event_texts))
        if summary_text:
            parts.append(f"[之前的對話摘要]\n{summary_text}")
        if mem_items:
            mem_section = _format_memory_section(
                mem_items, character.id, character_name_lookup,
            )
            if mem_section:
                parts.append(mem_section)
        if memo_items:
            memo_texts = [f"- {m['key']}：{m['value']}" for m in memo_items]
            memo_block = "[用戶資訊備忘]\n" + "\n".join(memo_texts)
            parts.append(memo_block)
        if story_text:
            parts.append(f"[故事進度]\n{story_text}")
        return parts

    def _total_tokens(core: list[str], degradable: list[str]) -> int:
        joined = "\n\n".join(core + degradable)
        return _estimate_tokens(joined)

    # -- Progressive degradation loop -----------------------------------------

    degradable = _build_degradable()

    # 12. post_history -- V2 level-based, fallback to static
    post_history_text = _select_post_history(
        character.post_history_by_level,
        character.post_history_instructions,
        level,
    )

    if _total_tokens(core_sections, degradable) <= max_tokens:
        all_sections = core_sections + degradable
        if post_history_text:
            all_sections.append(f"[角色提醒]\n{post_history_text}")
        return "\n\n".join(all_sections)

    # Phase 1: remove memories (lowest relevance first = list tail)
    while mem_items and _total_tokens(core_sections, _build_degradable()) > max_tokens:
        mem_items.pop()

    # Phase 2: remove events (oldest first = list tail)
    while evt_items and _total_tokens(core_sections, _build_degradable()) > max_tokens:
        evt_items.pop()

    # Phase 3: truncate memos
    if memo_items and _total_tokens(core_sections, _build_degradable()) > max_tokens:
        max_memo_chars = max_memo_tokens * 2
        memo_block = "\n".join(f"- {m['key']}：{m['value']}" for m in memo_items)
        if len(memo_block) > max_memo_chars:
            memo_block = memo_block[:max_memo_chars]
            memo_items.clear()
            memo_items.append({"key": "（已截斷）", "value": memo_block})

    all_sections = core_sections + _build_degradable()
    if post_history_text:
        all_sections.append(f"[角色提醒]\n{post_history_text}")
    return "\n\n".join(all_sections)
