"""客户长期记忆。

长期记忆与普通聊天记录不同：聊天记录属于某次会话，长期记忆可以在新会话中
被 Agent 召回。只有用户明确要求“请记住”时才会写入，并在写入前阻止手机号、
身份证号和银行卡号等敏感标识进入记忆库。
"""

import re
from collections.abc import Callable
from dataclasses import dataclass, replace
from datetime import datetime
from typing import Protocol

from zbt.core.errors import AppError
from zbt.core.identifiers import new_ulid


@dataclass(frozen=True, slots=True)
class CustomerMemory:
    """一条归属于特定 H5 用户的长期记忆。"""

    id: str
    owner_id: str
    category: str
    content: str
    status: str
    created_at: datetime
    updated_at: datetime


@dataclass(frozen=True, slots=True)
class CustomerMemoryHit:
    memory_id: str
    score: float


class CustomerMemoryRepository(Protocol):
    """长期记忆正文的持久化接口，正式环境由 MySQL 实现。"""

    def save(self, memory: CustomerMemory) -> None: ...

    def get(self, memory_id: str) -> CustomerMemory | None: ...

    def list_active(self, owner_id: str) -> list[CustomerMemory]: ...


class CustomerMemoryIndex(Protocol):
    """长期记忆的语义索引接口，正式环境由 Milvus 实现。"""

    def upsert(self, memory: CustomerMemory) -> None: ...

    def search(
        self,
        *,
        owner_id: str,
        query: str,
        limit: int,
    ) -> list[CustomerMemoryHit]: ...

    def delete(self, memory_id: str) -> None: ...


class InMemoryCustomerMemoryRepository:
    """供单元测试使用的内存仓储，不用于跨进程保存数据。"""

    def __init__(self) -> None:
        self._items: dict[str, CustomerMemory] = {}

    def save(self, memory: CustomerMemory) -> None:
        self._items[memory.id] = memory

    def get(self, memory_id: str) -> CustomerMemory | None:
        return self._items.get(memory_id)

    def list_active(self, owner_id: str) -> list[CustomerMemory]:
        return [
            memory
            for memory in self._items.values()
            if memory.owner_id == owner_id and memory.status == "ACTIVE"
        ]


class InMemoryCustomerMemoryIndex:
    def __init__(self) -> None:
        self._items: dict[str, CustomerMemory] = {}

    def upsert(self, memory: CustomerMemory) -> None:
        self._items[memory.id] = memory

    def search(
        self,
        *,
        owner_id: str,
        query: str,
        limit: int,
    ) -> list[CustomerMemoryHit]:
        query_terms = _terms(query)
        candidates = []
        for memory in self._items.values():
            if memory.owner_id != owner_id or memory.status != "ACTIVE":
                continue
            content_terms = _terms(memory.content)
            union = query_terms | content_terms
            score = (
                len(query_terms & content_terms) / len(union)
                if union
                else 0.0
            )
            if score > 0:
                candidates.append(CustomerMemoryHit(memory.id, score))
        return sorted(
            candidates,
            key=lambda item: (-item.score, item.memory_id),
        )[:limit]

    def delete(self, memory_id: str) -> None:
        self._items.pop(memory_id, None)


class CustomerMemoryService:
    """执行长期记忆的保存、按用户召回和逻辑删除。"""

    def __init__(
        self,
        repository: CustomerMemoryRepository,
        index: CustomerMemoryIndex,
        clock: Callable[[], datetime],
    ) -> None:
        self._repository = repository
        self._index = index
        self._clock = clock

    def save(
        self,
        *,
        owner_id: str,
        category: str,
        content: str,
    ) -> dict[str, object]:
        """校验并保存一条用户主动授权的长期记忆。"""

        normalized = " ".join(content.split())
        if category not in {"PREFERENCE", "FAMILY_CONTEXT", "CONSTRAINT"}:
            raise AppError("CUSTOMER_MEMORY_CATEGORY_INVALID", "记忆类别无效", 422)
        if len(normalized) < 2 or len(normalized) > 300:
            raise AppError("CUSTOMER_MEMORY_CONTENT_INVALID", "记忆内容长度无效", 422)
        if _contains_sensitive_identifier(normalized):
            raise AppError(
                "CUSTOMER_MEMORY_SENSITIVE",
                "长期记忆不能保存手机号、身份证号或银行卡号",
                422,
            )
        now = self._clock()
        memory = CustomerMemory(
            id=new_ulid(),
            owner_id=owner_id,
            category=category,
            content=normalized,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        # 正文写入 MySQL，向量写入 Milvus；二者职责不同但使用同一个记忆编号。
        self._repository.save(memory)
        self._index.upsert(memory)
        return self._serialize(memory)

    def search(
        self,
        *,
        owner_id: str,
        query: str,
        limit: int = 3,
    ) -> dict[str, object]:
        """只在当前登录用户自己的记忆范围内进行相似度检索。"""

        memories = {
            memory.id: memory for memory in self._repository.list_active(owner_id)
        }
        hits = self._index.search(
            owner_id=owner_id,
            query=query,
            limit=min(max(limit, 1), 5),
        )
        items = [
            {
                **self._serialize(memories[hit.memory_id]),
                "score": hit.score,
            }
            for hit in hits
            if hit.memory_id in memories
        ]
        return {"items": items, "total": len(items)}

    def delete(self, *, owner_id: str, memory_id: str) -> dict[str, object]:
        """逻辑删除正文并同步移除向量，后续对话不再召回该内容。"""

        memory = self._repository.get(memory_id)
        if memory is None or memory.owner_id != owner_id or memory.status != "ACTIVE":
            raise AppError("CUSTOMER_MEMORY_NOT_FOUND", "未找到可删除的长期记忆", 404)
        disabled = replace(memory, status="DELETED", updated_at=self._clock())
        self._repository.save(disabled)
        self._index.delete(memory_id)
        return self._serialize(disabled)

    @staticmethod
    def _serialize(memory: CustomerMemory) -> dict[str, object]:
        return {
            "memory_id": memory.id,
            "category": memory.category,
            "content": memory.content,
            "status": memory.status,
            "created_at": memory.created_at,
            "updated_at": memory.updated_at,
        }


def _contains_sensitive_identifier(content: str) -> bool:
    """用确定性规则拦截常见敏感号码，不把安全判断交给大模型。"""

    patterns = (
        r"(?<!\d)1[3-9]\d{9}(?!\d)",
        r"(?<!\d)\d{17}[\dXx](?!\d)",
        r"(?<!\d)\d{16,19}(?!\d)",
    )
    return any(re.search(pattern, content) for pattern in patterns)


def _terms(content: str) -> set[str]:
    normalized = re.sub(r"\s+", "", content.casefold())
    return {
        normalized[index : index + 2]
        for index in range(max(len(normalized) - 1, 0))
    } or {normalized}
