package chat.vojo.app;

import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Per-room MessagingStyle history cache.
 *
 * Stores the last N messages observed for each room so renderMessageNotification
 * can rebuild a NotificationCompat.MessagingStyle with conversation context on
 * every new event instead of posting a fresh single-message notification per
 * event. Without this every 5-message DM produced 5 distinct entries in the
 * shade; with it the user sees one expandable conversation per room — the
 * WhatsApp/Telegram convention.
 *
 * Thread-safety: ConcurrentHashMap + per-key synchronized mutation via the
 * compute() / get() pattern. Both VojoFirebaseMessagingService.onMessageReceived
 * (Firebase-managed thread) and VojoPollWorker.doWork (WorkManager executor)
 * mutate the cache; without serialization a same-room FCM + polling race could
 * lose a message. Mutations are short — only deque append + bounded trim.
 *
 * Persistence: in-memory only. After process kill the cache is empty, and
 * renderMessageNotification falls back to extractMessagingStyleFromNotification
 * to recover history from the live system shade. If the user dismissed the
 * notification too, the conversation legitimately starts fresh — no signal we
 * could recover from there anyway.
 *
 * Eviction: bounded at MAX_MESSAGES_PER_ROOM per room, with FIFO eviction
 * (oldest message at the head of the deque is dropped via pollFirst when the
 * append would exceed the cap). Map itself is unbounded; in practice the
 * dump from dismissRoom (when a server-side read receipt clears unread) keeps
 * the room count proportional to active conversations. For safety against
 * runaway growth from rooms the user never reads, we cap the map at MAX_ROOMS.
 */
final class RoomMessageCache {

    // Element-android keeps a similar in-memory queue (NotificationEventQueue);
    // 20 messages per room is generous enough for an active group chat while
    // staying well under Android's MessagingStyle render budget — Android only
    // shows the last ~7 messages in the shade anyway.
    private static final int MAX_MESSAGES_PER_ROOM = 20;

    // Hard cap on the map size so a long-running session that touches many
    // rooms without ever clearing receipts can't slowly leak memory.
    // Eviction is approximate (oldest-touched first via insertion order from
    // ConcurrentHashMap is NOT guaranteed, so we just clear the oldest by
    // arbitrary entry on overflow — acceptable for an LRU at this scale).
    private static final int MAX_ROOMS = 200;

    private static final ConcurrentHashMap<String, Deque<Entry>> store =
        new ConcurrentHashMap<>();

    private RoomMessageCache() {}

    /**
     * Snapshot of a single rendered message. We can't store
     * NotificationCompat.MessagingStyle.Message directly because Person's
     * Icon field is not safely shareable across threads / not cheap to
     * rebuild on every poll. Building the Message at render time from this
     * record matches element-android's RoomGroupMessageCreator pattern.
     */
    static final class Entry {
        // Matrix event_id when known (incoming pushes always carry one;
        // outgoing optimistic-echo entries pass null). Used by append() to
        // suppress duplicate appends when FCM retries / cross-source
        // delivery hands the same event in twice — without this the
        // MessagingStyle conversation would render the same message N
        // times in the shade.
        final String eventId;
        final String body;
        final long timestamp;
        final String senderKey;
        final String senderName;
        final boolean fromSelf;

        Entry(
            String eventId,
            String body,
            long timestamp,
            String senderKey,
            String senderName,
            boolean fromSelf
        ) {
            this.eventId = eventId;
            this.body = body;
            this.timestamp = timestamp;
            this.senderKey = senderKey;
            this.senderName = senderName;
            this.fromSelf = fromSelf;
        }
    }

    /**
     * Append a message to the room's history and return an ordered snapshot
     * including the newly-added entry. Snapshot is taken INSIDE the atomic
     * compute() so a concurrent append for the same room can't mutate the
     * deque between our addLast and our copy. Returning the deque reference
     * and copying outside is unsafe — ConcurrentHashMap.compute serialises
     * only the lambda body per key, not subsequent reads of the value.
     */
    static List<Entry> append(String roomId, Entry entry) {
        if (roomId == null || roomId.isEmpty() || entry == null) {
            return java.util.Collections.emptyList();
        }
        final List<Entry> snapshot = new ArrayList<>();
        store.compute(roomId, (key, existing) -> {
            Deque<Entry> d = (existing != null) ? existing : new ArrayDeque<>();
            // Dedup by eventId — protects against FCM retry / cross-source
            // (FCM + polling Worker) double-delivery that would otherwise
            // append the same event twice. Only applies when both the new
            // entry and a prior one carry a non-empty eventId; outgoing
            // self-echo entries have null eventId by design and never
            // collide.
            boolean isDup = false;
            if (entry.eventId != null && !entry.eventId.isEmpty()) {
                for (Entry prior : d) {
                    if (entry.eventId.equals(prior.eventId)) {
                        isDup = true;
                        break;
                    }
                }
            }
            if (!isDup) {
                d.addLast(entry);
                while (d.size() > MAX_MESSAGES_PER_ROOM) {
                    d.pollFirst();
                }
            }
            snapshot.addAll(d);
            return d;
        });
        // Bound the map. Iteration order of ConcurrentHashMap is unspecified
        // and the size() check is racy with concurrent puts; we accept ±1
        // eviction precision at the 200-room cap as an acceptable approximation
        // of LRU (the alternative is a global lock on every append which is
        // far more expensive than letting the cache drift by one).
        if (store.size() > MAX_ROOMS) {
            java.util.Iterator<String> it = store.keySet().iterator();
            while (it.hasNext() && store.size() > MAX_ROOMS) {
                String key = it.next();
                if (!key.equals(roomId)) it.remove();
            }
        }
        return snapshot;
    }

    /**
     * Seed the room's history from an already-posted MessagingStyle (recovered
     * via NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification
     * after process kill). Idempotent: if the room already has cached entries
     * we leave them alone — they are by construction at least as recent.
     */
    static void seedIfAbsent(String roomId, List<Entry> entries) {
        if (roomId == null || roomId.isEmpty() || entries == null || entries.isEmpty()) return;
        store.computeIfAbsent(roomId, key -> {
            Deque<Entry> d = new ArrayDeque<>();
            for (Entry e : entries) {
                d.addLast(e);
                while (d.size() > MAX_MESSAGES_PER_ROOM) d.pollFirst();
            }
            return d;
        });
    }

    /** Drop all cached messages for a room (e.g. on receipt-driven dismiss). */
    static void clear(String roomId) {
        if (roomId == null || roomId.isEmpty()) return;
        store.remove(roomId);
    }
}
