import json
import os
import random

from services.llm import LLMError, send_message_with_model
import logging

logger = logging.getLogger(__name__)

NARRATOR_MODEL = os.getenv("RPG_NARRATOR_MODEL", "").strip() or "deepseek/deepseek-chat-v3"

NARRATOR_PRE_SYSTEM = """You are the System/Narrator of an RPG chat.
Decide if the user's action requires a skill/ability check (physical action, persuasion, deception, stealth, combat, etc.).
Pure dialogue, questions, or passive observation do NOT require a check.
Return ONLY valid JSON (no markdown):
{
  "needs_check": true,
  "check_reason": "brief reason why a check is needed (e.g. 'jumping over a pit')",
  "directives": ["short imperative rules for the next character reply"],
  "resolution_text": "what actually happens as result of the action — written as narrator prose (1-2 sentences). Only if needs_check=true and roll/outcome provided.",
  "status_quo_update": "optional short update about the world state",
  "scene_update": {"place": "", "time_of_day": "", "day": "", "weather": "", "exits": [], "layout_note": ""}
}
scene_update: only include keys that changed (partial). Omit scene_update if nothing changed.
If needs_check=false: directives may still guide tone/pacing, resolution_text must be empty string.
If needs_check=true and roll/outcome are provided: resolution_text MUST reflect the outcome.
- critical failure (1): embarrassing or painful failure with extra complication
- failure (2-8): action fails, partial or no progress
- success (9-19): action succeeds as intended
- critical success (20): spectacular success with bonus effect"""

NARRATOR_POST_SYSTEM = """You are the System/Narrator of an RPG chat.
After the character replied, update persistent state.
Return ONLY valid JSON (no markdown):
{
  "status_quo_update": "what changed in the world/state (1-3 sentences)",
  "facts": ["durable fact strings OR {\"text\":\"...\",\"rp_day\":\"день 1\"}"],
  "choices": [{"id":"a","label":"..."}, ...],
  "affinity_delta": 0,
  "stats_delta": {"lust": 0, "stamina": 0, "tension": 0},
  "scene_update": {"place": "", "place_id": "", "time_of_day": "", "day": "", "weather": "", "exits": [], "layout_note": ""},
  "quest_updates": [{"title": "quest title", "status": "active|done|failed"}],
  "outfit_update": ["danbooru_tag", "danbooru_tag"],
  "step_complete": false,
  "step_completion_note": "optional 1 sentence when step_complete is true"
}
Rules:
- status_quo_update: internal DM state only (facts, location, mood). Never address the player, never use headers like "Status quo"/"Статус кво", P.S., or author commentary.
- affinity_delta: integer -2..+2. Positive if character warmed up to player, negative if pushed away. 0 if neutral.
- stats_delta: each lust/stamina/tension -2..+2 (0 if unchanged). lust=arousal, stamina=energy, tension=stress.
- scene_update: partial location/time schema; only keys that changed. Do not duplicate all of status_quo into scene_update.
- quest_updates: legacy; prefer step_complete for story progression. Empty array otherwise.
- step_complete: true ONLY when the CURRENT story step completion_criteria are clearly met. Do not rush.
- choices: 0-4 options for what the player can do next. REQUIRED when scripted beats are exhausted — never return an empty choices array unless the session truly ended.
- outfit_update: ONLY if clothing visibly changed. Use danbooru underscore_tags WITH COLOR when possible
  (e.g. white_tank_top, black_sports_shorts, gold_championship_belt, blue_jeans, red_ribbon).
  Every garment tag should include a color prefix unless the item is inherently colorless (barefoot, nude).
  Never bare generic tags like sports_shorts or torn_tank_top without a color. Empty array if no change."""


async def narrator_pre(
    persona_name: str,
    context: str,
    global_plot: str,
    facts_block: str,
    user_message: str,
    roll: int | None = None,
    outcome: str | None = None,
    extra_context: str = "",
    *,
    lang: str = "ru",
) -> dict:
    roll_block = f"Roll d20={roll}\nOutcome={outcome}\n\n" if roll is not None else ""
    user = (
        f"Persona: {persona_name}\n"
        f"{roll_block}"
        f"User action: {user_message}\n\n"
        f"Global plot:\n{global_plot}\n\n"
        f"Facts:\n{facts_block}\n\n"
        f"Recent context:\n{context}\n"
    )
    if extra_context:
        user += f"\n--- Session state ---\n{extra_context}\n---\n"
    from services.rpg_locale import locale_instruction

    try:
        raw = await send_message_with_model(
            [
                {
                    "role": "system",
                    "content": NARRATOR_PRE_SYSTEM + "\n" + locale_instruction(lang),
                },
                {"role": "user", "content": user},
            ],
            NARRATOR_MODEL,
        )
    except LLMError as e:
        logger.warning("Narrator-pre LLM failed (model=%s): %s", NARRATOR_MODEL, e)
        return {"needs_check": False, "directives": [], "status_quo_update": "", "resolution_text": "", "_ok": False}
    except Exception as e:
        logger.warning("Narrator-pre unexpected error: %s", e)
        return {"needs_check": False, "directives": [], "status_quo_update": "", "resolution_text": "", "_ok": False}

    cleaned = raw.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.split("\n", 1)[1] if "\n" in cleaned else cleaned
        if cleaned.endswith("```"):
            cleaned = cleaned.rsplit("```", 1)[0]
        cleaned = cleaned.strip()
    try:
        data = json.loads(cleaned)
        if isinstance(data, dict):
            data["_ok"] = True
            return data
    except Exception:
        logger.warning("Narrator-pre JSON parse failed. Raw=%.500s", raw)
    return {"needs_check": False, "directives": [], "status_quo_update": "", "resolution_text": "", "_ok": False}


async def narrator_post(
    persona_name: str,
    context: str,
    global_plot: str,
    facts_block: str,
    is_opening: bool = False,
    extra_context: str = "",
    *,
    lang: str = "ru",
) -> dict:
    opening_block = ""
    if is_opening:
        opening_block = (
            "\n\nOPENING SCENE: This is the first greeting, not a mid-conversation reply. "
            "Extract the character's INITIAL visible clothing from the greeting into outfit_update "
            "(danbooru underscore tags WITH color prefixes: white_shirt, black_shorts, gold_belt), "
            "even if clothing did not change during the scene. "
            "Set status_quo to describe the opening situation. "
            "Fill scene_update from greeting and scenario (place, time_of_day, day, layout_note). "
            "If the greeting shows clear warmth or hostility toward the player, set affinity_delta "
            "non-zero (-2..+2); use 0 only if truly neutral.\n"
        )
    user = (
        f"Persona: {persona_name}\n\n"
        f"Global plot:\n{global_plot}\n\n"
        f"Facts:\n{facts_block}\n\n"
        f"Recent context:\n{context}\n"
        f"{opening_block}"
    )
    if extra_context:
        user += f"\n--- Session state ---\n{extra_context}\n---\n"
    from services.rpg_locale import locale_instruction

    try:
        raw = await send_message_with_model(
            [
                {
                    "role": "system",
                    "content": NARRATOR_POST_SYSTEM + "\n" + locale_instruction(lang),
                },
                {"role": "user", "content": user},
            ],
            NARRATOR_MODEL,
        )
    except LLMError as e:
        logger.warning("Narrator-post LLM failed (model=%s): %s", NARRATOR_MODEL, e)
        return {"status_quo_update": "", "facts": [], "choices": [], "affinity_delta": 0, "quest_updates": [], "step_complete": False, "_ok": False}
    except Exception as e:
        logger.warning("Narrator-post unexpected error: %s", e)
        return {"status_quo_update": "", "facts": [], "choices": [], "affinity_delta": 0, "quest_updates": [], "step_complete": False, "_ok": False}

    cleaned = raw.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.split("\n", 1)[1] if "\n" in cleaned else cleaned
        if cleaned.endswith("```"):
            cleaned = cleaned.rsplit("```", 1)[0]
        cleaned = cleaned.strip()
    try:
        data = json.loads(cleaned)
        if isinstance(data, dict):
            data["_ok"] = True
            return data
    except Exception:
        logger.warning("Narrator-post JSON parse failed. Raw=%.500s", raw)
    return {"status_quo_update": "", "facts": [], "choices": [], "affinity_delta": 0, "quest_updates": [], "_ok": False}
