// ---------------------------------------------------------------------------
// Live-tail append-only store (sub-project 5, Phase 4 Task 17).
//
// Three independent slices (`claims` / `remittances` / `activity`) that
// `useTailStream` (Phase 5) writes into as `item` events arrive. Reads
// happen via `useMergedTail` (also Phase 5), which merges each slice with
// the page's JSON fetch results and applies the page's filter.
//
// Design notes:
// - `claims` and `remittances` are key-by-id maps (id is stable), so
//   duplicate snapshots are dedup'd by `addClaim`/`addRemittance` (first
//   write wins — the snapshot replay on reconnect must not trample the
//   canonical row).
// - `activity` is an append-only array (event ids aren't guaranteed to
//   be unique across snapshots; the ActivityLog renders in arrival
//   order).
// - Each slice is FIFO-capped at `TAIL_CAP` (10 000) so a runaway tail
//   can't grow the heap unbounded. Oldest entries are evicted in the
//   add function itself — `reset` is what production calls when a new
//   stream opens.
// ---------------------------------------------------------------------------

import { create } from "zustand";
import type { Activity, Claim, Remittance } from "@/types";
import type { TailResource } from "@/lib/tail-stream";

/** Maximum number of items retained per slice before FIFO eviction kicks in. */
export const TAIL_CAP = 10_000;

/**
 * Per-slice eviction batch. When the cap is exceeded, drop the oldest
 * `EVICT_BATCH` entries in a single `set()` call. Picking a batch > 1
 * amortizes the cost of eviction when a high-volume stream pushes many
 * items per frame — and 100 is small enough that a 10 005-item insert
 * still lands within one `set()` call.
 *
 * The store does NOT require this batch to be exactly the overflow
 * amount — it always drains down to the cap, so a +5 overflow evicts 5
 * even when EVICT_BATCH is 100. The batch is just an upper bound on how
 * many we delete in a single pass.
 */
const EVICT_BATCH = 100;

interface TailStore {
  // --- Slices (keyed by id for the two that have stable ids) -----------
  claims: Record<string, Claim>;
  remittances: Record<string, Remittance>;
  activity: Activity[];

  // --- Insertion-order trackers (kept in sync with the dicts above) ----
  // These are private to the store; consumers only read the dicts. We
  // store them as part of the state so they reactively update with the
  // same `set()` call (zustand shallow-merges, so the new array ref is
  // what triggers a re-render in subscribers that select `claims`).
  claimOrder: string[];
  remitOrder: string[];

  // --- Setters ---------------------------------------------------------
  addClaim: (c: Claim) => void;
  addRemittance: (r: RemitListItem) => void;
  addActivity: (a: Activity) => void;
  reset: (resource: TailResource) => void;
}

/**
 * The spec's sketch uses the placeholder name `RemitListItem` for the
 * remittance shape; locally we just use the existing `Remittance` type
 * from `@/types` (same value, no need to add a duplicate).
 */
type RemitListItem = Remittance;

function evictOldest(
  order: string[],
  dict: Record<string, Claim | Remittance>,
  batch: number,
): { order: string[]; dict: Record<string, Claim | Remittance> } {
  if (order.length <= TAIL_CAP) return { order, dict };
  // Drain down to the cap; never evict more than `batch` per call so a
  // 5-item overflow evicts 5, but a 1 000-item overflow evicts 100 in
  // this pass and the remaining 900 in subsequent add() calls.
  const toDrop = Math.min(batch, order.length - TAIL_CAP);
  const dropped = order.slice(0, toDrop);
  const nextOrder = order.slice(toDrop);
  // Object.assign is consistently faster than `{ ...dict }` in V8 for
  // large dicts; we follow up with `delete` for each evicted id.
  const nextDict: Record<string, Claim | Remittance> = Object.assign({}, dict);
  for (const id of dropped) delete nextDict[id];
  return { order: nextOrder, dict: nextDict };
}

export const useTailStore = create<TailStore>((set) => ({
  claims: {},
  remittances: {},
  activity: [],
  claimOrder: [],
  remitOrder: [],

  addClaim: (c) =>
    set((s) => {
      // Dedup: first write wins. The snapshot replay on reconnect
      // produces the same id repeatedly; we want the first occurrence
      // to stick so the canonical row isn't overwritten by an older
      // version that happened to be in the snapshot.
      if (s.claims[c.id]) return s;
      // Object.assign is faster than `{ ...s.claims, [c.id]: c }` for
      // large dicts; this hot path is called once per `item` event.
      const nextClaims: Record<string, Claim> = Object.assign({}, s.claims, {
        [c.id]: c,
      });
      const nextOrder = s.claimOrder.concat(c.id);
      if (nextOrder.length > TAIL_CAP) {
        const { order, dict } = evictOldest(nextOrder, nextClaims, EVICT_BATCH);
        return { claims: dict as Record<string, Claim>, claimOrder: order };
      }
      return { claims: nextClaims, claimOrder: nextOrder };
    }),

  addRemittance: (r) =>
    set((s) => {
      if (s.remittances[r.id]) return s;
      const nextRemits: Record<string, Remittance> = Object.assign(
        {},
        s.remittances,
        { [r.id]: r },
      );
      const nextOrder = s.remitOrder.concat(r.id);
      if (nextOrder.length > TAIL_CAP) {
        const { order, dict } = evictOldest(nextOrder, nextRemits, EVICT_BATCH);
        return {
          remittances: dict as Record<string, Remittance>,
          remitOrder: order,
        };
      }
      return { remittances: nextRemits, remitOrder: nextOrder };
    }),

  addActivity: (a) =>
    set((s) => {
      // Activity has no stable id, so it's a plain append. FIFO cap
      // evicts the oldest with a single `slice` (the typical case is
      // +1, +1, +1; an extreme burst falls back to multiple slice
      // passes).
      let next = [...s.activity, a];
      if (next.length > TAIL_CAP) {
        next = next.slice(next.length - TAIL_CAP);
      }
      return { activity: next };
    }),

  reset: (resource) =>
    set(() => {
      switch (resource) {
        case "claims":
          return { claims: {}, claimOrder: [] };
        case "remittances":
          return { remittances: {}, remitOrder: [] };
        case "activity":
          return { activity: [] };
      }
    }),
}));
