"""
语义记忆 — 事实/知识库
存储格式: memory/semantic/facts.jsonl (每行一个事实)
"""

import hashlib
import json
import re
import time
from pathlib import Path


class SemanticMemory:
    """语义记忆: 存储从对话中提取的关键事实"""

    def __init__(self, storage_dir: Path):
        self.storage_dir = Path(storage_dir)
        self.storage_dir.mkdir(parents=True, exist_ok=True)
        self._facts_path = self.storage_dir / "facts.jsonl"

    def add(
        self,
        fact: str,
        tags: list = None,
        priority: float = 1.0,
        source: str = None,
        confidence: float = 0.7,
    ) -> dict:
        """
        添加一条事实

        Args:
            fact: 事实陈述文本
            tags: 标签列表
            priority: 优先级 (越高越不容易被遗忘)
            source: 来源记忆模块 ID
            confidence: 可信度
        """
        fact = fact.strip()
        if not fact:
            return {}

        facts = self._read_all()
        now = time.time()
        normalized = self._normalize_text(fact)
        source_ids = [source] if source else []

        for entry in facts:
            if entry.get("normalized") == normalized:
                entry["fact"] = fact
                entry["tags"] = sorted(set(entry.get("tags", []) + (tags or [])))
                entry["priority"] = max(entry.get("priority", 1.0), priority)
                entry["confidence"] = max(entry.get("confidence", 0.7), confidence)
                entry["updated_at"] = now
                entry["source_ids"] = sorted(set(entry.get("source_ids", []) + source_ids))
                self._write_all(facts)
                return entry

        entry = {
            "id": self._fact_id(normalized),
            "fact": fact,
            "normalized": normalized,
            "tags": tags or [],
            "priority": priority,
            "confidence": confidence,
            "created_at": time.time(),
            "updated_at": now,
            "last_accessed": 0,
            "access_count": 0,
            "source_ids": source_ids,
            "status": "active",
        }
        with open(self._facts_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")
        return entry

    def retrieve_relevant(self, query: str = "", limit: int = 20) -> list[str]:
        """
        获取相关事实 (相关性 + 优先级 + 访问频率)

        Args:
            query: 可选, 用于关键词过滤
            limit: 返回条数
        Returns:
            事实文本列表
        """
        facts = self._read_all()
        if not facts:
            return []

        terms = self._query_terms(query)
        scored = []
        for fact in facts:
            if fact.get("status", "active") != "active":
                continue
            score = self._relevance_score(fact, terms)
            if terms and score <= 0:
                continue
            scored.append((score, fact))

        scored.sort(
            key=lambda item: (
                item[0],
                item[1].get("priority", 1.0),
                item[1].get("access_count", 0),
                item[1].get("updated_at", item[1].get("created_at", 0)),
            ),
            reverse=True,
        )

        selected = [fact for _, fact in scored[:limit]]
        if selected:
            now = time.time()
            selected_ids = {f.get("id") for f in selected}
            for fact in facts:
                if fact.get("id") in selected_ids:
                    fact["access_count"] = fact.get("access_count", 0) + 1
                    fact["last_accessed"] = now
            self._write_all(facts)

        return [f["fact"] for f in selected]

    def search(self, query: str, limit: int = 5) -> list[dict]:
        """
        关键词搜索语义记忆

        Returns:
            [{score, fact, tags}, ...]
        """
        facts = self._read_all()
        results = []
        terms = self._query_terms(query)

        for f in facts:
            if f.get("status", "active") != "active":
                continue
            fact_text = f.get("fact", "")
            score = self._relevance_score(f, terms)
            if score > 0:
                results.append({
                    "score": score,
                    "content": fact_text,
                    "id": f.get("id", ""),
                    "tags": f.get("tags", []),
                    "priority": f.get("priority", 1.0),
                    "source_ids": f.get("source_ids", []),
                })

        results.sort(key=lambda r: r["score"], reverse=True)
        return results[:limit]

    def forget_low_priority(self, threshold: float = 0.3):
        """遗忘低优先级事实(软删除: 降低优先级)"""
        facts = self._read_all()
        updated = []
        for f in facts:
            if f.get("priority", 1.0) < threshold:
                f["priority"] = 0  # 标记为已遗忘
            updated.append(f)

        with open(self._facts_path, "w", encoding="utf-8") as fp:
            for f in updated:
                if f.get("priority", 0) > 0:
                    fp.write(json.dumps(f, ensure_ascii=False) + "\n")

    def decay_unused(self, decay_days: int = 90, forget_days: int = 180):
        """长期未使用的低价值事实会降权，最终软遗忘。"""
        facts = self._read_all()
        if not facts:
            return

        now = time.time()
        decay_seconds = max(1, int(decay_days)) * 86400
        forget_seconds = max(int(forget_days), int(decay_days)) * 86400
        changed = False

        for fact in facts:
            if fact.get("status", "active") != "active":
                continue

            last_seen = fact.get("last_accessed") or fact.get("updated_at") or fact.get("created_at") or now
            age = now - float(last_seen)
            access_count = int(fact.get("access_count", 0) or 0)
            priority = float(fact.get("priority", 1.0) or 0)

            if age >= decay_seconds and access_count == 0 and priority <= 1.0:
                fact["priority"] = min(priority, 0.3)
                changed = True

            if age >= forget_seconds and float(fact.get("priority", 1.0) or 0) <= 0.3:
                fact["status"] = "forgotten"
                fact["forgotten_at"] = now
                changed = True

        if changed:
            self._write_all(facts)

    def count(self) -> int:
        """事实总数"""
        return len(self._read_all())

    def _read_all(self) -> list[dict]:
        if not self._facts_path.exists():
            return []
        facts = []
        with open(self._facts_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        facts.append(self._normalize_entry(json.loads(line)))
                    except json.JSONDecodeError:
                        continue
        return facts

    def _write_all(self, facts: list[dict]) -> None:
        with open(self._facts_path, "w", encoding="utf-8") as fp:
            for fact in facts:
                fp.write(json.dumps(fact, ensure_ascii=False) + "\n")

    def _normalize_entry(self, entry: dict) -> dict:
        fact = entry.get("fact", "").strip()
        normalized = entry.get("normalized") or self._normalize_text(fact)
        entry["id"] = entry.get("id") or self._fact_id(normalized)
        entry["normalized"] = normalized
        entry["tags"] = entry.get("tags", [])
        entry["priority"] = entry.get("priority", 1.0)
        entry["confidence"] = entry.get("confidence", 0.7)
        entry["created_at"] = entry.get("created_at", time.time())
        entry["updated_at"] = entry.get("updated_at", entry["created_at"])
        entry["last_accessed"] = entry.get("last_accessed", 0)
        entry["access_count"] = entry.get("access_count", 0)
        entry["source_ids"] = entry.get("source_ids", [])
        entry["status"] = entry.get("status", "active")
        return entry

    def _fact_id(self, normalized: str) -> str:
        return hashlib.sha1(normalized.encode("utf-8")).hexdigest()[:16]

    def _normalize_text(self, text: str) -> str:
        return re.sub(r"\s+", "", text.strip().lower())

    def _query_terms(self, query: str) -> list[str]:
        if not query:
            return []
        terms = set(re.findall(r"[\w\u4e00-\u9fff]+", query.lower()))
        compact = self._normalize_text(query)
        if compact:
            terms.add(compact)
        for term in list(terms):
            if re.search(r"[\u4e00-\u9fff]", term) and len(term) > 4:
                terms.update(term[i:i + 2] for i in range(len(term) - 1))
        return sorted(t for t in terms if t)

    def _relevance_score(self, fact: dict, terms: list[str]) -> float:
        if not terms:
            return fact.get("priority", 1.0) + fact.get("access_count", 0) * 0.05

        haystack = " ".join([
            fact.get("fact", ""),
            " ".join(fact.get("tags", [])),
        ]).lower()
        compact = self._normalize_text(haystack)
        score = 0.0
        for term in terms:
            if term in haystack:
                score += 2.0
            elif term in compact:
                score += 1.0

        if score:
            score += fact.get("priority", 1.0)
            score += fact.get("access_count", 0) * 0.05
            score += fact.get("confidence", 0.7) * 0.25
        return score
