import json

import aiosqlite
from database.db import DB_PATH


async def get_or_create_session(session_id: str, persona_id: str | None = None) -> dict:
    """Existing sessions keep their persona_id; persona_id applies only on INSERT."""
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT * FROM sessions WHERE session_id = ?", (session_id,)
        ) as cursor:
            row = await cursor.fetchone()

        if row:
            return dict(row)

        pid = (persona_id or "default").strip() or "default"
        await db.execute(
            "INSERT INTO sessions (session_id, persona_id) VALUES (?, ?)",
            (session_id, pid),
        )
        await db.commit()

        async with db.execute(
            "SELECT * FROM sessions WHERE session_id = ?", (session_id,)
        ) as cursor:
            row = await cursor.fetchone()
        return dict(row)


async def get_all_sessions() -> list:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT * FROM sessions ORDER BY updated_at DESC"
        ) as cursor:
            rows = await cursor.fetchall()
    return [dict(r) for r in rows]


async def get_session(session_id: str) -> dict | None:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT * FROM sessions WHERE session_id = ?",
            (session_id,),
        ) as cursor:
            row = await cursor.fetchone()
    return dict(row) if row else None


async def update_session_title(session_id: str, title: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (title, session_id),
        )
        await db.commit()


async def update_session_persona(session_id: str, persona_id: str):
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT persona_id FROM sessions WHERE session_id = ?",
            (session_id,),
        ) as cur:
            row = await cur.fetchone()
        prev = row["persona_id"] if row else None

        await db.execute(
            "UPDATE sessions SET persona_id = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (persona_id, session_id),
        )

        if prev is not None and prev != persona_id:
            await _reset_persona_bound_state(db, session_id)
        await db.commit()


async def _reset_persona_bound_state(db: aiosqlite.Connection, session_id: str) -> None:
    from services.rpg_state import DEFAULT_NARRATIVE_STATS

    stats_default = json.dumps(DEFAULT_NARRATIVE_STATS, ensure_ascii=False)
    await db.execute(
        """UPDATE sessions
           SET facts_json = '[]',
               global_plot = '',
               status_quo = '',
               plot_arc_json = '{}',
               outfit_json = '[]',
               affinity = 0,
               scene_json = '{}',
               narrative_stats_json = ?
           WHERE session_id = ?""",
        (stats_default, session_id),
    )
    await db.execute("DELETE FROM action_resolutions WHERE session_id = ?", (session_id,))
    await db.execute("DELETE FROM rpg_quests WHERE session_id = ?", (session_id,))


async def upsert_static_system_message(
    session_id: str, static_prompt: str, history: list | None = None
) -> bool:
    """Store only static persona prompt in messages. Returns True if written."""
    hist = history if history is not None else await get_history(session_id)
    async with aiosqlite.connect(DB_PATH) as db:
        if not hist:
            await db.execute(
                """INSERT INTO messages (session_id, role, content, image_prompt, image_path)
                   VALUES (?, 'system', ?, NULL, NULL)""",
                (session_id, static_prompt),
            )
            await db.execute(
                "UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
                (session_id,),
            )
            await db.commit()
            return True

        if hist[0]["role"] == "system":
            if hist[0]["content"] == static_prompt:
                return False
            await db.execute(
                """UPDATE messages SET content = ?
                   WHERE session_id = ? AND role = 'system'
                   AND id = (SELECT MIN(id) FROM messages WHERE session_id = ?)""",
                (static_prompt, session_id, session_id),
            )
            await db.commit()
            return True

        await db.execute(
            """INSERT INTO messages (session_id, role, content, image_prompt, image_path)
               VALUES (?, 'system', ?, NULL, NULL)""",
            (session_id, static_prompt),
        )
        await db.commit()
        return True


async def delete_dialog_messages(session_id: str) -> None:
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "DELETE FROM messages WHERE session_id = ? AND role IN ('user', 'assistant')",
            (session_id,),
        )
        await db.commit()


async def rebind_session_persona(
    session_id: str,
    persona_id: str,
    *,
    clear_history: bool = False,
    static_prompt: str,
    first_mes: str | None = None,
) -> None:
    session = await get_session(session_id)
    if not session:
        raise ValueError("Session not found")

    await update_session_persona(session_id, persona_id)
    if clear_history:
        await delete_dialog_messages(session_id)

    history = await get_history(session_id)
    await upsert_static_system_message(session_id, static_prompt, history)

    if clear_history and first_mes and first_mes.strip():
        await add_message(session_id, "assistant", first_mes.strip())


async def update_session_rpg(session_id: str, rpg_enabled: bool):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET rpg_enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (1 if rpg_enabled else 0, session_id),
        )
        await db.commit()


async def update_session_facts(session_id: str, facts_json: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET facts_json = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (facts_json, session_id),
        )
        await db.commit()


async def update_session_global_plot(session_id: str, global_plot: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET global_plot = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (global_plot, session_id),
        )
        await db.commit()


async def update_session_status_quo(session_id: str, status_quo: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET status_quo = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (status_quo, session_id),
        )
        await db.commit()


async def update_session_plot_arc(session_id: str, plot_arc_json: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET plot_arc_json = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (plot_arc_json, session_id),
        )
        await db.commit()


async def add_action_resolution(
    session_id: str,
    intent_text: str,
    roll: int,
    outcome: str,
    resolution_text: str,
    message_id: int | None = None,
):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            """INSERT INTO action_resolutions
               (session_id, message_id, intent_text, roll, outcome, resolution_text)
               VALUES (?, ?, ?, ?, ?, ?)""",
            (session_id, message_id, intent_text, roll, outcome, resolution_text),
        )
        await db.commit()


async def get_last_action_resolution(session_id: str) -> dict | None:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            """SELECT * FROM action_resolutions
               WHERE session_id = ?
               ORDER BY id DESC LIMIT 1""",
            (session_id,),
        ) as cur:
            row = await cur.fetchone()
    return dict(row) if row else None


async def delete_session(session_id: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute("DELETE FROM messages WHERE session_id = ?", (session_id,))
        await db.execute("DELETE FROM rpg_quests WHERE session_id = ?", (session_id,))
        await db.execute("DELETE FROM action_resolutions WHERE session_id = ?", (session_id,))
        await db.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
        await db.commit()


async def get_action_resolutions_map(session_id: str) -> dict[int, dict]:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            """SELECT message_id, intent_text, roll, outcome, resolution_text
               FROM action_resolutions
               WHERE session_id = ? AND message_id IS NOT NULL
               ORDER BY id""",
            (session_id,),
        ) as cur:
            rows = await cur.fetchall()
    out: dict[int, dict] = {}
    for r in rows:
        mid = r["message_id"]
        if mid is not None:
            out[int(mid)] = {
                "intent_text": r["intent_text"],
                "roll": r["roll"],
                "outcome": r["outcome"],
                "resolution_text": r["resolution_text"],
            }
    return out


def narrator_message_content(narrator: dict) -> str:
    return json.dumps(
        {
            "roll": narrator.get("roll"),
            "outcome": narrator.get("outcome"),
            "text": narrator.get("text", ""),
            "original_intent": narrator.get("original_intent"),
        },
        ensure_ascii=False,
    )


def parse_narrator_message(content: str) -> dict | None:
    try:
        data = json.loads(content or "{}")
    except (json.JSONDecodeError, TypeError):
        return None
    if not isinstance(data, dict) or not (data.get("text") or "").strip():
        return None
    return data


async def seed_quests_from_arc(session_id: str, arc: dict) -> int:
    """Create active quests for arc beats that are not already in rpg_quests."""
    if not arc:
        return 0
    existing = {q["title"] for q in await get_quests(session_id)}
    added = 0
    for beat in arc.get("beats", []):
        title = (beat.get("title") or beat.get("injection", "")).strip()[:120]
        if title and title not in existing:
            await upsert_quest(session_id, title, "active")
            existing.add(title)
            added += 1
    return added


async def get_history(session_id: str) -> list:
    resolutions = await get_action_resolutions_map(session_id)
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            """SELECT id, role, content, image_prompt, image_path,
                      image_prompt_alt, image_path_alt, choices_json
               FROM messages WHERE session_id = ? ORDER BY id""",
            (session_id,),
        ) as cursor:
            rows = await cursor.fetchall()
    result = []
    for idx, r in enumerate(rows):
        item = {
            "id": r["id"],
            "role": r["role"],
            "content": r["content"],
            "image_prompt": r["image_prompt"],
            "image_path": r["image_path"],
            "image_prompt_alt": r["image_prompt_alt"],
            "image_path_alt": r["image_path_alt"],
            "choices_json": r["choices_json"],
        }
        if r["role"] == "user" and r["id"] in resolutions:
            item["action_resolution"] = resolutions[r["id"]]
        result.append(item)
        if r["role"] == "user" and r["id"] in resolutions:
            nxt = rows[idx + 1] if idx + 1 < len(rows) else None
            if not nxt or nxt["role"] != "narrator":
                res = resolutions[r["id"]]
                result.append(
                    {
                        "id": -int(r["id"]),
                        "role": "narrator",
                        "content": narrator_message_content(
                            {
                                "roll": res.get("roll"),
                                "outcome": res.get("outcome"),
                                "text": res.get("resolution_text", ""),
                            }
                        ),
                        "image_prompt": None,
                        "image_path": None,
                        "image_prompt_alt": None,
                        "image_path_alt": None,
                        "choices_json": None,
                    }
                )
    return result


async def get_message(message_id: int) -> dict | None:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT id, session_id, role, content FROM messages WHERE id = ?",
            (message_id,),
        ) as cursor:
            row = await cursor.fetchone()
    return dict(row) if row else None


async def update_message_content(message_id: int, content: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE messages SET content = ? WHERE id = ?",
            (content, message_id),
        )
        await db.commit()


async def delete_messages_after(session_id: str, message_id: int):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "DELETE FROM messages WHERE session_id = ? AND id > ?",
            (session_id, message_id),
        )
        await db.execute(
            "UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (session_id,),
        )
        await db.commit()
    await restore_session_to_message(session_id, message_id)


async def delete_message(message_id: int):
    msg = await get_message(message_id)
    if not msg:
        return
    session_id = msg["session_id"]
    anchor = await get_max_message_id_before(session_id, message_id)
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute("DELETE FROM messages WHERE id = ?", (message_id,))
        await db.execute(
            "UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (session_id,),
        )
        await db.commit()
    await restore_session_to_message(session_id, anchor)


async def delete_message_and_following(session_id: str, message_id: int) -> bool:
    anchor = await get_max_message_id_before(session_id, message_id)
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "DELETE FROM messages WHERE session_id = ? AND id >= ?",
            (session_id, message_id),
        )
        await db.execute(
            "UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (session_id,),
        )
        await db.commit()
    await restore_session_to_message(session_id, anchor)
    return True


async def update_message_choices(message_id: int, choices_json: str | None):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE messages SET choices_json = ? WHERE id = ?",
            (choices_json, message_id),
        )
        await db.commit()


async def clear_choices_for_session(session_id: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE messages SET choices_json = NULL WHERE session_id = ?",
            (session_id,),
        )
        await db.commit()


async def get_last_message_preview(session_id: str, max_len: int = 80) -> str:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            """SELECT content, role FROM messages
               WHERE session_id = ? AND role IN ('user', 'assistant')
               ORDER BY id DESC LIMIT 1""",
            (session_id,),
        ) as cursor:
            row = await cursor.fetchone()
    if not row:
        return ""
    prefix = "Вы: " if row["role"] == "user" else "AI: "
    text = (row["content"] or "").replace("\n", " ").strip()
    if len(text) > max_len:
        text = text[:max_len] + "…"
    return prefix + text


async def get_max_message_id_before(session_id: str, message_id: int) -> int | None:
    async with aiosqlite.connect(DB_PATH) as db:
        async with db.execute(
            "SELECT MAX(id) FROM messages WHERE session_id = ? AND id < ?",
            (session_id, message_id),
        ) as cur:
            row = await cur.fetchone()
    if not row or row[0] is None:
        return None
    return int(row[0])


async def _collect_action_resolutions(session_id: str) -> list[dict]:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            """SELECT message_id, intent_text, roll, outcome, resolution_text
               FROM action_resolutions
               WHERE session_id = ?
               ORDER BY id""",
            (session_id,),
        ) as cur:
            rows = await cur.fetchall()
    return [
        {
            "message_id": r["message_id"],
            "intent_text": r["intent_text"],
            "roll": r["roll"],
            "outcome": r["outcome"],
            "resolution_text": r["resolution_text"],
        }
        for r in rows
    ]


async def save_state_snapshot(session_id: str, message_id: int) -> None:
    """Persist RPG session state as it exists right after this message."""
    session = await get_session(session_id)
    if not session:
        return
    quests = await get_quests(session_id)
    resolutions = await _collect_action_resolutions(session_id)
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            """INSERT INTO session_state_snapshots
               (message_id, session_id, facts_json, global_plot, status_quo,
                plot_arc_json, affinity, outfit_json, scene_json,
                narrative_stats_json, quests_json, action_resolutions_json)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
               ON CONFLICT(message_id) DO UPDATE SET
                 session_id = excluded.session_id,
                 facts_json = excluded.facts_json,
                 global_plot = excluded.global_plot,
                 status_quo = excluded.status_quo,
                 plot_arc_json = excluded.plot_arc_json,
                 affinity = excluded.affinity,
                 outfit_json = excluded.outfit_json,
                 scene_json = excluded.scene_json,
                 narrative_stats_json = excluded.narrative_stats_json,
                 quests_json = excluded.quests_json,
                 action_resolutions_json = excluded.action_resolutions_json""",
            (
                message_id,
                session_id,
                session.get("facts_json", "[]"),
                session.get("global_plot", ""),
                session.get("status_quo", ""),
                session.get("plot_arc_json", "{}"),
                int(session.get("affinity") or 0),
                session.get("outfit_json", "[]"),
                session.get("scene_json", "{}"),
                session.get("narrative_stats_json", '{"lust":0,"stamina":10,"tension":0}'),
                json.dumps(quests, ensure_ascii=False),
                json.dumps(resolutions, ensure_ascii=False),
            ),
        )
        await db.commit()


async def get_snapshot_at_or_before(session_id: str, message_id: int) -> dict | None:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            """SELECT * FROM session_state_snapshots
               WHERE session_id = ? AND message_id <= ?
               ORDER BY message_id DESC LIMIT 1""",
            (session_id, message_id),
        ) as cur:
            row = await cur.fetchone()
    return dict(row) if row else None


async def restore_session_from_snapshot(session_id: str, snapshot: dict) -> None:
    from services.rpg_state import DEFAULT_NARRATIVE_STATS

    stats_default = json.dumps(DEFAULT_NARRATIVE_STATS, ensure_ascii=False)
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            """UPDATE sessions SET
                 facts_json = ?,
                 global_plot = ?,
                 status_quo = ?,
                 plot_arc_json = ?,
                 affinity = ?,
                 outfit_json = ?,
                 scene_json = ?,
                 narrative_stats_json = ?,
                 updated_at = CURRENT_TIMESTAMP
               WHERE session_id = ?""",
            (
                snapshot.get("facts_json", "[]"),
                snapshot.get("global_plot", ""),
                snapshot.get("status_quo", ""),
                snapshot.get("plot_arc_json", "{}"),
                int(snapshot.get("affinity") or 0),
                snapshot.get("outfit_json", "[]"),
                snapshot.get("scene_json", "{}"),
                snapshot.get("narrative_stats_json") or stats_default,
                session_id,
            ),
        )
        await db.execute("DELETE FROM rpg_quests WHERE session_id = ?", (session_id,))
        try:
            quests = json.loads(snapshot.get("quests_json") or "[]")
        except (json.JSONDecodeError, TypeError):
            quests = []
        if isinstance(quests, list):
            for q in quests:
                if not isinstance(q, dict):
                    continue
                title = (q.get("title") or "").strip()
                if title:
                    await db.execute(
                        "INSERT INTO rpg_quests (session_id, title, status) VALUES (?, ?, ?)",
                        (session_id, title[:120], q.get("status", "active")),
                    )
        await db.execute("DELETE FROM action_resolutions WHERE session_id = ?", (session_id,))
        try:
            resolutions = json.loads(snapshot.get("action_resolutions_json") or "[]")
        except (json.JSONDecodeError, TypeError):
            resolutions = []
        if isinstance(resolutions, list):
            for r in resolutions:
                if not isinstance(r, dict):
                    continue
                await db.execute(
                    """INSERT INTO action_resolutions
                       (session_id, message_id, intent_text, roll, outcome, resolution_text)
                       VALUES (?, ?, ?, ?, ?, ?)""",
                    (
                        session_id,
                        r.get("message_id"),
                        r.get("intent_text", ""),
                        int(r.get("roll") or 0),
                        r.get("outcome", ""),
                        r.get("resolution_text", ""),
                    ),
                )
        await db.commit()


async def restore_session_to_message(session_id: str, anchor_message_id: int | None) -> bool:
    """Restore RPG state to snapshot at anchor (or nearest earlier message)."""
    if anchor_message_id is None:
        await _reset_persona_bound_state_only(session_id)
        return False
    snap = await get_snapshot_at_or_before(session_id, anchor_message_id)
    if not snap:
        return False
    await restore_session_from_snapshot(session_id, snap)
    return True


async def _reset_persona_bound_state_only(session_id: str) -> None:
    async with aiosqlite.connect(DB_PATH) as db:
        await _reset_persona_bound_state(db, session_id)
        await db.commit()


async def fork_session(source_session_id: str, until_message_id: int) -> str | None:
    source = await get_session(source_session_id)
    if not source:
        return None

    import uuid
    new_id = "sess_" + uuid.uuid4().hex[:8]

    snap = await get_snapshot_at_or_before(source_session_id, until_message_id)

    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            """INSERT INTO sessions
               (session_id, persona_id, title, rpg_enabled, facts_json, global_plot,
                status_quo, plot_arc_json, genre, rpg_settings_json, affinity,
                outfit_json, scene_json, narrative_stats_json)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (
                new_id,
                source["persona_id"],
                (source.get("title") or "Новый чат") + " (ветка)",
                source.get("rpg_enabled", 0),
                (snap or source).get("facts_json", "[]"),
                (snap or source).get("global_plot", ""),
                (snap or source).get("status_quo", ""),
                (snap or source).get("plot_arc_json", "{}"),
                source.get("genre", "adventure"),
                source.get("rpg_settings_json", "{}"),
                int((snap or source).get("affinity") or 0),
                (snap or source).get("outfit_json", "[]"),
                (snap or source).get("scene_json", "{}"),
                (snap or source).get(
                    "narrative_stats_json", '{"lust":0,"stamina":10,"tension":0}'
                ),
            ),
        )
        async with db.execute(
            """SELECT id, role, content, image_prompt, image_path,
                      image_prompt_alt, image_path_alt, choices_json
               FROM messages
               WHERE session_id = ? AND id <= ? ORDER BY id""",
            (source_session_id, until_message_id),
        ) as cur:
            rows = await cur.fetchall()
        id_map: dict[int, int] = {}
        for r in rows:
            cur_ins = await db.execute(
                """INSERT INTO messages
                   (session_id, role, content, image_prompt, image_path,
                    image_prompt_alt, image_path_alt, choices_json)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
                (new_id, r[1], r[2], r[3], r[4], r[5], r[6], r[7]),
            )
            id_map[int(r[0])] = int(cur_ins.lastrowid)
        if snap:
            try:
                quests = json.loads(snap.get("quests_json") or "[]")
            except (json.JSONDecodeError, TypeError):
                quests = []
            if isinstance(quests, list):
                for q in quests:
                    if isinstance(q, dict) and (q.get("title") or "").strip():
                        await db.execute(
                            "INSERT INTO rpg_quests (session_id, title, status) VALUES (?, ?, ?)",
                            (new_id, q["title"][:120], q.get("status", "active")),
                        )
            try:
                resolutions = json.loads(snap.get("action_resolutions_json") or "[]")
            except (json.JSONDecodeError, TypeError):
                resolutions = []
            if isinstance(resolutions, list):
                for res in resolutions:
                    if not isinstance(res, dict):
                        continue
                    old_mid = res.get("message_id")
                    new_mid = id_map.get(int(old_mid)) if old_mid is not None else None
                    if new_mid is None:
                        continue
                    await db.execute(
                        """INSERT INTO action_resolutions
                           (session_id, message_id, intent_text, roll, outcome, resolution_text)
                           VALUES (?, ?, ?, ?, ?, ?)""",
                        (
                            new_id,
                            new_mid,
                            res.get("intent_text", ""),
                            int(res.get("roll") or 0),
                            res.get("outcome", ""),
                            res.get("resolution_text", ""),
                        ),
                    )
            async with db.execute(
                """SELECT message_id, facts_json, global_plot, status_quo, plot_arc_json,
                          affinity, outfit_json, scene_json, narrative_stats_json,
                          quests_json, action_resolutions_json
                   FROM session_state_snapshots
                   WHERE session_id = ? AND message_id <= ?""",
                (source_session_id, until_message_id),
            ) as snap_cur:
                snap_rows = await snap_cur.fetchall()
            for srow in snap_rows:
                new_mid = id_map.get(int(srow[0]))
                if new_mid is None:
                    continue
                res_json = srow[10]
                try:
                    res_list = json.loads(res_json or "[]")
                except (json.JSONDecodeError, TypeError):
                    res_list = []
                if isinstance(res_list, list):
                    remapped = []
                    for res in res_list:
                        if not isinstance(res, dict):
                            continue
                        old_mid = res.get("message_id")
                        nm = id_map.get(int(old_mid)) if old_mid is not None else None
                        if nm is not None:
                            remapped.append({**res, "message_id": nm})
                    res_json = json.dumps(remapped, ensure_ascii=False)
                await db.execute(
                    """INSERT INTO session_state_snapshots
                       (message_id, session_id, facts_json, global_plot, status_quo,
                        plot_arc_json, affinity, outfit_json, scene_json,
                        narrative_stats_json, quests_json, action_resolutions_json)
                       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                    (new_mid, new_id, srow[1], srow[2], srow[3], srow[4], srow[5],
                     srow[6], srow[7], srow[8], srow[9], res_json),
                )
        await db.commit()
    return new_id


async def add_message(
    session_id: str,
    role: str,
    content: str,
    image_prompt: str | None = None,
    image_path: str | None = None,
) -> int:
    async with aiosqlite.connect(DB_PATH) as db:
        cur = await db.execute(
            """INSERT INTO messages (session_id, role, content, image_prompt, image_path)
               VALUES (?, ?, ?, ?, ?)""",
            (session_id, role, content, image_prompt, image_path),
        )
        msg_id = cur.lastrowid
        await db.execute(
            "UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (session_id,),
        )
        await db.commit()
    return msg_id


async def update_message_image(message_id: int, image_path: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE messages SET image_path = ? WHERE id = ?",
            (image_path, message_id),
        )
        await db.commit()


async def update_message_prompt(message_id: int, image_prompt: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE messages SET image_prompt = ? WHERE id = ?",
            (image_prompt, message_id),
        )
        await db.commit()


async def update_message_prompt_alt(message_id: int, image_prompt_alt: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE messages SET image_prompt_alt = ? WHERE id = ?",
            (image_prompt_alt, message_id),
        )
        await db.commit()


async def update_message_image_alt(message_id: int, image_path_alt: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE messages SET image_path_alt = ? WHERE id = ?",
            (image_path_alt, message_id),
        )
        await db.commit()


async def get_last_message_id(session_id: str) -> int | None:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT id FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT 1",
            (session_id,),
        ) as cursor:
            row = await cursor.fetchone()
    return int(row["id"]) if row else None


async def get_last_assistant_message_id(session_id: str) -> int | None:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            """SELECT id FROM messages
               WHERE session_id = ? AND role = 'assistant'
               ORDER BY id DESC LIMIT 1""",
            (session_id,),
        ) as cursor:
            row = await cursor.fetchone()
    return row["id"] if row else None


async def clear_history(session_id: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "DELETE FROM messages WHERE session_id = ?", (session_id,)
        )
        await db.commit()
    await _reset_persona_bound_state_only(session_id)


async def update_session_affinity(session_id: str, delta: int):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET affinity = affinity + ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (delta, session_id),
        )
        await db.commit()


async def set_session_affinity(session_id: str, value: int):
    """Debug / admin: set absolute affinity (-30..30)."""
    clamped = max(-30, min(30, int(value)))
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET affinity = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (clamped, session_id),
        )
        await db.commit()
    return clamped


async def update_session_genre(session_id: str, genre: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET genre = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (genre, session_id),
        )
        await db.commit()


async def update_session_rpg_settings(session_id: str, settings_json: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET rpg_settings_json = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (settings_json, session_id),
        )
        await db.commit()


async def update_session_outfit(session_id: str, outfit_json: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET outfit_json = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (outfit_json, session_id),
        )
        await db.commit()


async def update_session_scene(session_id: str, scene_json: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET scene_json = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (scene_json, session_id),
        )
        await db.commit()


async def update_session_narrative_stats(session_id: str, stats_json: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE sessions SET narrative_stats_json = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?",
            (stats_json, session_id),
        )
        await db.commit()


async def upsert_quest(session_id: str, title: str, status: str = "active"):
    async with aiosqlite.connect(DB_PATH) as db:
        async with db.execute(
            "SELECT id FROM rpg_quests WHERE session_id = ? AND title = ?",
            (session_id, title),
        ) as cur:
            row = await cur.fetchone()
        if row:
            await db.execute(
                "UPDATE rpg_quests SET status = ? WHERE id = ?",
                (status, row[0]),
            )
        else:
            await db.execute(
                "INSERT INTO rpg_quests (session_id, title, status) VALUES (?, ?, ?)",
                (session_id, title, status),
            )
        await db.commit()


async def get_quests(session_id: str) -> list:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT id, title, status FROM rpg_quests WHERE session_id = ? ORDER BY id",
            (session_id,),
        ) as cur:
            rows = await cur.fetchall()
    return [dict(r) for r in rows]


async def update_quest_status(session_id: str, title: str, status: str):
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute(
            "UPDATE rpg_quests SET status = ? WHERE session_id = ? AND title = ?",
            (status, session_id, title),
        )
        await db.commit()


async def update_quest_by_id(quest_id: int, session_id: str, status: str) -> bool:
    if status not in ("active", "done", "failed"):
        return False
    async with aiosqlite.connect(DB_PATH) as db:
        cur = await db.execute(
            "UPDATE rpg_quests SET status = ? WHERE id = ? AND session_id = ?",
            (status, quest_id, session_id),
        )
        await db.commit()
        return cur.rowcount > 0


async def get_message_count(session_id: str) -> int:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        async with db.execute(
            "SELECT COUNT(*) as cnt FROM messages WHERE session_id = ? AND role != 'system'",
            (session_id,),
        ) as cursor:
            row = await cursor.fetchone()
    return row["cnt"]
