"""Reflection service - 9-step reflection engine with trait lifecycle management."""

from __future__ import annotations

import json
import logging
import uuid as _uuid
from datetime import datetime, timedelta, timezone
from typing import Optional

from sqlalchemy import text as sql_text
from sqlalchemy.ext.asyncio import AsyncSession

from neuromem.models.memory import Memory
from neuromem.models.reflection_cycle import ReflectionCycle
from neuromem.providers.embedding import EmbeddingProvider
from neuromem.providers.llm import LLMProvider
from neuromem.services.sensitive_filter import is_sensitive_trait  # noqa: F401
from neuromem.services.trait_engine import TraitEngine

logger = logging.getLogger(__name__)


REFLECTION_PROMPT_TEMPLATE = """## 已有特质
{existing_traits_json}

## 新增记忆（自上次反思以来）
{new_memories_json}

## 分析任务

请严格按以下规则分析，返回 JSON 结果：

### 0. 敏感内容排除（最高优先级）

以下类别**严禁**创建为特质：心理健康诊断、精神疾病、医疗信息、政治倾向、宗教信仰、性取向、具体收入/债务金额、犯罪记录、成瘾行为、虐待/创伤经历。
如果新增记忆涉及这些类别，直接跳过，不要在 new_trends 或 new_behaviors 中输出。

注意：新增记忆中的 metadata 可能包含 emotion 字段（含 valence、arousal、label）。在检测趋势和行为模式时，请关注：
- 特定话题/情境下反复出现的情绪模式（如"讨论工作时总是焦虑"）
- 情绪变化趋势（如"最近整体情绪变积极了"）
- 情绪触发关联（如"提到某人时情绪波动大"）
将这些情绪模式作为情境化 trait 产生，context 从话题推断。

### 1. 短期趋势检测 (new_trends)
- 从新增记忆中识别**近期趋势**（证据跨度短、数量少的初步模式）
- 每条 trend 需要至少 2 条记忆支撑
- 为每条 trend 推断情境标签(context): work/personal/social/learning/general
- 建议观察窗口(window_days): 情绪相关趋势 14 天，其他 30 天

### 2. 行为模式检测 (new_behaviors)
- 从新增记忆中识别**行为模式**（≥3 条记忆呈现相同模式，或跨度较长）
- 与已有特质内容去重：如果某个模式已被已有特质覆盖，归入 reinforcements 而非 new_behaviors
- 为每条 behavior 推断情境标签(context)
- 为每条 behavior 推断行为类型(behavior_kind):
  * "pattern": 统计规律型（如"深夜活跃"、"偏好简洁"、"每周运动3次"）
  * "procedural": 操作流程型（如"先画架构图再写代码"、"调试时先看日志再加断点"）
- 初始置信度建议：通常 0.4，跨情境一致的可以给 0.5

### 3. 已有特质强化 (reinforcements)
- 检查新增记忆中是否有支持已有特质的证据
- 标注证据质量等级：
  - A: 跨情境行为一致性（同一模式在 work+personal 等不同情境中出现）
  - B: 用户显式自我陈述（"我是个急性子"）
  - C: 跨对话行为（不同对话中观测到相同模式）
  - D: 同对话内信号或隐式推断

### 4. 矛盾检测 (contradictions)
- 检查新增记忆中是否有**与已有特质矛盾**的证据
- 仅报告明确矛盾，不报告细微差异

### 5. 升级建议 (upgrades)
- 检查已有 behavior 是否有 ≥2 个指向同一倾向 → 建议升级为 preference
- 检查已有 preference 是否有 ≥2 个指向同一人格维度 → 建议升级为 core
- 注意：升级建议由代码层验证 confidence 门槛后执行

### 6. 记忆关联检测 (links)
- 检查新增记忆之间、以及新增记忆与已有记忆之间是否存在语义关联
- 关联类型: elaborates（补充说明）、contradicts（矛盾）、same_topic（同主题）、causal（因果）
- 只标注有明确关联的记忆对，不要过度关联

如果没有发现任何模式、强化或矛盾，返回所有数组为空的 JSON。

只返回 JSON，不要其他内容：
```json
{{
  "new_trends": [
    {{
      "content": "趋势描述（具体、有细节）",
      "evidence_ids": ["记忆ID1", "记忆ID2"],
      "window_days": 30,
      "context": "work"
    }}
  ],
  "new_behaviors": [
    {{
      "content": "行为模式描述",
      "evidence_ids": ["记忆ID1", "记忆ID2", "记忆ID3"],
      "confidence": 0.4,
      "context": "work",
      "behavior_kind": "pattern"
    }}
  ],
  "reinforcements": [
    {{
      "trait_id": "已有特质的UUID",
      "new_evidence_ids": ["记忆ID"],
      "quality_grade": "C"
    }}
  ],
  "contradictions": [
    {{
      "trait_id": "已有特质的UUID",
      "contradicting_evidence_ids": ["记忆ID"],
      "description": "矛盾描述"
    }}
  ],
  "upgrades": [
    {{
      "from_trait_ids": ["behavior-id-1", "behavior-id-2"],
      "new_content": "升级后的描述",
      "new_subtype": "preference",
      "reasoning": "升级理由"
    }}
  ],
  "links": [
    {{
      "source_id": "记忆ID-A",
      "target_id": "记忆ID-B",
      "relation": "关联类型"
    }}
  ]
}}
```"""


class ReflectionService:
    """9-step reflection engine with trait lifecycle management.

    Replaces the original insight-based reflection with structured trait analysis.
    """

    def __init__(
        self,
        db: AsyncSession,
        embedding: EmbeddingProvider,
        llm: LLMProvider,
    ):
        self.db = db
        self._embedding = embedding
        self._llm = llm
        self._trait_engine = TraitEngine(db, embedding)

    async def _get_watermark(self, user_id: str) -> datetime | None:
        """Get the last completed reflection timestamp for a user."""
        result = await self.db.execute(
            sql_text(
                "SELECT completed_at FROM reflection_cycles "
                "WHERE user_id = :uid AND status = 'completed' "
                "ORDER BY completed_at DESC LIMIT 1"
            ),
            {"uid": user_id},
        )
        row = result.first()
        return row.completed_at if row else None

    async def should_reflect(self, user_id: str) -> tuple[bool, str | None, float | None]:
        """Check whether reflection should be triggered.

        Returns:
            (should_trigger, trigger_type, trigger_value)
        """
        last_reflected = await self._get_watermark(user_id)

        if last_reflected is None:
            # Check if user has any memories at all
            mem_result = await self.db.execute(
                sql_text(
                    "SELECT COUNT(*) FROM memories "
                    "WHERE user_id = :uid AND memory_type IN ('fact', 'episodic')"
                ),
                {"uid": user_id},
            )
            mem_count = mem_result.scalar() or 0
            if mem_count == 0:
                return (False, None, None)
            return (True, "first_time", None)

        now = datetime.now(timezone.utc)

        # Idempotency check: if last reflected within 60s, skip
        if (now - last_reflected).total_seconds() < 60:
            return (False, None, None)

        # Check importance accumulation
        # Use metadata importance as override when available (extraction sets it there),
        # fall back to the ORM importance column
        result = await self.db.execute(
            sql_text(
                "SELECT COALESCE(SUM(COALESCE((metadata->>'importance')::float, importance)), 0) AS total "
                "FROM memories "
                "WHERE user_id = :uid "
                "AND memory_type IN ('fact', 'episodic') "
                "AND created_at > :watermark"
            ),
            {"uid": user_id, "watermark": last_reflected},
        )
        accumulated = float(result.scalar() or 0)
        if accumulated >= 30:
            return (True, "importance_accumulated", accumulated)

        # Check 24h scheduled trigger
        if (now - last_reflected) >= timedelta(hours=24):
            return (True, "scheduled", None)

        # Check if there are any new memories at all
        result = await self.db.execute(
            sql_text(
                "SELECT COUNT(*) FROM memories "
                "WHERE user_id = :uid "
                "AND memory_type IN ('fact', 'episodic') "
                "AND created_at > :watermark"
            ),
            {"uid": user_id, "watermark": last_reflected},
        )
        new_count = result.scalar() or 0
        if new_count == 0:
            return (False, None, None)

        return (False, None, None)

    async def reflect(
        self,
        user_id: str,
        force: bool = False,
        session_ended: bool = False,
    ) -> dict:
        """Execute 9-step reflection pipeline.

        Args:
            user_id: User ID.
            force: Skip trigger check if True.
            session_ended: Mark as session-end trigger.

        Returns:
            Result dict with triggered, trigger_type, and stats.
        """
        # Step 0: Trigger check
        trigger_type: str | None = None
        trigger_value: float | None = None

        if session_ended:
            trigger_type = "session_end"
        elif force:
            trigger_type = "force"
        else:
            should, ttype, tvalue = await self.should_reflect(user_id)
            if not should:
                return {
                    "triggered": False,
                    "trigger_type": None,
                    "memories_scanned": 0,
                    "traits_created": 0,
                    "traits_updated": 0,
                    "traits_dissolved": 0,
                    "cycle_id": None,
                }
            trigger_type = ttype
            trigger_value = tvalue

        # Get watermark once, pass it down to avoid redundant queries
        watermark = await self._get_watermark(user_id)

        # Step 1: Create reflection cycle record
        cycle = ReflectionCycle(
            user_id=user_id,
            trigger_type=trigger_type or "unknown",
            trigger_value=trigger_value,
            status="running",
        )
        self.db.add(cycle)
        await self.db.flush()
        cycle_id = str(cycle.id)

        # Step 2: Run reflection steps
        try:
            stats = await self._run_reflection_steps(user_id, trigger_type, trigger_value, cycle_id, watermark)

            # Update cycle record
            cycle.status = "completed"
            cycle.completed_at = datetime.now(timezone.utc)
            cycle.memories_scanned = stats["memories_scanned"]
            cycle.traits_created = stats["traits_created"]
            cycle.traits_updated = stats["traits_updated"]
            cycle.traits_dissolved = stats["traits_dissolved"]
            await self.db.flush()

            return {
                "triggered": True,
                "trigger_type": trigger_type,
                "cycle_id": cycle_id,
                **stats,
            }

        except Exception as e:
            logger.error("Reflection failed for user=%s: %s", user_id, e, exc_info=True)
            try:
                cycle.status = "failed"
                cycle.completed_at = datetime.now(timezone.utc)
                cycle.error_message = str(e)[:500]
                await self.db.flush()
            except Exception:
                pass

            return {
                "triggered": True,
                "trigger_type": trigger_type,
                "cycle_id": cycle_id,
                "memories_scanned": 0,
                "traits_created": 0,
                "traits_updated": 0,
                "traits_dissolved": 0,
                "error": str(e),
            }

    async def _run_reflection_steps(
        self,
        user_id: str,
        trigger_type: str | None,
        trigger_value: float | None,
        cycle_id: str,
        watermark: datetime | None = ...,
    ) -> dict:
        """Core 9-step reflection pipeline."""
        stats = {"memories_scanned": 0, "traits_created": 0, "traits_updated": 0, "traits_dissolved": 0}

        # Step 1: Scan new memories (reuse watermark to avoid redundant query)
        new_memories = await self._scan_new_memories(user_id, watermark)
        stats["memories_scanned"] = len(new_memories)

        # Step 3 (before LLM): trend expiry/promotion (pure code)
        expired = await self._trait_engine.expire_trends(user_id)
        promoted = await self._trait_engine.promote_trends(user_id)
        stats["traits_dissolved"] += expired
        stats["traits_updated"] += promoted

        await self._expire_prospective_facts(user_id)

        if not new_memories:
            # No new memories -> only apply decay
            dissolved = await self._trait_engine.apply_decay(user_id)
            stats["traits_dissolved"] += dissolved
            return stats

        # Step 2: LLM main call
        existing_traits = await self._load_existing_traits(user_id)

        use_two_stage = trigger_type in ("importance_accumulated",)
        if use_two_stage:
            llm_result = await self._two_stage_reflect(user_id, new_memories, existing_traits)
        else:
            llm_result = await self._call_reflection_llm(new_memories, existing_traits)

        if llm_result is None:
            # LLM failed -> only apply decay, don't update watermark
            dissolved = await self._trait_engine.apply_decay(user_id)
            stats["traits_dissolved"] += dissolved
            return stats

        # Step 4: Process new_trends + new_behaviors
        for trend in llm_result.get("new_trends", []):
            if is_sensitive_trait(trend.get("content", "")):
                logger.info("Skipping sensitive trend: %s", trend["content"][:60])
                continue
            await self._trait_engine.create_trend(
                user_id=user_id,
                content=trend["content"],
                evidence_ids=trend.get("evidence_ids", []),
                window_days=trend.get("window_days", 30),
                context=trend.get("context", "general"),
                cycle_id=cycle_id,
            )
            stats["traits_created"] += 1

        for behavior in llm_result.get("new_behaviors", []):
            if is_sensitive_trait(behavior.get("content", "")):
                logger.info("Skipping sensitive behavior: %s", behavior["content"][:60])
                continue
            await self._trait_engine.create_behavior(
                user_id=user_id,
                content=behavior["content"],
                evidence_ids=behavior.get("evidence_ids", []),
                confidence=behavior.get("confidence", 0.4),
                context=behavior.get("context", "general"),
                cycle_id=cycle_id,
                behavior_kind=behavior.get("behavior_kind", "pattern"),
            )
            stats["traits_created"] += 1

        # Step 5: Process reinforcements
        for reinforcement in llm_result.get("reinforcements", []):
            trait_id = reinforcement.get("trait_id", "")
            if not self._is_valid_uuid(trait_id):
                logger.warning("Skipping reinforcement with invalid trait_id: %s", trait_id)
                continue
            await self._trait_engine.reinforce_trait(
                trait_id=trait_id,
                evidence_ids=reinforcement.get("new_evidence_ids", []),
                quality_grade=reinforcement.get("quality_grade", "C"),
                cycle_id=cycle_id,
                user_id=user_id,
            )
            stats["traits_updated"] += 1

        # Step 6: Process upgrades
        for upgrade in llm_result.get("upgrades", []):
            from_ids = upgrade.get("from_trait_ids", [])
            from_ids = [tid for tid in from_ids if self._is_valid_uuid(tid)]
            if not from_ids:
                logger.warning("Skipping upgrade with no valid from_trait_ids")
                continue
            result = await self._trait_engine.try_upgrade(
                from_trait_ids=from_ids,
                new_content=upgrade["new_content"],
                new_subtype=upgrade["new_subtype"],
                reasoning=upgrade.get("reasoning", ""),
                cycle_id=cycle_id,
            )
            if result:
                stats["traits_created"] += 1

        # Step 7: Process contradictions + possible special reflection
        for contradiction in llm_result.get("contradictions", []):
            trait_id = contradiction.get("trait_id", "")
            if not self._is_valid_uuid(trait_id):
                logger.warning("Skipping contradiction with invalid trait_id: %s", trait_id)
                continue
            result = await self._trait_engine.apply_contradiction(
                trait_id=trait_id,
                evidence_ids=contradiction.get("contradicting_evidence_ids", []),
                cycle_id=cycle_id,
                user_id=user_id,
            )
            if result.get("needs_special_reflection"):
                resolved = await self._trait_engine.resolve_contradiction(
                    trait_id=contradiction["trait_id"],
                    llm=self._llm,
                    cycle_id=cycle_id,
                )
                if resolved.get("action") == "dissolve":
                    stats["traits_dissolved"] += 1
                else:
                    stats["traits_updated"] += 1

        # Step: Process memory links (Zettelkasten)
        for link in llm_result.get("links", []):
            await self._create_memory_link(
                source_id=link.get("source_id"),
                target_id=link.get("target_id"),
                relation=link.get("relation", "related"),
            )

        # Step 8: Time decay
        dissolved = await self._trait_engine.apply_decay(user_id)
        stats["traits_dissolved"] += dissolved

        return stats

    async def _create_memory_link(
        self,
        source_id: str | None,
        target_id: str | None,
        relation: str = "related",
    ) -> None:
        """Create bidirectional link between two memories via metadata."""
        if not source_id or not target_id:
            return
        try:
            import uuid as _uuid
            source_uuid = _uuid.UUID(source_id)
            target_uuid = _uuid.UUID(target_id)

            _link_sql = sql_text(
                "UPDATE memories "
                "SET metadata = jsonb_set("
                "  COALESCE(metadata, '{}'), "
                "  '{related_memories}', "
                "  COALESCE(metadata->'related_memories', '[]') || CAST(:link_json AS jsonb)"
                ") "
                "WHERE id = :id "
                "AND NOT (COALESCE(metadata->'related_memories', '[]') @> CAST(:link_json AS jsonb))"
            )

            # Add target to source's related_memories
            await self.db.execute(
                _link_sql,
                {"id": source_uuid, "link_json": json.dumps([{"id": target_id, "relation": relation}])},
            )
            # Add source to target's related_memories (reverse direction)
            await self.db.execute(
                _link_sql,
                {"id": target_uuid, "link_json": json.dumps([{"id": source_id, "relation": relation}])},
            )
        except Exception as e:
            logger.warning("Failed to create memory link %s<->%s: %s", source_id, target_id, e)

    async def _expire_prospective_facts(self, user_id: str) -> int:
        """Mark prospective facts as historical when their event_time has passed."""
        result = await self.db.execute(
            sql_text(
                "UPDATE memories SET metadata = jsonb_set(metadata, '{temporality}', '\"historical\"') "
                "WHERE user_id = :uid AND memory_type = 'fact' "
                "AND metadata->>'temporality' = 'prospective' "
                "AND (metadata->>'event_time') IS NOT NULL "
                "AND (metadata->>'event_time')::timestamp < NOW() "
                "RETURNING id"
            ),
            {"uid": user_id},
        )
        rows = result.fetchall()
        if rows:
            logger.info("Expired %d prospective facts to historical for user=%s", len(rows), user_id)
        return len(rows)

    async def _scan_new_memories(
        self, user_id: str, watermark: datetime | None = ...,
    ) -> list[dict]:
        """Scan memories created after last reflection watermark.

        Args:
            watermark: If provided, use as the watermark timestamp.
                If ... (sentinel), query from reflection_cycles.
        """
        if watermark is ...:
            # Query watermark from reflection_cycles
            result = await self.db.execute(
                sql_text(
                    "SELECT completed_at FROM reflection_cycles "
                    "WHERE user_id = :uid AND status = 'completed' "
                    "ORDER BY completed_at DESC LIMIT 1"
                ),
                {"uid": user_id},
            )
            row = result.first()
            watermark = row.completed_at if row else None

        # Build query
        where = "user_id = :uid AND memory_type IN ('fact', 'episodic')"
        params: dict = {"uid": user_id}
        if watermark:
            where += " AND created_at > :watermark"
            params["watermark"] = watermark

        result = await self.db.execute(
            sql_text(
                f"SELECT id, content, memory_type, importance, metadata, created_at "
                f"FROM memories WHERE {where} "
                f"ORDER BY created_at ASC LIMIT 200"
            ),
            params,
        )
        rows = result.fetchall()

        return [
            {
                "id": str(r.id),
                "content": r.content,
                "memory_type": r.memory_type,
                "importance": float(r.importance) if r.importance else 0.5,
                "metadata": r.metadata,
                "created_at": str(r.created_at),
            }
            for r in rows
        ]

    async def _load_existing_traits(self, user_id: str) -> list[dict]:
        """Load existing active traits for LLM context."""
        result = await self.db.execute(
            sql_text(
                "SELECT id, content, trait_stage, trait_subtype, trait_confidence, trait_context "
                "FROM memories "
                "WHERE user_id = :uid AND memory_type = 'trait' "
                "AND trait_stage NOT IN ('dissolved') "
                "ORDER BY trait_confidence DESC NULLS LAST LIMIT 50"
            ),
            {"uid": user_id},
        )
        rows = result.fetchall()

        return [
            {
                "id": str(r.id),
                "content": r.content,
                "stage": r.trait_stage,
                "subtype": r.trait_subtype,
                "confidence": float(r.trait_confidence) if r.trait_confidence else 0.0,
                "context": r.trait_context,
            }
            for r in rows
        ]

    async def _two_stage_reflect(
        self,
        user_id: str,
        new_memories: list[dict],
        existing_traits: list[dict],
    ) -> dict | None:
        """Two-stage reflection: generate questions, retrieve evidence, then analyze."""
        # Stage 1: Generate questions
        memory_summary = json.dumps(
            [{"id": m["id"], "content": m["content"]} for m in new_memories[:20]],
            ensure_ascii=False,
        )
        question_prompt = (
            f"根据以下用户近期记忆，生成 3-5 个关键问题来深入了解该用户。\n\n"
            f"记忆:\n{memory_summary}\n\n"
            f"只返回 JSON:\n```json\n{{\"questions\": [\"问题1\", \"问题2\", ...]}}\n```"
        )
        try:
            q_result = await self._llm.chat(
                messages=[
                    {"role": "system", "content": "你是一个用户分析引擎。生成关于用户的关键问题。只返回 JSON。"},
                    {"role": "user", "content": question_prompt},
                ],
                temperature=0.3,
                max_tokens=512,
            )
            questions = self._parse_questions(q_result)
        except Exception as e:
            logger.warning("Two-stage reflection stage 1 failed: %s, falling back to single-stage", e)
            return await self._call_reflection_llm(new_memories, existing_traits)

        if not questions:
            return await self._call_reflection_llm(new_memories, existing_traits)

        # Stage 2: Retrieve evidence for each question (parallel)
        import asyncio
        from neuromem.services.search import SearchService
        search_svc = SearchService(self.db, self._embedding)

        async def _search_question(q: str) -> list[dict]:
            try:
                return await search_svc.scored_search(user_id=user_id, query=q, limit=3)
            except Exception:
                return []

        all_hits = await asyncio.gather(*[_search_question(q) for q in questions[:5]])
        evidence: list[dict] = []
        seen_ids: set[str] = set()
        for hits in all_hits:
            for h in hits:
                if h["id"] not in seen_ids:
                    seen_ids.add(h["id"])
                    evidence.append({"id": h["id"], "content": h["content"], "score": h["score"]})

        # Build enriched prompt with evidence
        enriched_memories = new_memories + [
            {"id": e["id"], "content": f"[历史证据] {e['content']}", "memory_type": "evidence"}
            for e in evidence[:10]
        ]

        return await self._call_reflection_llm(enriched_memories, existing_traits)

    def _parse_questions(self, result_text: str) -> list[str]:
        """Parse question generation result."""
        try:
            t = result_text.strip()
            if "```json" in t:
                start = t.find("```json") + 7
                end = t.find("```", start)
                t = t[start:end].strip()
            elif "```" in t:
                start = t.find("```") + 3
                end = t.find("```", start)
                t = t[start:end].strip()
            result = json.loads(t)
            questions = result.get("questions", [])
            return [q for q in questions if isinstance(q, str)]
        except Exception as e:
            logger.warning("Failed to parse questions: %s", e)
            return []

    async def _call_reflection_llm(
        self,
        new_memories: list[dict],
        existing_traits: list[dict],
    ) -> dict | None:
        """Call LLM for main reflection analysis. Returns None on failure."""
        prompt = self._build_reflection_prompt(new_memories, existing_traits)
        try:
            result_text = await self._llm.chat(
                messages=[
                    {"role": "system", "content": "你是一个用户特质分析引擎。根据用户的新增记忆和已有特质，执行结构化分析。只返回 JSON。"},
                    {"role": "user", "content": prompt},
                ],
                temperature=0.1,
                max_tokens=4096,
            )
            return self._parse_reflection_result(result_text)
        except Exception as e:
            logger.error("Reflection LLM call failed: %s", e, exc_info=True)
            return None

    def _build_reflection_prompt(
        self,
        new_memories: list[dict],
        existing_traits: list[dict],
    ) -> str:
        """Build the main reflection prompt."""
        existing_json = json.dumps(existing_traits, ensure_ascii=False, indent=2) if existing_traits else "[]"
        memories_json = json.dumps(
            [{"id": m["id"], "content": m["content"], "memory_type": m["memory_type"]} for m in new_memories],
            ensure_ascii=False,
            indent=2,
        )
        return REFLECTION_PROMPT_TEMPLATE.format(
            existing_traits_json=existing_json,
            new_memories_json=memories_json,
        )

    def _parse_reflection_result(self, result_text: str) -> dict | None:
        """Parse LLM reflection JSON response."""
        try:
            t = result_text.strip()
            if "```json" in t:
                start = t.find("```json") + 7
                end = t.find("```", start)
                t = t[start:end].strip()
            elif "```" in t:
                start = t.find("```") + 3
                end = t.find("```", start)
                t = t[start:end].strip()

            result = json.loads(t)
            if not isinstance(result, dict):
                return None
            return result
        except json.JSONDecodeError as e:
            logger.error("Failed to parse reflection JSON: %s", e)
            return None
        except Exception as e:
            logger.error("Error parsing reflection result: %s", e)
            return None

    # ============== Backward compatibility ==============

    async def digest(
        self,
        user_id: str,
        recent_memories: list[dict],
        existing_traits: Optional[list[dict]] = None,
    ) -> dict:
        """Backward-compatible digest entry point.

        Generates pattern/summary traits via LLM and stores as trait(trend).

        Note: New code should use reflect() instead.
        """
        if not recent_memories:
            return {"traits": []}

        traits = await self._generate_traits(user_id, recent_memories, existing_traits)

        return {"traits": traits}

    async def _generate_traits(
        self,
        user_id: str,
        recent_memories: list[dict],
        existing_traits: Optional[list[dict]] = None,
    ) -> list[dict]:
        """Generate pattern and summary traits (legacy digest behavior)."""
        prompt = self._build_trait_prompt(recent_memories, existing_traits)

        try:
            result_text = await self._llm.chat(
                messages=[{"role": "user", "content": prompt}],
                temperature=0.3,
                max_tokens=2048,
            )
            traits = self._parse_trait_result(result_text)
        except Exception as e:
            logger.error("Trait generation LLM call failed: %s", e, exc_info=True)
            return []

        # Filter valid traits first
        _MIN_TRAIT_IMPORTANCE = 7
        valid_traits = []
        for trait_item in traits:
            content = trait_item.get("content")
            category = trait_item.get("category", "pattern")
            importance = int(trait_item.get("importance", 8))
            if not content or category not in ("pattern", "summary"):
                continue
            if importance < _MIN_TRAIT_IMPORTANCE:
                logger.debug("Skipping low-importance trait (importance=%d): %s", importance, content[:60])
                continue
            valid_traits.append(trait_item)

        if not valid_traits:
            return []

        # Embed all traits in batch
        contents = [item["content"] for item in valid_traits]
        try:
            vectors = await self._embedding.embed_batch(contents)
        except Exception as e:
            logger.error("Failed to embed traits batch: %s", e)
            return []

        stored = []
        for trait_item, vector in zip(valid_traits, vectors):
            embedding_obj = Memory(
                user_id=user_id,
                content=trait_item["content"],
                embedding=vector,
                memory_type="trait",
                trait_stage="trend",
                metadata_={
                    "category": trait_item.get("category", "pattern"),
                    "source_ids": trait_item.get("source_ids", []),
                    "importance": int(trait_item.get("importance", 8)),
                },
            )
            self.db.add(embedding_obj)
            stored.append(trait_item)

        if stored:
            await self.db.flush()

        return stored

    def _build_trait_prompt(
        self,
        memories: list[dict],
        existing_traits: Optional[list[dict]] = None,
    ) -> str:
        """Build prompt for generating pattern and summary traits."""
        memory_lines = []
        for i, m in enumerate(memories):
            content = m.get("content", "")
            mtype = m.get("memory_type", "unknown")
            memory_lines.append(f"{i+1}. [{mtype}] {content}")

        memories_text = "\n".join(memory_lines)

        existing_text = ""
        if existing_traits:
            recent = existing_traits[-20:]
            existing_lines = [f"- {ins.get('content', '')}" for ins in recent]
            existing_text = f"""
已有洞察（共 {len(existing_traits)} 条，显示最近 {len(recent)} 条）：
{chr(10).join(existing_lines)}

⚠️ 严格去重规则：
- 如果新洞察与已有洞察表达相同或相似的含义，直接跳过，不要输出
- 只输出已有洞察中**未覆盖**的新角度、新发现、或具体细节的补充
- 如果本批记忆没有带来新的洞察，返回空列表 {{"traits": []}}
"""

        return f"""你是一个记忆分析系统。根据用户的新记忆，生成**增量**行为模式和阶段总结洞察。

用户最新的记忆：
{memories_text}
{existing_text}
生成规则：
1. 每条洞察必须综合多条记忆，得出更深层的理解（不是复述单条记忆）
2. 类别：
   - pattern: 具体的行为模式或习惯（需有细节，如"用户在压力大时会回避社交，但之后主动寻求朋友帮助"）
   - summary: 近期经历的阶段性总结（需有时间感，如"用户近两周集中在解决技术债，已完成重构"）
3. importance（1-10）：对理解用户有多大价值
   - 9-10：揭示用户核心性格/价值观/重大转变
   - 7-8：有具体细节的有用模式
   - 5-6：泛泛的观察
   - <7：不要输出
4. 如果没有值得输出的新洞察，返回空列表

只返回 JSON，不要其他内容：
```json
{{
  "traits": [
    {{
      "content": "洞察内容（具体、有细节）",
      "category": "pattern|summary",
      "importance": 7,
      "source_ids": []
    }}
  ]
}}
```"""

    def _parse_trait_result(self, result_text: str) -> list[dict]:
        """Parse LLM trait generation output."""
        try:
            t = result_text.strip()

            if "```json" in t:
                start = t.find("```json") + 7
                end = t.find("```", start)
                t = t[start:end].strip()
            elif "```" in t:
                start = t.find("```") + 3
                end = t.find("```", start)
                t = t[start:end].strip()

            result = json.loads(t)

            if not isinstance(result, dict):
                return []

            traits = result.get("traits") or result.get("insights", [])
            if not isinstance(traits, list):
                return []

            valid = []
            for ins in traits:
                if isinstance(ins, dict) and ins.get("content"):
                    valid.append({
                        "content": ins["content"],
                        "category": ins.get("category", "pattern"),
                        "importance": ins.get("importance", 8),
                        "source_ids": ins.get("source_ids", []),
                    })
            return valid

        except json.JSONDecodeError as e:
            logger.error("Failed to parse trait JSON: %s", e)
            return []
        except Exception as e:
            logger.error("Error parsing trait result: %s", e)
            return []

    @staticmethod
    def _is_valid_uuid(val: str) -> bool:
        """Check if a string is a valid UUID."""
        try:
            _uuid.UUID(val)
            return True
        except (ValueError, AttributeError):
            return False

