import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.*;
import java.util.*;
import java.util.regex.Pattern;

/** Java 21, standard library only. Educational, single-process, offline memory. */
public class MemoryDemo {
    public static final Instant NOW = Instant.parse("2026-09-23T08:00:00Z");
    public record Candidate(String slot, String value) {}
    public record Evidence(String eventId, String quote, Instant observedAt) {}
    public record Memory(String id, String user, String slot, String value, int version,
                         Instant validFrom, Instant validUntil, List<Evidence> evidence) {
        public Memory { evidence = List.copyOf(evidence); }
        public boolean activeAt(Instant at) {
            return !at.isBefore(validFrom) && (validUntil == null || at.isBefore(validUntil));
        }
    }
    public record Hit(Memory memory, int score) {}
    private record EventKey(String user, String eventId) {}
    private record Receipt(String fingerprint, String memoryId) {}
    private record SlotKey(String user, String slot) {}
    private record Rule(Pattern pattern, String slot) {}
    private static final List<Rule> RULES = List.of(
        new Rule(Pattern.compile("^我主要写 ([A-Za-z0-9+#. -]{1,80})$"), "background.primary"),
        new Rule(Pattern.compile("^我的主要语言改为 ([A-Za-z0-9+#. -]{1,80})$"), "background.primary"),
        new Rule(Pattern.compile("^我最近在学习 ([^？?\\n]{1,80})$"), "goal.current"),
        new Rule(Pattern.compile("^我学到 ([^？?\\n]{1,80})$"), "progress.current")
    );
    public static Optional<Candidate> extract(String text) {
        String normalized = required(text).strip();
        for (Rule rule : RULES) {
            var matcher = rule.pattern().matcher(normalized);
            if (matcher.matches()) return Optional.of(new Candidate(rule.slot(), matcher.group(1).strip()));
        }
        return Optional.empty();
    }
    public static Set<String> terms(String text) {
        var result = new TreeSet<String>();
        var matcher = Pattern.compile("[a-z0-9+#.]+|[\\p{IsHan}]+")
            .matcher(text.toLowerCase(Locale.ROOT));
        while (matcher.find()) {
            String token = matcher.group();
            int[] points = token.codePoints().toArray();
            if (Character.UnicodeScript.of(points[0]) == Character.UnicodeScript.HAN) {
                if (points.length == 1) result.add(token);
                for (int i = 0; i + 1 < points.length; i++) result.add(new String(points, i, 2));
            } else result.add(token);
        }
        return result;
    }
    private static String required(String value) {
        if (value == null || value.isBlank()) throw new IllegalArgumentException("blank value");
        return value;
    }
    private static String fingerprint(String text, Instant from, Instant until) {
        try {
            byte[] bytes = (text + "\u0000" + from + "\u0000" + until).getBytes(StandardCharsets.UTF_8);
            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
        } catch (java.security.NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
    }
    public static class Store {
        private final Clock clock;
        private final Map<String, Memory> memories = new LinkedHashMap<>();
        private final Map<EventKey, Receipt> receipts = new HashMap<>();
        private final Map<SlotKey, Integer> versions = new HashMap<>();
        private long sequence;
        public Store(Clock clock) { this.clock = Objects.requireNonNull(clock); }

        /** Same event + same payload is idempotent, including after deletion. */
        public synchronized Optional<Memory> remember(String user, String eventId, String text,
                                                      Instant observedAt, Instant validUntil) {
            required(user); required(eventId); required(text); Objects.requireNonNull(observedAt);
            if (observedAt.isAfter(clock.instant())) throw new IllegalArgumentException("future event");
            if (validUntil != null && !validUntil.isAfter(observedAt))
                throw new IllegalArgumentException("empty validity window");
            var key = new EventKey(user, eventId);
            String hash = fingerprint(text, observedAt, validUntil);
            Receipt receipt = receipts.get(key);
            if (receipt != null) {
                if (!receipt.fingerprint().equals(hash)) throw new IllegalArgumentException("event payload changed");
                return Optional.ofNullable(memories.get(receipt.memoryId()));
            }
            var candidate = extract(text);
            if (candidate.isEmpty()) return Optional.empty();
            Candidate c = candidate.get();
            var slotKey = new SlotKey(user, c.slot());
            Memory previous = memories.values().stream()
                .filter(m -> m.user().equals(user) && m.slot().equals(c.slot()))
                .max(Comparator.comparingInt(Memory::version)).orElse(null);
            if (previous != null && observedAt.isBefore(previous.validFrom()))
                throw new IllegalArgumentException("out-of-order event");
            var evidence = new Evidence(eventId, text, observedAt);
            if (previous != null && previous.activeAt(observedAt) && previous.value().equals(c.value())
                    && Objects.equals(previous.validUntil(), validUntil)) {
                var sources = new ArrayList<>(previous.evidence()); sources.add(evidence);
                Memory merged = new Memory(previous.id(), user, c.slot(), c.value(), previous.version(),
                    previous.validFrom(), previous.validUntil(), sources);
                memories.put(merged.id(), merged); receipts.put(key, new Receipt(hash, merged.id()));
                return Optional.of(merged);
            }
            if (previous != null && observedAt.equals(previous.validFrom()))
                throw new IllegalArgumentException("ambiguous simultaneous update");
            if (previous != null && previous.activeAt(observedAt)) {
                memories.put(previous.id(), new Memory(previous.id(), user, previous.slot(), previous.value(),
                    previous.version(), previous.validFrom(), observedAt, previous.evidence()));
            }
            int version = versions.getOrDefault(slotKey, 0) + 1;
            var next = new Memory("m-" + (++sequence), user, c.slot(), c.value(), version,
                observedAt, validUntil, List.of(evidence));
            versions.put(slotKey, version); memories.put(next.id(), next);
            receipts.put(key, new Receipt(hash, next.id()));
            return Optional.of(next);
        }
        public synchronized List<Memory> current(String user) {
            required(user);
            return memories.values().stream().filter(m -> m.user().equals(user) && m.activeAt(clock.instant()))
                .sorted(Comparator.comparing(Memory::slot).thenComparing(Memory::id)).toList();
        }
        public synchronized List<Memory> history(String user, String slot) {
            required(user); required(slot);
            return memories.values().stream().filter(m -> m.user().equals(user) && m.slot().equals(slot))
                .sorted(Comparator.comparingInt(Memory::version)).toList();
        }
        /** Erase every revision and quote in this slot; keep receipt hashes to block old retries. */
        public synchronized int forget(String user, String slot) {
            required(user); required(slot);
            int before = memories.size();
            memories.values().removeIf(m -> m.user().equals(user) && m.slot().equals(slot));
            return before - memories.size();
        }
        public synchronized List<Hit> search(String user, String query, int limit) {
            if (limit < 0) throw new IllegalArgumentException("negative limit");
            Set<String> queryTerms = terms(Objects.requireNonNull(query));
            return current(user).stream().map(m -> {
                var overlap = new HashSet<>(terms(m.value())); overlap.retainAll(queryTerms);
                return new Hit(m, overlap.size());
            }).filter(h -> h.score() > 0)
                .sorted(Comparator.comparingInt(Hit::score).reversed()
                    .thenComparing((Hit h) -> h.memory().validFrom(), Comparator.reverseOrder())
                    .thenComparing(h -> h.memory().id()))
                .limit(limit).toList();
        }
        /** Character budget includes labels and evidence IDs; this is not a token counter. */
        public synchronized String context(String user, String query, int maxCodePoints) {
            if (maxCodePoints < 0) throw new IllegalArgumentException("negative budget");
            StringBuilder out = new StringBuilder();
            int used = 0;
            for (Hit hit : search(user, query, Integer.MAX_VALUE)) {
                Memory m = hit.memory();
                String line = "[" + m.id() + " " + m.slot() + " source=" + m.evidence().getFirst().eventId()
                    + "] " + m.value() + "\n";
                int size = line.codePointCount(0, line.length());
                if (used + size > maxCodePoints) continue;
                out.append(line); used += size;
            }
            return out.toString();
        }
    }
    public static Store learningAssistant() {
        var store = new Store(Clock.fixed(NOW, ZoneOffset.UTC));
        store.remember("alice", "e1", "我主要写 Java", NOW.minusSeconds(3600), null);
        store.remember("alice", "e2", "我最近在学习 Python", NOW.minusSeconds(2400), NOW.plusSeconds(86400));
        store.remember("alice", "e3", "我学到 Python 生成器", NOW.minusSeconds(1200), null);
        store.remember("alice", "e4", "我的主要语言改为 Kotlin", NOW.minusSeconds(600), null);
        store.remember("bob", "e1", "我主要写 Python", NOW.minusSeconds(3600), null);
        return store;
    }
    public static void main(String[] args) {
        var store = learningAssistant();
        System.out.println("Offline educational demo; fixed clock: " + NOW);
        for (Memory memory : store.current("alice"))
            System.out.println(memory.slot() + "=" + memory.value() + " v" + memory.version());
        System.out.println("Context for Python:\n" + store.context("alice", "Python", 200));
        System.out.println("Erased goal revisions: " + store.forget("alice", "goal.current"));
        System.out.println("Remaining Python hits: " + store.search("alice", "Python", 10).size());
    }
}
