/**
 * Pure store logic for the **inter-agent announcements** feature (ticket #4, F1).
 *
 * When an agent contacts another via `idea_ask_agent`, the backend streams the
 * target's intermediate réflexions as `agentAnnouncement` domain events. These
 * are **ephemeral** live signals — never persisted — folded here into a bounded
 * index keyed by `(target, ticketId)`:
 *
 *   - the **target cell** overlays the announcements destined to it (F3);
 *   - the **requester cell** previews the announcements it is waiting on (F2),
 *     filtered by `requester == self` — the target thread is shared across
 *     requesters, so without that filter one requester would see another's
 *     announcements (Architect contract, point 7).
 *
 * The overlay's mount/retract is driven by the target's **busy/idle** state (the
 * single lifecycle authority — see `AnnouncementsProvider`), not by this index;
 * the index only carries the scrolling *content*. It is retracted per busy-cycle
 * (`purgeAllForTarget` on `busy:false`) so a fresh turn starts from a clean feed,
 * and each bucket is bounded (oldest dropped first) so a long turn cannot grow it
 * without limit.
 *
 * This module is intentionally free of React/Tauri: it is a set of immutable
 * reducers + selectors, unit-tested in isolation (`announcementsStore.test.ts`).
 */

/** A single live announcement folded from an `agentAnnouncement` event. */
export interface Announcement {
  /** Requesting party: `"user"` or the requesting agent's id. */
  requester: string;
  /** Target agent being contacted. */
  target: string;
  /** FIFO ticket correlating the inter-agent turn. */
  ticketId: string;
  /** Human-readable réflexion/annonce text. */
  text: string;
  /** Wall-clock timestamp (epoch-ms) as emitted by the backend. */
  atMs: number;
  /**
   * Monotonic local sequence assigned on ingest. Provides a stable React key and
   * a total order even when two announcements share the same `atMs`.
   */
  seq: number;
}

/**
 * Immutable announcement index. `byTarget[target][ticketId]` is a bounded list of
 * announcements ordered oldest → newest. Empty ticket/target buckets are pruned,
 * so `byTarget[target]` existing implies the target has ≥1 active ticket.
 */
export interface AnnouncementIndex {
  byTarget: Record<string, Record<string, Announcement[]>>;
  /** Next sequence number to assign (monotonic, never reused). */
  nextSeq: number;
}

/** Default per-`(target, ticketId)` bucket cap (Architect: ~20–50). */
export const DEFAULT_ANNOUNCEMENT_CAP = 50;

/** An empty index. */
export function emptyAnnouncementIndex(): AnnouncementIndex {
  return { byTarget: {}, nextSeq: 1 };
}

/** Fields carried by an `agentAnnouncement` event (sans the local `seq`). */
export type AnnouncementInput = Omit<Announcement, "seq">;

/**
 * Appends one announcement to its `(target, ticketId)` bucket, assigning the next
 * sequence and trimming the bucket to `cap` (dropping the oldest). Returns a new
 * index; the input is never mutated.
 */
export function appendAnnouncement(
  index: AnnouncementIndex,
  input: AnnouncementInput,
  cap: number = DEFAULT_ANNOUNCEMENT_CAP,
): AnnouncementIndex {
  const { target, ticketId } = input;
  const announcement: Announcement = { ...input, seq: index.nextSeq };

  const targetBuckets = index.byTarget[target] ?? {};
  const prevBucket = targetBuckets[ticketId] ?? [];
  const grown = [...prevBucket, announcement];
  // Keep only the last `cap` (bound the live index; oldest fall off first).
  const bounded = grown.length > cap ? grown.slice(grown.length - cap) : grown;

  return {
    byTarget: {
      ...index.byTarget,
      [target]: { ...targetBuckets, [ticketId]: bounded },
    },
    nextSeq: index.nextSeq + 1,
  };
}

/**
 * Retracts **all** tickets of `target` at once — called when the target goes idle
 * (`agentBusyChanged` `busy:false`), which has no ticket to scope by. Keeps the
 * overlay content ephemeral per busy-cycle: a fresh turn starts from a clean feed
 * rather than showing the previous turn's stale réflexions. No-op (stable
 * reference) when the target has nothing indexed.
 */
export function purgeAllForTarget(
  index: AnnouncementIndex,
  target: string,
): AnnouncementIndex {
  if (!(target in index.byTarget)) return index;
  const { [target]: _dropped, ...rest } = index.byTarget;
  return { byTarget: rest, nextSeq: index.nextSeq };
}

/** Orders two announcements oldest → newest (by `atMs`, tie-broken by `seq`). */
function byTime(a: Announcement, b: Announcement): number {
  return a.atMs - b.atMs || a.seq - b.seq;
}

/**
 * All active announcements destined to `target`, flattened across its tickets and
 * ordered oldest → newest. Empty when the target has no active turn (F3 source).
 */
export function announcementsForTarget(
  index: AnnouncementIndex,
  target: string,
): Announcement[] {
  const buckets = index.byTarget[target];
  if (!buckets) return [];
  return Object.values(buckets).flat().sort(byTime);
}

/** Whether `target` has ≥1 active announcement (drives the overlay's mount). */
export function targetHasActiveAnnouncements(
  index: AnnouncementIndex,
  target: string,
): boolean {
  const buckets = index.byTarget[target];
  if (!buckets) return false;
  return Object.values(buckets).some((bucket) => bucket.length > 0);
}

/**
 * All active announcements whose `requester === requester`, across every target
 * and ticket, ordered oldest → newest. This is the F2 filter: because the target
 * thread is **shared** across requesters, filtering by `requester` prevents one
 * requester from previewing another's announcements. When `ticketId` is given the
 * result is further scoped to that ticket.
 */
export function announcementsForRequester(
  index: AnnouncementIndex,
  requester: string,
  ticketId?: string,
): Announcement[] {
  const out: Announcement[] = [];
  for (const buckets of Object.values(index.byTarget)) {
    for (const [tid, bucket] of Object.entries(buckets)) {
      if (ticketId !== undefined && tid !== ticketId) continue;
      for (const ann of bucket) {
        if (ann.requester === requester) out.push(ann);
      }
    }
  }
  return out.sort(byTime);
}
