package com.gzzm.lobster.memory;

import com.gzzm.lobster.common.IdGenerator;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.MemoryCategory;
import com.gzzm.lobster.common.MemoryStatus;
import com.gzzm.lobster.identity.UserContext;
import com.gzzm.platform.commons.Tools;
import net.cyan.arachne.annotation.Service;
import net.cyan.nest.annotation.Inject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * MemoryService —— 个人记忆读写 / Personal memory read/write service.
 *
 * <p>按 Claude Code 的「索引 + 正文」两层模型：
 * <ul>
 *   <li>{@link #listIndex}：供 ContextAssembler 注入稳定索引段</li>
 *   <li>{@link #search}：工具层按关键词检索索引</li>
 *   <li>{@link #read}：按 memoryId 读完整正文</li>
 *   <li>{@link #write}：写入前强制 name + description + category</li>
 * </ul>
 *
 * <p>严格按 userId 隔离；写入自动按 name 去重 + 限速。
 */
@Service
public class MemoryService {

    public static final int MAX_NAME_LEN = 80;
    public static final int MAX_DESC_LEN = 200;
    public static final int MAX_CONTENT_LEN = 2000;
    public static final int DEFAULT_INDEX_LIMIT = 120;
    private static final int WRITE_LIMIT_PER_MIN = 20;

    @Inject
    private PersonalMemoryDao memoryDao;

    private final ConcurrentHashMap<String, WriteBucket> writeBuckets = new ConcurrentHashMap<>();

    /**
     * thunwind DAO 绑定到首次使用的线程，Tomcat 线程复用 / ToolExecutor 线程会踩
     * "dao is created in a thread and used in an anther thread"；每次从容器现拿即可。
     */
    private PersonalMemoryDao dao() {
        try {
            PersonalMemoryDao d = Tools.getBean(PersonalMemoryDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* 容器不可用，回退到 @Inject 字段 */ }
        return memoryDao;
    }

    /**
     * 写入一条个人记忆。
     *
     * @param name        短标题，必填，≤{@value #MAX_NAME_LEN}
     * @param description 一句话钩子，必填，≤{@value #MAX_DESC_LEN}
     * @param content     完整正文，可选，≤{@value #MAX_CONTENT_LEN}
     * @param category    分类，必填
     * @param sourceType  来源标记，可空（默认 explicit_user_write）
     * @return 新建或更新后的记忆
     */
    public PersonalMemory write(UserContext user, String name, String description,
                                String content, MemoryCategory category, String sourceType) throws Exception {
        if (user == null) throw new LobsterException("memory.auth", "Unauthenticated");
        if (name == null || name.trim().isEmpty()) {
            throw new LobsterException("memory.invalid", "Memory name is required");
        }
        if (description == null || description.trim().isEmpty()) {
            throw new LobsterException("memory.invalid", "Memory description is required");
        }
        if (category == null) {
            throw new LobsterException("memory.invalid", "Memory category is required");
        }
        String nameT = name.trim();
        String descT = description.trim();
        String contentT = content == null ? null : content.trim();
        if (nameT.length() > MAX_NAME_LEN) {
            throw new LobsterException("memory.too_long", "Name over " + MAX_NAME_LEN);
        }
        if (descT.length() > MAX_DESC_LEN) {
            throw new LobsterException("memory.too_long", "Description over " + MAX_DESC_LEN);
        }
        if (contentT != null && contentT.length() > MAX_CONTENT_LEN) {
            throw new LobsterException("memory.too_long", "Content over " + MAX_CONTENT_LEN);
        }
        checkWriteRate(user.getUserId());

        PersonalMemoryDao d = dao();
        Date now = new Date();
        PersonalMemory existing = d.findByName(user.getUserId(), nameT, MemoryStatus.active);
        if (existing != null) {
            return doUpdate(d, existing, descT, contentT, now);
        }
        PersonalMemory m = new PersonalMemory();
        m.setMemoryId(IdGenerator.memoryId());
        m.setUserId(user.getUserId());
        m.setCategory(category);
        m.setName(nameT);
        m.setDescription(descT);
        m.setContent(contentT);
        m.setSourceType(sourceType == null || sourceType.isEmpty() ? "explicit_user_write" : sourceType);
        m.setStatus(MemoryStatus.active);
        m.setCreateTime(now);
        m.setUpdateTime(now);
        try {
            d.save(m);
            return m;
        } catch (Throwable t) {
            // 竞态：另一个线程已用同名插入；DB 的 (userId, name) 唯一索引抛错。
            // 重新查并走更新路径，语义与 findByName 命中一致。
            PersonalMemory racer = d.findByName(user.getUserId(), nameT, MemoryStatus.active);
            if (racer != null) {
                try { Tools.log("[MemoryService] write race resolved as update: " + nameT, t); } catch (Throwable ignore) { /* ignore */ }
                return doUpdate(d, racer, descT, contentT, now);
            }
            // 不是唯一冲突，原样抛
            throw t instanceof Exception ? (Exception) t : new Exception(t);
        }
    }

    private PersonalMemory doUpdate(PersonalMemoryDao d, PersonalMemory existing,
                                    String descT, String contentT, Date now) throws Exception {
        d.updateBody(descT, contentT, now, existing.getMemoryId(), existing.getUserId());
        existing.setDescription(descT);
        existing.setContent(contentT);
        existing.setUpdateTime(now);
        return existing;
    }

    /**
     * 取用户的全量「索引视图」，供 ContextAssembler 组装稳定索引段。
     * 返回顺序为 createTime asc（新记忆在列表末尾），利于 prompt-cache 命中。
     * 返回条目预期只使用 memoryId / name / description / category / updateTime 字段——
     * {@code content} 已在实体上标记 {@code @Lazy}，不会真正拉取。
     */
    public List<PersonalMemory> listIndex(UserContext user, int limit) throws Exception {
        if (user == null) throw new LobsterException("memory.auth", "Unauthenticated");
        int l = limit <= 0 ? DEFAULT_INDEX_LIMIT : Math.min(limit, 500);
        List<PersonalMemory> newestFirst = dao().listIndex(user.getUserId(), MemoryStatus.active, l);
        Collections.reverse(newestFirst);
        return newestFirst;
    }

    /** 分类筛选——管理后台按 tab 切换用；同样按 createTime asc 呈现。 */
    public List<PersonalMemory> listByCategory(UserContext user, MemoryCategory category, int limit) throws Exception {
        if (user == null) throw new LobsterException("memory.auth", "Unauthenticated");
        int l = limit <= 0 ? DEFAULT_INDEX_LIMIT : Math.min(limit, 500);
        List<PersonalMemory> newestFirst = dao().listByCategory(user.getUserId(), category, MemoryStatus.active, l);
        Collections.reverse(newestFirst);
        return newestFirst;
    }

    /**
     * 关键词检索，匹配 name / description / content 任一字段；可选按 {@code category}
     * 进一步过滤。空 / 过短 query 退化为 {@link #listIndex}；如同时给了 category，则
     * 走 {@link #listByCategory}。
     */
    public List<PersonalMemory> search(UserContext user, String query, MemoryCategory category,
                                       int maxResults) throws Exception {
        if (user == null) throw new LobsterException("memory.auth", "Unauthenticated");
        int limit = maxResults <= 0 ? 10 : Math.min(maxResults, 50);
        String q = query == null ? "" : query.trim();
        // 短查询（<2 字）bigram 无意义，直接退化
        if (q.length() < 2) {
            return category == null
                    ? listIndex(user, limit)
                    : listByCategory(user, category, limit);
        }
        Set<String> tokens = extractBigrams(q);
        List<PersonalMemory> out = new ArrayList<>();
        Set<String> seen = new LinkedHashSet<>();
        PersonalMemoryDao d = dao();
        for (String tok : tokens) {
            if (tok.length() < 2) continue;
            List<PersonalMemory> hits = d.searchSimple(user.getUserId(), MemoryStatus.active, "%" + tok + "%", limit);
            for (PersonalMemory m : hits) {
                if (category != null && m.getCategory() != category) continue;
                if (seen.add(m.getMemoryId())) {
                    out.add(m);
                    if (out.size() >= limit) return out;
                }
            }
        }
        return out;
    }

    /**
     * 按 memoryId 读完整正文，并校验归属。
     * 同时 touch 访问时间——lastAccessedAt 是后续做 GC / 自动淡化的信号。
     */
    public PersonalMemory read(UserContext user, String memoryId) throws Exception {
        if (user == null) throw new LobsterException("memory.auth", "Unauthenticated");
        if (memoryId == null || memoryId.isEmpty()) {
            throw new LobsterException("memory.invalid", "memoryId required");
        }
        PersonalMemoryDao d = dao();
        PersonalMemory m = d.getMemory(memoryId);
        if (m == null) throw new LobsterException("memory.not_found", "Memory not found: " + memoryId);
        if (!m.getUserId().equals(user.getUserId())) {
            throw new LobsterException("memory.forbidden", "Memory does not belong to current user");
        }
        try {
            d.touch(new Date(), memoryId);
        } catch (Throwable t) {
            // touch 失败不阻断读——但必须留堆栈，否则 lastAccessedAt 长期不更新也无处排查
            try { Tools.log("[MemoryService] touch lastAccessedAt failed: " + memoryId, t); } catch (Throwable ignore) { /* ignore */ }
        }
        return m;
    }

    /** 软删除 / 抑制一条记忆（仅归属者可操作）。 */
    public int suppress(UserContext user, String memoryId) throws Exception {
        if (user == null) throw new LobsterException("memory.auth", "Unauthenticated");
        if (memoryId == null || memoryId.isEmpty()) {
            throw new LobsterException("memory.invalid", "memoryId required");
        }
        return dao().suppress(MemoryStatus.suppressed, new Date(), memoryId, user.getUserId());
    }

    private void checkWriteRate(String userId) {
        long now = System.currentTimeMillis();
        WriteBucket b = writeBuckets.computeIfAbsent(userId, new java.util.function.Function<String, WriteBucket>() {
            @Override public WriteBucket apply(String s) { return new WriteBucket(); }
        });
        synchronized (b) {
            if (now - b.windowStart >= 60_000L) {
                b.windowStart = now;
                b.count.set(0);
            }
            if (b.count.incrementAndGet() > WRITE_LIMIT_PER_MIN) {
                throw new LobsterException("memory.rate_limit", "Memory write rate limited");
            }
        }
    }

    private Set<String> extractBigrams(String s) {
        Set<String> set = new LinkedHashSet<>();
        String trimmed = s.trim();
        if (trimmed.length() <= 1) { set.add(trimmed); return set; }
        for (int i = 0; i < trimmed.length() - 1; i++) {
            char c1 = trimmed.charAt(i);
            char c2 = trimmed.charAt(i + 1);
            if (!Character.isWhitespace(c1) && !Character.isWhitespace(c2)) {
                set.add(String.valueOf(c1) + c2);
            }
        }
        return set;
    }

    private static final class WriteBucket {
        long windowStart = System.currentTimeMillis();
        final AtomicInteger count = new AtomicInteger(0);
    }
}
