# utils/conversation_manager.py - 对话持久化管理器（集成Token统计）

import json
import os
import time
import tempfile
import threading
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
try:
    from config import DATA_DIR, HOST_WORKSPACES_FILE
except ImportError:
    import sys
    from pathlib import Path
    project_root = Path(__file__).resolve().parents[1]
    if str(project_root) not in sys.path:
        sys.path.insert(0, str(project_root))
    from config import DATA_DIR, HOST_WORKSPACES_FILE

try:
    from utils.perf_log import perf_log
except Exception:
    perf_log = None

@dataclass
class ConversationMetadata:
    """对话元数据"""
    id: str
    title: str
    created_at: str
    updated_at: str
    project_path: Optional[str]
    project_relative_path: Optional[str]
    thinking_mode: bool
    total_messages: int
    total_tools: int
    run_mode: str = "fast"
    model_key: Optional[str] = None
    has_images: bool = False
    has_videos: bool = False
    status: str = "active"  # active, archived, error


class IndexMixin:
    """ConversationManager index mixin 能力 mixin。"""

    def _migrate_legacy_conversation_files(self):
        """把 data/conversations/conv_*.json 按 metadata.project_path 移入对应 workspace 子目录。"""
        try:
            legacy_files = [
                p for p in self.conversations_root.glob("conv_*.json")
                if p.is_file() and p.parent == self.conversations_root
            ]
        except Exception:
            return

        moved_count = 0
        for file_path in legacy_files:
            try:
                data = json.loads(file_path.read_text(encoding="utf-8"))
                metadata = data.get("metadata", {}) if isinstance(data, dict) else {}
                project_path = self._normalize_path(metadata.get("project_path"))
                workspace_id = self._resolve_workspace_id(project_path)
                if not workspace_id:
                    continue
                target_dir = self._conversation_dir_for_workspace(workspace_id)
                target_dir.mkdir(parents=True, exist_ok=True)
                target = target_dir / file_path.name
                if target.exists():
                    print(f"⚠️ legacy 对话迁移跳过，目标已存在: {target}")
                    continue
                file_path.replace(target)
                moved_count += 1
            except Exception as exc:
                print(f"⚠️ legacy 对话迁移失败 {file_path.name}: {exc}")
        if moved_count:
            print(f"🔄 已迁移 {moved_count} 个 legacy 对话到工作区目录")

    def _iter_conversation_files(self, sort_by_mtime: bool = True):
        """遍历对话文件（排除索引文件），可按修改时间降序排序。"""
        files = []
        for p in self.conversations_dir.glob("*.json"):
            if p == self.index_file:
                continue
            stem = p.stem or ""
            # 跳过索引备份/损坏文件，避免被误当作对话文件参与重建
            if stem == "index" or stem.startswith("index_corrupt_"):
                continue
            # 仅纳入标准对话文件，避免其他 json 干扰索引
            if not stem.startswith("conv_"):
                continue
            files.append(p)
        if sort_by_mtime:
            files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
        return files

    def _atomic_write_json(self, target_file: Path, payload: Dict[str, Any]):
        """原子写入 JSON：使用唯一临时文件，避免并发写同名 .tmp 产生竞态。"""
        target_file.parent.mkdir(parents=True, exist_ok=True)
        temp_path: Optional[Path] = None
        try:
            with tempfile.NamedTemporaryFile(
                mode="w",
                encoding="utf-8",
                dir=str(target_file.parent),
                prefix=f".{target_file.name}.",
                suffix=".tmp",
                delete=False,
            ) as fh:
                temp_path = Path(fh.name)
                json.dump(payload, fh, ensure_ascii=False, indent=2)
                fh.flush()
                os.fsync(fh.fileno())
            os.replace(str(temp_path), str(target_file))
        finally:
            if temp_path and temp_path.exists():
                try:
                    temp_path.unlink()
                except Exception:
                    pass

    def _rebuild_index_from_files(self, max_count: Optional[int] = None) -> Dict:
        """
        从现有对话文件重建索引。

        Args:
            max_count: 限制重建的条目数（按文件修改时间倒序）；None 表示全量重建。
        """
        t0 = time.perf_counter()
        rebuilt_index: Dict[str, Dict] = {}
        files = self._iter_conversation_files(sort_by_mtime=True)
        if max_count is not None:
            files = files[:max(0, int(max_count))]
        if perf_log:
            perf_log("_rebuild_index_from_files start", extra={"file_count": len(files), "max_count": max_count})
        for file_path in files:
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    raw = f.read().strip()
                    if not raw:
                        continue
                    data = json.loads(raw)
            except Exception as exc:
                print(f"⚠️ 重建索引时跳过 {file_path.name}: {exc}")
                continue

            conv_id = data.get("id") or file_path.stem
            metadata = data.get("metadata", {}) or {}

            rebuilt_index[conv_id] = {
                "title": data.get("title") or "未命名对话",
                "created_at": data.get("created_at"),
                "updated_at": data.get("updated_at"),
                "project_path": metadata.get("project_path"),
                "project_relative_path": metadata.get("project_relative_path"),
                "thinking_mode": metadata.get("thinking_mode", False),
                "run_mode": metadata.get("run_mode") or ("thinking" if metadata.get("thinking_mode") else "fast"),
                "model_key": metadata.get("model_key"),
                "has_images": metadata.get("has_images", False),
                "has_videos": metadata.get("has_videos", False),
                "total_messages": metadata.get("total_messages", 0),
                "total_tools": metadata.get("total_tools", 0),
                "status": metadata.get("status", "active"),
                "multi_agent_mode": bool(metadata.get("multi_agent_mode", False)),
            }
        elapsed_ms = (time.perf_counter() - t0) * 1000
        if perf_log:
            perf_log("_rebuild_index_from_files done", elapsed_ms=elapsed_ms, extra={"rebuilt_size": len(rebuilt_index), "max_count": max_count})
        if rebuilt_index:
            print(f"🔄 已从对话文件重建索引，共 {len(rebuilt_index)} 条记录")
        return rebuilt_index

    def _index_missing_conversations(self, index: Dict) -> bool:
        """检查索引是否缺失本地对话文件"""
        t0 = time.perf_counter()
        index_ids = set(index.keys())
        files = list(self._iter_conversation_files())
        missing = False
        for file_path in files:
            conv_id = file_path.stem
            if conv_id and conv_id not in index_ids:
                print(f"🔍 对话 {conv_id} 未出现在索引中，将重建索引。")
                missing = True
                break
        elapsed_ms = (time.perf_counter() - t0) * 1000
        if perf_log:
            perf_log("_index_missing_conversations", elapsed_ms=elapsed_ms, extra={"file_count": len(files), "index_size": len(index), "missing": missing})
        return missing

    def _load_index(self, ensure_integrity: bool = False, max_rebuild: Optional[int] = None) -> Dict:
        """加载对话索引，可选地在缺失时自动重建（可限制重建条数）"""
        try:
            index: Dict = {}
            if self.index_file.exists():
                with open(self.index_file, 'r', encoding='utf-8') as f:
                    content = f.read().strip()
                    if content:
                        index = json.loads(content)
                        if index:
                            if ensure_integrity and not self._index_verified:
                                if self._index_missing_conversations(index):
                                    rebuilt = self._rebuild_index_from_files(max_count=max_rebuild)
                                    if rebuilt:
                                        self._save_index(rebuilt)
                                        index = rebuilt
                                self._index_verified = True
                            return index
                        # 索引为空但对话文件仍然存在时尝试重建
                        rebuilt = self._rebuild_index_from_files(max_count=max_rebuild)
                        if rebuilt:
                            self._save_index(rebuilt)
                            if ensure_integrity:
                                self._index_verified = True
                            return rebuilt
                        return {}
            # 索引缺失但存在对话文件时重建
            rebuilt = self._rebuild_index_from_files(max_count=max_rebuild)
            if rebuilt:
                self._save_index(rebuilt)
                if ensure_integrity:
                    self._index_verified = True
                return rebuilt
            return index
        except (json.JSONDecodeError, Exception) as e:
            print(f"⚠️ 加载对话索引失败，将尝试重建: {e}")
            backup_path = self.index_file.with_name(
                f"{self.index_file.stem}_corrupt_{int(time.time() * 1000)}{self.index_file.suffix}"
            )
            try:
                if self.index_file.exists():
                    self.index_file.replace(backup_path)
                    print(f"🗄️ 已备份损坏的索引文件到: {backup_path.name}")
            except Exception as backup_exc:
                print(f"⚠️ 备份损坏索引文件失败: {backup_exc}")
            # 索引损坏场景优先全量重建，避免仅重建部分导致“对话丢失”错觉
            rebuilt = self._rebuild_index_from_files(max_count=None)
            if rebuilt:
                self._save_index(rebuilt)
                if ensure_integrity:
                    self._index_verified = True
                return rebuilt
            return {}

    def _save_index(self, index: Dict):
        """保存对话索引"""
        try:
            with self._io_lock:
                self._atomic_write_json(self.index_file, index)
        except Exception as e:
            print(f"⌘ 保存对话索引失败: {e}")

    def _ensure_index_covering(self, limit: int, offset: int) -> Dict:
        """
        确保索引涵盖到 offset+limit 条记录，不足时按需扩展重建（仍按 mtime 倒序，增量加载批量）。
        每次先检查是否有新增对话文件未被纳入索引，避免"创建后列表不显示"的问题。
        """
        t0 = time.perf_counter()
        needed = max(0, int(offset) + int(limit))
        index = self._load_index()
        load_ms = (time.perf_counter() - t0) * 1000
        if perf_log:
            perf_log("_ensure_index_covering load_index", elapsed_ms=load_ms, extra={"index_size": len(index), "needed": needed})

        # 主动检查新增文件：索引可能滞后于实际对话文件，导致列表不同步。
        t1 = time.perf_counter()
        missing = self._index_missing_conversations(index)
        missing_ms = (time.perf_counter() - t1) * 1000
        if perf_log:
            perf_log("_ensure_index_covering check_missing", elapsed_ms=missing_ms, extra={"missing": missing, "index_size": len(index)})
        if missing:
            t2 = time.perf_counter()
            rebuilt = self._rebuild_index_from_files(max_count=max(needed, len(index) + 10))
            rebuild_ms = (time.perf_counter() - t2) * 1000
            if perf_log:
                perf_log("_ensure_index_covering rebuild_missing", elapsed_ms=rebuild_ms, extra={"rebuilt_size": len(rebuilt) if rebuilt else 0})
            if rebuilt:
                self._save_index(rebuilt)
                index = rebuilt

        if len(index) >= needed:
            total_ms = (time.perf_counter() - t0) * 1000
            if perf_log:
                perf_log("_ensure_index_covering return", elapsed_ms=total_ms, extra={"index_size": len(index), "needed": needed})
            return index

        # 第一次尝试：扩展到需要的数量（按更新时间倒序）
        t3 = time.perf_counter()
        rebuilt = self._rebuild_index_from_files(max_count=needed)
        rebuild_ms = (time.perf_counter() - t3) * 1000
        if perf_log:
            perf_log("_ensure_index_covering rebuild_needed", elapsed_ms=rebuild_ms, extra={"rebuilt_size": len(rebuilt) if rebuilt else 0})
        if rebuilt:
            self._save_index(rebuilt)
            index = rebuilt

        # 如果仍不足且存在更多文件可能未被纳入（例如首批限定过小），进行一次全量重建兜底
        if len(index) < needed:
            t4 = time.perf_counter()
            rebuilt_full = self._rebuild_index_from_files(max_count=None)
            rebuild_full_ms = (time.perf_counter() - t4) * 1000
            if perf_log:
                perf_log("_ensure_index_covering rebuild_full", elapsed_ms=rebuild_full_ms, extra={"rebuilt_size": len(rebuilt_full) if rebuilt_full else 0})
            if rebuilt_full:
                self._save_index(rebuilt_full)
                index = rebuilt_full

        total_ms = (time.perf_counter() - t0) * 1000
        if perf_log:
            perf_log("_ensure_index_covering return", elapsed_ms=total_ms, extra={"index_size": len(index), "needed": needed})
        return index
