"""
Studio worldbook step LLM responder (R3/R4).

Assembles R2 context blocks + short step dialogue, calls LLM for structured JSON,
returns thinking, draft, questions, and evaluation.
"""
from __future__ import annotations

import json
import re
import uuid
from datetime import datetime
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage

from models.studio_models import (
    LastToolResponse,
    PromptBlock,
    StudioNode,
    StepMessage,
    ToolQuestionOption,
)
from services.studio_context_service import assemble_prompt_blocks
from utils.llm_client import LLMClient

_llm_client = LLMClient()

_JSON_FENCE = re.compile(r"```(?:json)?\s*([\s\S]*?)```", re.IGNORECASE)


def resolve_api_config(
    profile_id: Optional[str],
    api_config: Optional[Dict[str, str]],
) -> Dict[str, str]:
    """Merge frontend apiConfig with stored profile mainLLM key (same as chat WS)."""
    resolved = dict(api_config or {})
    if profile_id:
        try:
            try:
                from api.routes.apiConfigRoute import load_profile
            except ImportError:
                from backend.api.routes.apiConfigRoute import load_profile

            profile = load_profile(profile_id)
            if profile:
                main_llm = profile.get("apis", {}).get("mainLLM", {})
                if main_llm.get("apiUrl") and not resolved.get("api_url"):
                    resolved["api_url"] = main_llm.get("apiUrl", "")
                if main_llm.get("model") and not resolved.get("model"):
                    resolved["model"] = main_llm.get("model", "")
                api_key = main_llm.get("apiKey", "")
                if api_key:
                    resolved["api_key"] = api_key
        except Exception as exc:
            print(f"[StudioStepRespond] 加载 API 配置失败: {exc}")
    return resolved


def _blocks_to_context_text(blocks: List[PromptBlock]) -> str:
    sections: List[str] = []
    for block in blocks:
        sections.append(f"## {block.label}\n{block.content}")
    return "\n\n".join(sections)


def _build_system_prompt(node: StudioNode) -> str:
    insertion = (node.config or {}).get("insertion") or {}
    key = insertion.get("key") or "（未配置关键词）"
    comment = insertion.get("comment") or ""

    return f"""你是 Studio 创作助手，负责为当前流水线步骤生成或修订世界书条目草稿。

当前步骤：{node.displayName}
目标关键词：{key}
备注：{comment or "（无）"}

你必须只输出一个 JSON 对象（不要 markdown 代码块外的其他文字），字段如下：
{{
  "thinking": "你的内部思考过程（逐步推理，中文）",
  "currentProduct": "世界书条目正文（纯文本或 Markdown，可直接写入条目 content）",
  "questions": [
    {{
      "question": "需要用户澄清的问题",
      "options": ["选项A", "选项B", "选项C"]
    }}
  ],
  "evaluation": "对照评价维度的自检与优化建议（中文，面向用户）"
}}

规则：
1. currentProduct 必须是完整、可注入世界书的条目正文。
2. questions 为 0–3 条；每条至少 2 个 options；若无需澄清则 questions 为空数组。
3. evaluation 需引用上下文中的评价标准，给出具体、可操作的反馈。
4. 若用户要求修改，在 currentProduct 中输出修订后的完整条目，而非仅说明改了什么。
5. 全部字段使用中文（专有名词除外）。"""


def _dialogue_to_langchain(
    step_messages: List[StepMessage],
) -> List[Any]:
    messages: List[Any] = []
    for msg in step_messages:
        if msg.role == "user":
            messages.append(HumanMessage(content=msg.content))
        elif msg.role == "assistant":
            messages.append(AIMessage(content=msg.content))
    return messages


def _build_llm_messages(
    node: StudioNode,
    prompt_blocks: List[PromptBlock],
    step_messages: List[StepMessage],
    user_message: str,
) -> List[Any]:
    context_text = _blocks_to_context_text(prompt_blocks)
    system_prompt = _build_system_prompt(node)

    messages: List[Any] = [SystemMessage(content=system_prompt)]
    messages.append(
        HumanMessage(
            content=f"以下为当前步骤上下文（不含完整聊天历史）：\n\n{context_text}"
        )
    )
    messages.extend(_dialogue_to_langchain(step_messages))
    messages.append(HumanMessage(content=user_message))
    return messages


def _validate_api_config(api_config: Dict[str, str]) -> None:
    if not api_config.get("api_key"):
        raise ValueError("API Key 未配置，请先在 API 配置页面保存密钥")
    if not api_config.get("api_url"):
        raise ValueError("API 地址未配置，请先在 API 配置页面保存 mainLLM")


def _extract_json(raw: str) -> Dict[str, Any]:
    text = (raw or "").strip()
    if not text:
        raise ValueError("模型返回为空")

    fence = _JSON_FENCE.search(text)
    if fence:
        text = fence.group(1).strip()

    try:
        return json.loads(text)
    except json.JSONDecodeError:
        start = text.find("{")
        end = text.rfind("}")
        if start >= 0 and end > start:
            return json.loads(text[start : end + 1])
        raise ValueError("无法解析模型返回的 JSON")


def _decode_json_string_partial(raw: str) -> str:
    """Decode a possibly incomplete JSON string body (no surrounding quotes)."""
    out: List[str] = []
    i = 0
    while i < len(raw):
        if raw[i] == "\\" and i + 1 < len(raw):
            nxt = raw[i + 1]
            if nxt == "n":
                out.append("\n")
            elif nxt == "t":
                out.append("\t")
            elif nxt == "r":
                out.append("\r")
            elif nxt == '"':
                out.append('"')
            elif nxt == "\\":
                out.append("\\")
            elif nxt == "/":
                out.append("/")
            elif nxt == "u" and i + 5 < len(raw):
                try:
                    out.append(chr(int(raw[i + 2 : i + 6], 16)))
                    i += 6
                    continue
                except ValueError:
                    out.append(nxt)
            else:
                out.append(nxt)
            i += 2
        else:
            out.append(raw[i])
            i += 1
    return "".join(out)


def _extract_partial_thinking(raw: str) -> Optional[str]:
    """Best-effort extraction of thinking field from incomplete JSON stream."""
    marker = '"thinking"'
    idx = raw.find(marker)
    if idx < 0:
        return None

    colon = raw.find(":", idx + len(marker))
    if colon < 0:
        return None

    rest = raw[colon + 1 :].lstrip()
    if not rest.startswith('"'):
        return None

    body_start = 1
    i = body_start
    while i < len(rest):
        ch = rest[i]
        if ch == '"':
            break
        if ch == "\\":
            i += 2
            continue
        i += 1

    partial = rest[body_start:i]
    if not partial:
        return None
    return _decode_json_string_partial(partial)


def _normalize_draft(
    current_product: Any,
    node: StudioNode,
    existing_draft: Optional[Dict[str, Any]],
) -> Dict[str, Any]:
    draft: Dict[str, Any] = dict(existing_draft or {})
    insertion = (node.config or {}).get("insertion") or {}

    if isinstance(current_product, str):
        draft["entryContent"] = current_product.strip()
    elif isinstance(current_product, dict):
        draft.update(current_product)
        if "entryContent" not in draft and "content" in draft:
            draft["entryContent"] = draft["content"]
    else:
        draft["entryContent"] = str(current_product)

    if insertion.get("key"):
        draft["insertionKey"] = insertion["key"]
    if insertion.get("comment"):
        draft["insertionComment"] = insertion["comment"]
    draft["nodeId"] = node.id
    draft["displayName"] = node.displayName
    return draft


def _normalize_questions(raw: Any) -> List[ToolQuestionOption]:
    if not isinstance(raw, list):
        return []
    result: List[ToolQuestionOption] = []
    for item in raw:
        if not isinstance(item, dict):
            continue
        question = (item.get("question") or "").strip()
        if not question:
            continue
        options = [
            str(o).strip()
            for o in (item.get("options") or [])
            if str(o).strip()
        ]
        if len(options) < 2:
            continue
        result.append(ToolQuestionOption(question=question, options=options))
    return result[:3]


def _assistant_message_text(parsed: Dict[str, Any]) -> str:
    evaluation = (parsed.get("evaluation") or "").strip()
    if evaluation:
        return evaluation
    product = parsed.get("currentProduct")
    if isinstance(product, str) and product.strip():
        preview = product.strip()
        if len(preview) > 400:
            preview = preview[:400] + "…"
        return f"已更新条目草稿：\n\n{preview}"
    return "已处理您的消息，请查看左侧目前产物。"


def _build_turn_result(
    parsed: Dict[str, Any],
    *,
    node: StudioNode,
    user_message: str,
    existing_draft: Optional[Dict[str, Any]],
) -> Tuple[Dict[str, Any], LastToolResponse, StepMessage, StepMessage]:
    now = datetime.now().isoformat()

    last_draft = _normalize_draft(
        parsed.get("currentProduct"),
        node,
        existing_draft,
    )
    last_tool_response = LastToolResponse(
        thinking=(parsed.get("thinking") or "").strip() or None,
        evaluation=(parsed.get("evaluation") or "").strip() or None,
        questions=_normalize_questions(parsed.get("questions")),
        generatedAt=now,
    )

    user_step_msg = StepMessage(
        id=f"msg-{uuid.uuid4().hex[:12]}",
        role="user",
        content=user_message,
        createdAt=now,
    )
    assistant_step_msg = StepMessage(
        id=f"msg-{uuid.uuid4().hex[:12]}",
        role="assistant",
        content=_assistant_message_text(parsed),
        createdAt=now,
    )

    return last_draft, last_tool_response, user_step_msg, assistant_step_msg


async def studio_step_respond(
    *,
    node: StudioNode,
    prompt_blocks: List[PromptBlock],
    step_messages: List[StepMessage],
    user_message: str,
    existing_draft: Optional[Dict[str, Any]],
    api_config: Dict[str, str],
    stream: bool = False,
) -> Tuple[Dict[str, Any], LastToolResponse, StepMessage, StepMessage]:
    """
    Execute one worldbook step turn (non-streaming).

    Returns (last_draft, last_tool_response, user_step_msg, assistant_step_msg).
    """
    _validate_api_config(api_config)
    messages = _build_llm_messages(node, prompt_blocks, step_messages, user_message)
    model = api_config.get("model") or "gpt-4o-mini"

    if stream:
        print("[StudioStepRespond] stream=True 应使用 studio_step_respond_stream")

    response = await _llm_client.chat_completion(
        messages=messages,
        api_url=api_config.get("api_url", ""),
        api_key=api_config.get("api_key", ""),
        model=model,
        temperature=0.7,
        max_tokens=8000,
        request_timeout=120,
        stream=False,
    )

    raw_content = ""
    if isinstance(response, dict):
        raw_content = (
            response.get("choices", [{}])[0]
            .get("message", {})
            .get("content", "")
        )
    else:
        raw_content = str(response)

    parsed = _extract_json(raw_content)
    return _build_turn_result(
        parsed,
        node=node,
        user_message=user_message,
        existing_draft=existing_draft,
    )


async def studio_step_respond_stream(
    *,
    node: StudioNode,
    prompt_blocks: List[PromptBlock],
    step_messages: List[StepMessage],
    user_message: str,
    existing_draft: Optional[Dict[str, Any]],
    api_config: Dict[str, str],
) -> AsyncGenerator[Dict[str, Any], None]:
    """
    Stream thinking field while LLM generates structured JSON (R4).

    Yields:
      - {"type": "thinking_delta", "content": "..."}
      - {"type": "complete", "last_draft", "last_tool_response", "user_msg", "assistant_msg"}
    """
    _validate_api_config(api_config)
    messages = _build_llm_messages(node, prompt_blocks, step_messages, user_message)
    model = api_config.get("model") or "gpt-4o-mini"

    accumulated = ""
    last_thinking = ""

    async for chunk in _llm_client.stream_chat(
        messages=messages,
        api_url=api_config.get("api_url", ""),
        api_key=api_config.get("api_key", ""),
        model=model,
        temperature=0.7,
        max_tokens=8000,
        request_timeout=120,
    ):
        if chunk.get("type") != "chunk":
            continue
        accumulated += chunk.get("content") or ""
        partial = _extract_partial_thinking(accumulated)
        if partial and partial != last_thinking:
            last_thinking = partial
            yield {"type": "thinking_delta", "content": partial}

    parsed = _extract_json(accumulated)
    last_draft, last_tool_response, user_msg, assistant_msg = _build_turn_result(
        parsed,
        node=node,
        user_message=user_message,
        existing_draft=existing_draft,
    )

    yield {
        "type": "complete",
        "last_draft": last_draft,
        "last_tool_response": last_tool_response.model_dump(mode="json"),
        "user_msg": user_msg.model_dump(mode="json"),
        "assistant_msg": assistant_msg.model_dump(mode="json"),
    }
