from __future__ import annotations

import asyncio
import copy
import uuid
from datetime import datetime

import json

from fastapi import Request as FRequest
from fastapi.responses import JSONResponse


def register_conversation_routes(
    fastapi_app,
    delegate,
    schedule_save_chats_db,
    sync_save_chats_db,
    pretty_timestamp,
    log,
) -> None:
    def _emit_conversations_sidebar() -> None:
        try:
            delegate._push_event({"type": "conversations_changed"}, getattr(delegate, "_current_thread_id", "") or "")
            delegate._structured_bus.emit("__sidebar__", json.dumps({"type": "conversations_changed"}))
        except Exception:
            pass

    def _detach_tid_from_tabs(tid: str) -> None:
        for tab in delegate._tabs:
            tids = tab.get("thread_ids") or []
            if tid in tids:
                tids.remove(tid)
            if tab.get("current_thread_id") == tid:
                tab["current_thread_id"] = tids[0] if tids else ""

    def _bulk_purge_storage_blocking(tids: list[str]) -> None:
        from chat_storage import purge_thread_messages
        for tid in tids:
            try:
                purge_thread_messages(tid)
            except Exception:
                log.debug("purge_thread_messages failed for %s", tid, exc_info=True)

    async def permanently_delete_conversation_tid(
        tid_raw: str,
        *,
        emit_sidebar: bool = True,
        save_db: bool = True,
    ) -> JSONResponse:
        tid = tid_raw.strip()
        if not tid or tid not in delegate._conversation_meta:
            return JSONResponse(content={"error": "not found"}, status_code=404)
        delegate._conversation_meta.pop(tid, None)
        _detach_tid_from_tabs(tid)
        await asyncio.to_thread(_bulk_purge_storage_blocking, [tid])
        if save_db:
            await asyncio.to_thread(
                sync_save_chats_db,
                copy.deepcopy(delegate._tabs),
                copy.deepcopy(delegate._conversation_meta),
                getattr(delegate, "_live_view_event_bus", None),
            )
        if delegate._current_thread_id == tid:
            all_tids = [t for tab in delegate._tabs for t in (tab.get("thread_ids") or [])]
            delegate._current_thread_id = all_tids[0] if all_tids else ""
        if emit_sidebar:
            _emit_conversations_sidebar()
        return JSONResponse(content={"ok": True})

    @fastapi_app.delete("/conversations/trash")
    async def empty_trash_bin():
        meta = delegate._conversation_meta or {}
        targets = sorted(
            tid for tid, m in meta.items() if isinstance(m, dict) and m.get("trashed")
        )
        if not targets:
            return JSONResponse(content={"ok": True, "purged": 0})

        # Detach + drop meta in-memory first so a single chats_db save covers
        # every trashed thread.
        for tid in targets:
            delegate._conversation_meta.pop(tid, None)
            _detach_tid_from_tabs(tid)
        if delegate._current_thread_id in targets:
            all_tids = [t for tab in delegate._tabs for t in (tab.get("thread_ids") or [])]
            delegate._current_thread_id = all_tids[0] if all_tids else ""

        # All sqlite work happens in one worker thread so we don't pay the
        # asyncio context-switch cost per row, and the connection pool only
        # opens N connections instead of N * messages_per_thread.
        await asyncio.to_thread(_bulk_purge_storage_blocking, targets)

        # Single chats_db.json write covers the whole batch.
        await asyncio.to_thread(
            sync_save_chats_db,
            copy.deepcopy(delegate._tabs),
            copy.deepcopy(delegate._conversation_meta),
            getattr(delegate, "_live_view_event_bus", None),
        )
        _emit_conversations_sidebar()
        return JSONResponse(content={"ok": True, "purged": len(targets)})

    @fastapi_app.post("/conversations/{thread_id}/trash")
    async def move_conversation_to_trash(thread_id: str):
        tid = thread_id.strip()
        if not tid or tid not in delegate._conversation_meta:
            return JSONResponse(content={"error": "not found"}, status_code=404)
        delegate._conversation_meta[tid]["trashed"] = True
        schedule_save_chats_db(delegate)
        _emit_conversations_sidebar()
        return JSONResponse(content={"ok": True})

    @fastapi_app.post("/conversations/{thread_id}/restore")
    async def restore_conversation(thread_id: str):
        tid = thread_id.strip()
        if not tid or tid not in delegate._conversation_meta:
            return JSONResponse(content={"error": "not found"}, status_code=404)
        meta = delegate._conversation_meta[tid]
        if not isinstance(meta, dict):
            return JSONResponse(content={"error": "bad meta"}, status_code=500)
        meta["trashed"] = False
        delegate._conversation_meta[tid] = meta
        schedule_save_chats_db(delegate)
        _emit_conversations_sidebar()
        return JSONResponse(content={"ok": True})

    @fastapi_app.post("/conversations/new")
    async def new_conversation():
        new_tid = str(uuid.uuid4())
        tab = delegate._tabs[0] if delegate._tabs else None
        if tab is None:
            return JSONResponse(content={"error": "no tab"}, status_code=500)
        tab.setdefault("thread_ids", []).insert(0, new_tid)
        tab["current_thread_id"] = new_tid
        delegate._conversation_meta[new_tid] = {
            "title": pretty_timestamp(),
            "created_at": datetime.now().isoformat(),
            "saved_path": None,
            "folder_id": None,
            "pinned": False,
            "archived": False,
            "trashed": False,
            "sort_order": 0,
        }
        delegate._current_thread_id = new_tid
        schedule_save_chats_db(delegate)
        return JSONResponse(content={
            "thread_id": new_tid,
            "title": delegate._conversation_meta[new_tid]["title"],
            "created_at": delegate._conversation_meta[new_tid]["created_at"],
        })

    @fastapi_app.post("/conversations/ensure")
    async def ensure_conversation(request: FRequest):
        """Idempotent: create conversation metadata for a fixed thread_id (e.g. dashboard composer)."""
        try:
            body = await request.json()
        except Exception:
            return JSONResponse(content={"error": "invalid JSON"}, status_code=400)
        tid = str(body.get("thread_id") or "").strip()
        if not tid:
            return JSONResponse(content={"error": "thread_id required"}, status_code=400)
        title = str(body.get("title") or "").strip() or "Conversation"
        if tid in delegate._conversation_meta:
            return JSONResponse(content={"ok": True, "thread_id": tid, "existed": True})
        tab = delegate._tabs[0] if delegate._tabs else None
        if tab is None:
            return JSONResponse(content={"error": "no tab"}, status_code=500)
        tab.setdefault("thread_ids", [])
        if tid not in tab["thread_ids"]:
            tab["thread_ids"].insert(0, tid)
        delegate._conversation_meta[tid] = {
            "title": title,
            "created_at": datetime.now().isoformat(),
            "saved_path": None,
            "folder_id": None,
            "pinned": False,
            "archived": False,
            "trashed": False,
            "sort_order": 0,
        }
        schedule_save_chats_db(delegate)
        return JSONResponse(content={"ok": True, "thread_id": tid, "existed": False})

    @fastapi_app.delete("/conversations/{thread_id}")
    async def delete_conversation(thread_id: str):
        return await permanently_delete_conversation_tid(thread_id, emit_sidebar=True)

    @fastapi_app.get("/conversations/{thread_id}/branches")
    async def list_branches(thread_id: str):
        tid = thread_id.strip()
        if not tid or tid not in (delegate._conversation_meta or {}):
            return JSONResponse(content={"error": "not found"}, status_code=404)
        try:
            from chat_storage import get_branches_of
            branches = get_branches_of(tid)
            meta = delegate._conversation_meta or {}
            result = []
            for b in branches:
                bid = b.get("branch_thread_id", "")
                branch_at = b.get("branch_at_message_index", 0)
                m = meta.get(bid, {})
                result.append({
                    "thread_id": bid,
                    "title": m.get("title", "Untitled"),
                    "created_at": m.get("created_at", ""),
                    "branch_at_message_index": branch_at,
                })
            return JSONResponse(content=result)
        except Exception as exc:
            return JSONResponse(content={"error": str(exc)}, status_code=500)

    @fastapi_app.post("/conversations/branch")
    async def create_branch(request: FRequest):
        try:
            body = await request.json()
        except Exception:
            body = {}
        parent_tid = (body.get("parent_thread_id") or "").strip()
        branch_at = body.get("branch_at_message_index")
        if not parent_tid or parent_tid not in (delegate._conversation_meta or {}):
            return JSONResponse(content={"error": "parent not found"}, status_code=400)
        if not isinstance(branch_at, int) and not (isinstance(branch_at, float) and branch_at == int(branch_at)):
            return JSONResponse(content={"error": "branch_at_message_index required"}, status_code=400)
        branch_at = int(branch_at)
        try:
            from chat_storage import create_branch as _create_branch
            branch_tid = _create_branch(parent_tid, branch_at)
        except ValueError as e:
            return JSONResponse(content={"error": str(e)}, status_code=400)
        parent_title = (delegate._conversation_meta or {}).get(parent_tid, {}).get("title", "Untitled")
        default_title = f"{parent_title} (branch)"
        delegate._conversation_meta[branch_tid] = {
            "title": default_title,
            "created_at": datetime.now().isoformat(),
            "saved_path": None,
            "folder_id": None,
            "pinned": False,
            "archived": False,
            "trashed": False,
            "sort_order": 0,
        }
        tab = delegate._tabs[0] if delegate._tabs else None
        if tab:
            tab.setdefault("thread_ids", []).append(branch_tid)
        schedule_save_chats_db(delegate)
        return JSONResponse(content={
            "thread_id": branch_tid,
            "title": default_title,
            "created_at": delegate._conversation_meta[branch_tid]["created_at"],
        })

    @fastapi_app.post("/conversations/{thread_id}/rename")
    async def rename_conversation(thread_id: str, request: FRequest):
        tid = thread_id.strip()
        try:
            body = await request.json()
        except Exception:
            body = {}
        title = (body.get("title") or "").strip()
        if not tid or tid not in delegate._conversation_meta:
            return JSONResponse(content={"error": "not found"}, status_code=404)
        if title:
            delegate._conversation_meta[tid]["title"] = title
            schedule_save_chats_db(delegate)
        return JSONResponse(content={"ok": True, "title": delegate._conversation_meta[tid].get("title", "")})

    @fastapi_app.post("/conversations/reorder")
    async def reorder_conversations(request: FRequest):
        try:
            body = await request.json()
        except Exception:
            body = {}
        new_order = body.get("thread_ids", [])
        if not isinstance(new_order, list):
            return JSONResponse(content={"error": "invalid"}, status_code=400)
        if not delegate._tabs:
            return JSONResponse(content={"ok": True})

        # Reorder is REORDER, not delete. The pre-fix version assigned
        # `tabs[0]["thread_ids"] = list(new_order) + branches`, which meant a
        # client sending an empty/partial list (e.g. while `conversations` was
        # briefly empty during a UI race) silently wiped every non-branch thread
        # from the tab. That made every chat look like an "orphan" on the next
        # launch, and the repair routine then re-stubbed each one as "Untitled".
        # New behaviour: only reorder thread_ids that already live in this tab,
        # and never drop any — a missing thread_id keeps its place at the end.
        current = list(delegate._tabs[0].get("thread_ids") or [])
        if not current:
            return JSONResponse(content={"ok": True})
        current_set = set(current)
        sanitized: list[str] = []
        seen: set[str] = set()
        for tid in new_order:
            if isinstance(tid, str) and tid in current_set and tid not in seen:
                sanitized.append(tid)
                seen.add(tid)
        for tid in current:
            if tid not in seen:
                sanitized.append(tid)
                seen.add(tid)
        delegate._tabs[0]["thread_ids"] = sanitized
        for idx, tid in enumerate(sanitized):
            if tid in delegate._conversation_meta:
                meta = dict(delegate._conversation_meta.get(tid) or {})
                meta["sort_order"] = idx
                delegate._conversation_meta[tid] = meta
        schedule_save_chats_db(delegate)
        return JSONResponse(content={"ok": True})

