"""
memory_pack.py - 记忆包格式（v8.3）

记忆剥离/灌输/交易的标准格式和操作。

记忆包格式 (MemoryPack):
{
    "format_version": "1.0",
    "pack_id": "唯一ID",
    "created_at": timestamp,
    "source_agent": "来源Agent ID",
    "metadata": {
        "description": "描述",
        "tags": ["标签"],
        "scope": "full|partial|topic",
    },
    "memories": [
        {
            "memory_id": "原始ID",
            "content": "内容",
            "time_ts": timestamp,
            "person_id": "P01",
            "nature_id": "D02",
            "importance": "medium",
            "topics": ["topic1", "topic2"],
            "tools": ["t_lc"],
            "knowledge_types": ["K01"],
            "valence": 0.3,
            "arousal": 0.5,
            "dominance": 0.5,
            "significance": "notable",
            "confidence": 0.8,
            "primary_emotions": {},
            "compound_emotions": [],
        }
    ],
    "links": [
        {
            "source_id": "id1",
            "target_id": "id2",
            "link_type": "temporal",
            "weight": 0.8,
            "reason": "原因",
        }
    ],
    "dimensions_snapshot": {
        "natures": {...},
        "persons": {...},
        "topics": [...],
        "tools": {...},
        "knowledge_types": {...},
    },
    "checksum": "SHA256",
}
"""

from __future__ import annotations

import json
import hashlib
import time
import uuid
import logging
import os
from pathlib import Path

logger = logging.getLogger(__name__)

from .store import _chunked_placeholders, SQLITE_MAX_VARIABLES

FORMAT_VERSION = "1.0"


def _compute_checksum(data: dict) -> str:
    raw = json.dumps(data, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(raw.encode()).hexdigest()


def create_pack(
    memories: list[dict],
    links: list[dict] = None,
    dimensions_snapshot: dict = None,
    source_agent: str = "_system",
    description: str = "",
    tags: list[str] = None,
    scope: str = "partial",
) -> dict:
    """
    创建记忆包。

    参数:
        memories: 记忆列表
        links: 关联关系列表
        dimensions_snapshot: 维度快照
        source_agent: 来源 Agent ID
        description: 描述
        tags: 标签
        scope: 范围（full/partial/topic）

    返回: 记忆包字典
    """
    pack = {
        "format_version": FORMAT_VERSION,
        "pack_id": f"mp_{uuid.uuid4().hex[:12]}",
        "created_at": int(time.time()),
        "source_agent": source_agent,
        "metadata": {
            "description": description,
            "tags": tags or [],
            "scope": scope,
            "memory_count": len(memories),
            "link_count": len(links) if links else 0,
        },
        "memories": memories,
        "links": links or [],
        "dimensions_snapshot": dimensions_snapshot or {},
    }

    pack["checksum"] = _compute_checksum({
        "memories": pack["memories"],
        "links": pack["links"],
    })

    return pack


def validate_pack(pack: dict) -> dict:
    """
    验证记忆包的完整性和格式。

    返回: {"valid": bool, "errors": [str], "warnings": [str]}
    """
    errors = []
    warnings = []

    if not isinstance(pack, dict):
        return {"valid": False, "errors": ["pack 不是字典"], "warnings": []}

    if pack.get("format_version") != FORMAT_VERSION:
        errors.append(f"格式版本不匹配: 期望 {FORMAT_VERSION}, 实际 {pack.get('format_version')}")

    if "pack_id" not in pack:
        errors.append("缺少 pack_id")

    if "memories" not in pack or not isinstance(pack["memories"], list):
        errors.append("缺少或无效的 memories 字段")
    else:
        for i, mem in enumerate(pack["memories"]):
            if not isinstance(mem, dict):
                errors.append(f"memories[{i}] 不是字典")
                continue
            if "content" not in mem:
                errors.append(f"memories[{i}] 缺少 content")
            if "time_ts" not in mem:
                warnings.append(f"memories[{i}] 缺少 time_ts，将使用当前时间")

    if "checksum" in pack:
        actual = _compute_checksum({
            "memories": pack.get("memories", []),
            "links": pack.get("links", []),
        })
        if actual != pack["checksum"]:
            errors.append("校验和不匹配，数据可能被篡改")

    return {
        "valid": len(errors) == 0,
        "errors": errors,
        "warnings": warnings,
    }


class MemoryPackManager:
    """记忆包管理器"""

    def __init__(self, store=None, encoder=None, pack_dir: str = None):
        """
        参数:
            store: MemoryStore 实例
            encoder: DimensionEncoder 实例
            pack_dir: 记忆包存储目录
        """
        self.store = store
        self.encoder = encoder
        self.pack_dir = Path(pack_dir) if pack_dir else Path(__file__).parent / "packs"
        self.pack_dir.mkdir(parents=True, exist_ok=True)

    # ── 记忆剥离 ──────────────────────────────────────

    def extract(
        self,
        topic_code: str = None,
        person_id: str = None,
        time_from: int = None,
        time_to: int = None,
        importance: str = None,
        nature_id: str = None,
        limit: int = 1000,
        include_links: bool = True,
        include_dimensions: bool = True,
        source_agent: str = "_system",
        description: str = "",
        tags: list[str] = None,
    ) -> dict:
        """
        从记忆系统中剥离（导出）指定范围的记忆。

        参数:
            topic_code: 主题代码
            person_id: 人物 ID
            time_from/time_to: 时间范围
            importance: 重要性过滤
            nature_id: 性质过滤
            limit: 最大导出条数
            include_links: 是否包含关联关系
            include_dimensions: 是否包含维度快照
            source_agent: 来源 Agent
            description: 描述
            tags: 标签

        返回: 记忆包
        """
        if not self.store:
            return create_pack([], source_agent=source_agent, description="无存储")

        memories = self.store.query(
            time_from=time_from,
            time_to=time_to,
            person_id=person_id,
            nature_id=nature_id,
            topic_code=topic_code,
            importance=importance,
            limit=limit,
        )

        links = []
        if include_links and memories:
            memory_ids = [m.get("memory_id") for m in memories if m.get("memory_id")]
            links = self._extract_links(memory_ids)

        dimensions_snapshot = {}
        if include_dimensions and self.encoder:
            dimensions_snapshot = self._snapshot_dimensions()

        scope = "full"
        if topic_code or person_id or importance or nature_id:
            scope = "partial"
        if topic_code and not person_id and not importance:
            scope = "topic"

        pack = create_pack(
            memories=memories,
            links=links,
            dimensions_snapshot=dimensions_snapshot,
            source_agent=source_agent,
            description=description or f"剥离 {len(memories)} 条记忆",
            tags=tags or [],
            scope=scope,
        )

        logger.info(f"📦 记忆剥离: {len(memories)} 条记忆, {len(links)} 条关联")
        return pack

    def extract_by_ids(
        self,
        memory_ids: list[str],
        include_links: bool = True,
        source_agent: str = "_system",
        description: str = "",
    ) -> dict:
        """
        按记忆 ID 列表剥离记忆。

        参数:
            memory_ids: 记忆 ID 列表
            include_links: 是否包含关联
            source_agent: 来源 Agent
            description: 描述

        返回: 记忆包
        """
        if not self.store:
            return create_pack([], source_agent=source_agent)

        memories = []
        for mid in memory_ids:
            mem = self.store.get_memory(mid)
            if mem:
                memories.append(mem)

        links = []
        if include_links and memories:
            links = self._extract_links(memory_ids)

        return create_pack(
            memories=memories,
            links=links,
            source_agent=source_agent,
            description=description or f"按ID剥离 {len(memories)} 条",
            scope="partial",
        )

    def _extract_links(self, memory_ids: list[str]) -> list[dict]:
        """提取记忆之间的关联关系"""
        if not self.store or not memory_ids:
            return []

        links = []
        id_set = set(memory_ids)

        try:
            for placeholders, chunk_ids in _chunked_placeholders(memory_ids, SQLITE_MAX_VARIABLES // 2):
                rows = self.store.conn.execute(
                    f"SELECT source_id, target_id, link_type, weight, reason "
                    f"FROM memory_links WHERE source_id IN ({placeholders}) "
                    f"AND target_id IN ({placeholders})",
                    chunk_ids + chunk_ids,
                ).fetchall()

                for r in rows:
                    links.append({
                        "source_id": r[0],
                        "target_id": r[1],
                        "link_type": r[2],
                        "weight": r[3],
                        "reason": r[4],
                    })
        except Exception as e:
            logger.warning("memory_pack: %s", e)

        return links

    def _snapshot_dimensions(self) -> dict:
        """拍摄维度注册表快照"""
        if not self.encoder:
            return {}
        return {
            "natures": dict(self.encoder.registry.get("natures", {})),
            "persons": dict(self.encoder.registry.get("persons", {})),
            "tools": dict(self.encoder.registry.get("tools", {})),
            "knowledge_types": dict(self.encoder.registry.get("knowledge_types", {})),
        }

    # ── 记忆灌输 ──────────────────────────────────────

    def inject(
        self,
        pack: dict,
        id_remap: bool = True,
        skip_duplicates: bool = True,
        merge_links: bool = True,
        merge_dimensions: bool = False,
        owner_agent_id: str = None,
        visibility: str = "team",
    ) -> dict:
        """
        将记忆包灌输（导入）到记忆系统中。

        参数:
            pack: 记忆包
            id_remap: 是否重新映射 ID（避免 ID 冲突）
            skip_duplicates: 是否跳过重复记忆
            merge_links: 是否合并关联关系
            merge_dimensions: 是否合并维度注册表
            owner_agent_id: 导入后的所有者 Agent ID
            visibility: 可见性

        返回: {
            "injected_count": int,
            "skipped_count": int,
            "link_count": int,
            "errors": [str],
            "id_map": {old_id: new_id},
        }
        """
        validation = validate_pack(pack)
        if not validation["valid"]:
            return {
                "injected_count": 0,
                "skipped_count": 0,
                "link_count": 0,
                "errors": validation["errors"],
                "id_map": {},
            }

        if not self.store:
            return {
                "injected_count": 0,
                "skipped_count": 0,
                "link_count": 0,
                "errors": ["store unavailable"],
                "id_map": {},
            }

        memories = pack.get("memories", [])
        links = pack.get("links", [])
        id_map = {}
        injected = 0
        skipped = 0
        link_count = 0
        errors = []

        for mem in memories:
            old_id = mem.get("memory_id", "")
            content = mem.get("content", "")
            if not content:
                skipped += 1
                continue

            if skip_duplicates:
                content_hash = hashlib.sha256(content.encode()).hexdigest()
                try:
                    existing = self.store.conn.execute(
                        "SELECT memory_id FROM memories WHERE content_hash = ?",
                        (content_hash,),
                    ).fetchone()
                    if existing:
                        id_map[old_id] = existing[0]
                        skipped += 1
                        continue
                except Exception as e:
                    logger.warning("memory_pack: %s", e)

            new_id = old_id
            if id_remap:
                new_id = f"imp_{uuid.uuid4().hex[:12]}"
                id_map[old_id] = new_id

            try:
                self.store.insert_memory_in_txn(
                    txn_conn=self.store.conn,
                    memory_id=new_id,
                    time_id=mem.get("time_id", ""),
                    time_ts=mem.get("time_ts", int(time.time())),
                    person_id=mem.get("person_id", "P01"),
                    nature_id=mem.get("nature_id", "D02"),
                    content=content,
                    content_hash=hashlib.sha256(content.encode()).hexdigest(),
                    topics=mem.get("topics", []),
                    tools=mem.get("tools", []),
                    knowledge_types=mem.get("knowledge_types", []),
                    importance=mem.get("importance", "medium"),
                    owner_agent_id=owner_agent_id or pack.get("source_agent", "_system"),
                    visibility=visibility,
                    valence=mem.get("valence", 0.0),
                    arousal=mem.get("arousal", 0.2),
                    dominance=mem.get("dominance", 0.5),
                    significance=mem.get("significance", "notable"),
                    confidence=mem.get("confidence", 0.5),
                    primary_emotions=json.dumps(mem.get("primary_emotions", {}), ensure_ascii=False),
                    compound_emotions=json.dumps(mem.get("compound_emotions", []), ensure_ascii=False),
                )
                injected += 1
            except Exception as e:
                errors.append(f"记忆 {old_id} 导入失败: {e}")
                skipped += 1

        if merge_links and links:
            for link in links:
                source = id_map.get(link.get("source_id", ""), link.get("source_id", ""))
                target = id_map.get(link.get("target_id", ""), link.get("target_id", ""))
                if source and target:
                    try:
                        self.store.insert_link(
                            source, target,
                            link_type=link.get("link_type", "temporal"),
                            weight=link.get("weight", 0.5),
                            reason=link.get("reason", "记忆包导入"),
                        )
                        link_count += 1
                    except Exception as e:
                        errors.append(f"关联导入失败: {e}")

        if merge_dimensions:
            self._merge_dimensions(pack.get("dimensions_snapshot", {}))

        logger.info(f"📥 记忆灌输: {injected} 条导入, {skipped} 条跳过, {link_count} 条关联")
        return {
            "injected_count": injected,
            "skipped_count": skipped,
            "link_count": link_count,
            "errors": errors,
            "id_map": id_map,
        }

    def _merge_dimensions(self, snapshot: dict):
        """合并维度注册表快照"""
        if not self.encoder or not snapshot:
            return

        for dim_key in ("natures", "persons", "tools", "knowledge_types"):
            incoming = snapshot.get(dim_key, {})
            if not incoming:
                continue

            current = self.encoder.registry.get(dim_key, {})
            for dim_id, info in incoming.items():
                if dim_id not in current:
                    code = info.get("code", "")
                    if code:
                        existing_codes = {v.get("code") for v in current.values()}
                        if code not in existing_codes:
                            current[dim_id] = info

        self.encoder._flush_registry()

    # ── 记忆包文件操作 ────────────────────────────────

    def save_pack(self, pack: dict, filename: str = None) -> str:
        """
        保存记忆包到文件。

        参数:
            pack: 记忆包
            filename: 文件名（None=自动生成）

        返回: 文件路径
        """
        if not filename:
            pack_id = pack.get("pack_id", uuid.uuid4().hex[:8])
            filename = f"{pack_id}.json"

        filepath = self.pack_dir / filename
        with open(filepath, "w", encoding="utf-8") as f:
            json.dump(pack, f, ensure_ascii=False, indent=2)

        logger.info(f"💾 记忆包已保存: {filepath}")
        return str(filepath)

    def load_pack(self, filename: str) -> dict:
        """
        从文件加载记忆包。

        参数:
            filename: 文件名

        返回: 记忆包
        """
        filepath = self.pack_dir / filename
        with open(filepath, "r", encoding="utf-8") as f:
            pack = json.load(f)

        validation = validate_pack(pack)
        if not validation["valid"]:
            logger.warning(f"记忆包验证失败: {validation['errors']}")

        return pack

    def list_packs(self) -> list[dict]:
        """
        列出所有已保存的记忆包。

        返回: [{"filename": str, "pack_id": str, "memory_count": int, "created_at": int}]
        """
        packs = []
        for filepath in self.pack_dir.glob("*.json"):
            try:
                with open(filepath, "r", encoding="utf-8") as f:
                    pack = json.load(f)
                packs.append({
                    "filename": filepath.name,
                    "pack_id": pack.get("pack_id", ""),
                    "memory_count": pack.get("metadata", {}).get("memory_count", 0),
                    "created_at": pack.get("created_at", 0),
                    "description": pack.get("metadata", {}).get("description", ""),
                })
            except Exception:
                continue

        return sorted(packs, key=lambda x: -x.get("created_at", 0))

    def delete_pack(self, filename: str) -> bool:
        """删除记忆包文件"""
        filepath = self.pack_dir / filename
        if filepath.exists():
            filepath.unlink()
            return True
        return False

    # ── 记忆交易 ──────────────────────────────────────

    def trade(
        self,
        target_pack_manager: "MemoryPackManager",
        topic_code: str = None,
        memory_ids: list[str] = None,
        bidirectional: bool = False,
    ) -> dict:
        """
        与另一个记忆包管理器进行记忆交易。

        参数:
            target_pack_manager: 目标管理器
            topic_code: 交易的主题
            memory_ids: 指定交易的记忆 ID
            bidirectional: 是否双向交易

        返回: {"sent": dict, "received": dict}
        """
        if memory_ids:
            out_pack = self.extract_by_ids(memory_ids, description="记忆交易")
        else:
            out_pack = self.extract(
                topic_code=topic_code,
                description=f"记忆交易: {topic_code or '全部'}",
            )

        received = target_pack_manager.inject(out_pack)

        reverse_received = None
        if bidirectional:
            in_pack = target_pack_manager.extract(
                topic_code=topic_code,
                description=f"记忆交易(反向): {topic_code or '全部'}",
            )
            reverse_received = self.inject(in_pack)

        return {
            "sent": {
                "pack_id": out_pack.get("pack_id"),
                "memory_count": out_pack.get("metadata", {}).get("memory_count", 0),
            },
            "received": received,
            "reverse_received": reverse_received,
        }
