"""
agentpaas.engine.memory_store — Persistent cross-run agent memory.

Implements a simplified version of the Letta 3-layer memory model:

  Layer 1 — Core Memory (always in context):
      Small structured dict: persona, user facts, task context.
      Stored in {data_dir}/core_memory.json.

  Layer 2 — Recall Memory (recent interactions):
      Append-only JSONL log of (run_id, input, output, timestamp).
      Stored in {data_dir}/recall_log.jsonl.
      Bounded to last MAX_RECALL entries to control size.

  Layer 3 — Archival Memory (searchable long-term store):
      Delegated to the Knowledge Base system (BM25 indexes).
      This module only handles layers 1-2.

Usage in _execute_agent:
    ctx.memory = load_memory(agent_dir)    # before term.apply()
    save_memory(agent_dir, ctx.memory, run_id, input_text, str(result))
"""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any, Dict

MAX_RECALL = 50  # keep last N run summaries in recall log


def _memory_dir(agent_dir: str) -> Path:
    """Return the memory sub-directory for an agent, creating it if needed."""
    p = Path(agent_dir) / "memory"
    p.mkdir(parents=True, exist_ok=True)
    return p


def load_memory(agent_dir: str) -> Dict[str, Any]:
    """Load Core Memory dict from {agent_dir}/memory/core_memory.json.

    Returns an empty dict if the file does not exist yet.
    The dict is injected into ctx.memory before term execution so that
    Memory(agent, store) terms can read/write it within the run.
    """
    if not agent_dir:
        return {}
    core_file = _memory_dir(agent_dir) / "core_memory.json"
    if not core_file.exists():
        return {}
    try:
        return json.loads(core_file.read_text(encoding="utf-8"))
    except Exception:
        return {}


def save_memory(
    agent_dir: str,
    memory: Dict[str, Any],
    run_id: str = "",
    input_text: str = "",
    output_text: str = "",
) -> None:
    """Persist Core Memory and append a Recall entry.

    Args:
        agent_dir:   Agent / instance directory.
        memory:      ctx.memory dict after run completion.
        run_id:      Run identifier for recall log.
        input_text:  User input (truncated to 500 chars in recall).
        output_text: Agent output (truncated to 500 chars in recall).
    """
    if not agent_dir:
        return
    mem_dir = _memory_dir(agent_dir)

    # ── Layer 1: save Core Memory ────────────────────────────────
    try:
        core_file = mem_dir / "core_memory.json"
        tmp = core_file.with_suffix(".json.tmp")
        tmp.write_text(
            json.dumps(memory, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        tmp.replace(core_file)
    except Exception:
        pass

    # ── Layer 2: append Recall log ───────────────────────────────
    if not run_id:
        return
    try:
        from agentpaas.db.models import now_utc
        entry = json.dumps({
            "run_id": run_id,
            "input": input_text[:500],
            "output": output_text[:500],
            "ts": now_utc(),
        }, ensure_ascii=False)

        recall_file = mem_dir / "recall_log.jsonl"
        existing_lines: list[str] = []
        if recall_file.exists():
            existing_lines = recall_file.read_text(encoding="utf-8").splitlines()

        # Trim to MAX_RECALL - 1, then append new entry
        trimmed = existing_lines[-(MAX_RECALL - 1):] + [entry]
        recall_file.write_text("\n".join(trimmed) + "\n", encoding="utf-8")
    except Exception:
        pass


def load_recall_summary(agent_dir: str, n: int = 5) -> str:
    """Return the last N recall entries as a readable string for prompt injection.

    Example output:
        [上次对话]
        Run run_abc: Q: ... A: ...
        Run run_xyz: Q: ... A: ...
    """
    if not agent_dir:
        return ""
    recall_file = _memory_dir(agent_dir) / "recall_log.jsonl"
    if not recall_file.exists():
        return ""
    try:
        lines = recall_file.read_text(encoding="utf-8").splitlines()
        recent = lines[-n:] if len(lines) >= n else lines
        entries = []
        for line in recent:
            try:
                d = json.loads(line)
                entries.append(
                    f"Run {d.get('run_id','?')} ({d.get('ts','?')[:10]}): "
                    f"Q: {d.get('input','')[:100]}  A: {d.get('output','')[:100]}"
                )
            except Exception:
                pass
        if not entries:
            return ""
        return "[近期对话记录]\n" + "\n".join(entries)
    except Exception:
        return ""


# ── 核心记忆候选（L2 自动提炼，记忆 P3，MEMORY_DESIGN §4）─────────────────────
# 待确认项单列独立文件 memory_candidates.json，**不混进 core_memory.json**
# → 不污染 Memory term 注入；确认后才并入 core。评审#10：不自动转正。
import re as _re

CANDIDATES_FILE = "memory_candidates.json"
MAX_CORE_FACTS = 30          # 已确认正式区上限（满则淘汰最早）

# 双层红线之第二层（评审#10）：prompt 失效兜底，落库前正则拦截敏感内容
_SENSITIVE_PATTERNS = [
    _re.compile(r"\d{17}[\dXx]"),                       # 身份证号
    _re.compile(r"(成绩|分数|得分|考了|挂科).{0,4}\d"),    # 成绩
    _re.compile(r"(艾滋|抑郁|癌|乙肝|残疾|精神病|确诊)"),  # 健康
    _re.compile(r"1[3-9]\d{9}"),                         # 手机号
]


def is_sensitive(text: str) -> bool:
    t = text or ""
    return any(p.search(t) for p in _SENSITIVE_PATTERNS)


def _candidates_path(agent_dir: str) -> Path:
    return _memory_dir(agent_dir) / CANDIDATES_FILE


def load_candidates(agent_dir: str) -> List[Dict[str, Any]]:
    if not agent_dir:
        return []
    p = _candidates_path(agent_dir)
    if not p.exists():
        return []
    try:
        return json.loads(p.read_text(encoding="utf-8")) or []
    except Exception:
        return []


def _save_candidates(agent_dir: str, items: List[Dict[str, Any]]) -> None:
    p = _candidates_path(agent_dir)
    tmp = p.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
    tmp.replace(p)


def add_candidate(agent_dir: str, fact: str, confidence: float,
                  source_run: str = "") -> bool:
    """加一条候选。敏感内容（第二层红线）或重复 → 拒绝，返回 False。"""
    fact = (fact or "").strip()
    if not fact or is_sensitive(fact):
        return False
    items = load_candidates(agent_dir)
    if any(c.get("fact") == fact for c in items):
        return False
    core = load_memory(agent_dir)
    if fact in (core.get("_facts") or []):       # 已确认的不再提
        return False
    from agentpaas.db.models import now_utc
    items.append({"fact": fact, "confidence": round(float(confidence), 2),
                  "source_run": source_run, "created_at": now_utc()})
    _save_candidates(agent_dir, items[-50:])     # 候选区上限
    return True


def confirm_candidate(agent_dir: str, fact: str) -> bool:
    """把一条候选转正：从 candidates 移除、并入 core._facts（含容量淘汰）。"""
    items = load_candidates(agent_dir)
    if not any(c.get("fact") == fact for c in items):
        return False
    _save_candidates(agent_dir, [c for c in items if c.get("fact") != fact])
    core = load_memory(agent_dir)
    facts = list(core.get("_facts") or [])
    if fact not in facts:
        facts.append(fact)
        facts = facts[-MAX_CORE_FACTS:]
    core["_facts"] = facts
    core_file = _memory_dir(agent_dir) / "core_memory.json"
    tmp = core_file.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(core, ensure_ascii=False, indent=2), encoding="utf-8")
    tmp.replace(core_file)
    return True


def reject_candidate(agent_dir: str, fact: str) -> bool:
    items = load_candidates(agent_dir)
    new = [c for c in items if c.get("fact") != fact]
    if len(new) == len(items):
        return False
    _save_candidates(agent_dir, new)
    return True
