import type { InboxStore } from './store.js';

interface BlobRow {
  msgId: string;
  ciphertext: Uint8Array;
  receivedAt: number;
  expiresAt: number;
  senderFp?: string;
}

/**
 * In-memory InboxStore — used in tests and as the default fallback when
 * neither SHADE_INBOX_DB_PATH nor SHADE_INBOX_PG_URL is set.
 *
 * Blobs are organized in per-address insertion-ordered arrays so cursor
 * pagination is just a `>` filter on `receivedAt`.
 */
export class MemoryInboxStore implements InboxStore {
  private owners = new Map<string, Uint8Array>();
  private blobs = new Map<string, BlobRow[]>();
  private nextReceivedAt = 0;

  async saveAddressOwner(address: string, signingKey: Uint8Array): Promise<void> {
    this.owners.set(address, new Uint8Array(signingKey));
  }

  async getAddressOwner(address: string): Promise<Uint8Array | null> {
    const k = this.owners.get(address);
    return k ? new Uint8Array(k) : null;
  }

  async putBlob(args: {
    address: string;
    msgId: string;
    ciphertext: Uint8Array;
    expiresAt: number;
    senderFp?: string;
  }): Promise<{ created: boolean; receivedAt: number }> {
    const list = this.blobs.get(args.address) ?? [];
    const existing = list.find((r) => r.msgId === args.msgId);
    if (existing) return { created: false, receivedAt: existing.receivedAt };
    // Monotonic `receivedAt` so cursor compare is total-order even when
    // multiple blobs land in the same millisecond.
    const receivedAt = Math.max(this.nextReceivedAt + 1, Date.now());
    this.nextReceivedAt = receivedAt;
    const row: BlobRow = {
      msgId: args.msgId,
      ciphertext: new Uint8Array(args.ciphertext),
      receivedAt,
      expiresAt: args.expiresAt,
    };
    if (args.senderFp !== undefined) row.senderFp = args.senderFp;
    list.push(row);
    this.blobs.set(args.address, list);
    return { created: true, receivedAt };
  }

  async fetchBlobs(args: {
    address: string;
    sinceCursor: number;
    now: number;
    limit: number;
  }): Promise<
    Array<{
      msgId: string;
      ciphertext: Uint8Array;
      receivedAt: number;
      expiresAt: number;
      senderFp?: string;
    }>
  > {
    const list = this.blobs.get(args.address) ?? [];
    return list
      .filter((r) => r.receivedAt > args.sinceCursor && r.expiresAt > args.now)
      .sort((a, b) => a.receivedAt - b.receivedAt)
      .slice(0, args.limit)
      .map((r) => {
        const out: {
          msgId: string;
          ciphertext: Uint8Array;
          receivedAt: number;
          expiresAt: number;
          senderFp?: string;
        } = {
          msgId: r.msgId,
          ciphertext: new Uint8Array(r.ciphertext),
          receivedAt: r.receivedAt,
          expiresAt: r.expiresAt,
        };
        if (r.senderFp !== undefined) out.senderFp = r.senderFp;
        return out;
      });
  }

  async deleteBlob(address: string, msgId: string): Promise<boolean> {
    const list = this.blobs.get(address);
    if (!list) return false;
    const idx = list.findIndex((r) => r.msgId === msgId);
    if (idx === -1) return false;
    list.splice(idx, 1);
    return true;
  }

  async countBlobs(address: string, now: number): Promise<number> {
    const list = this.blobs.get(address) ?? [];
    return list.filter((r) => r.expiresAt > now).length;
  }

  async purgeExpired(now: number): Promise<number> {
    let removed = 0;
    for (const [addr, list] of this.blobs) {
      const filtered = list.filter((r) => r.expiresAt > now);
      removed += list.length - filtered.length;
      if (filtered.length === 0) {
        this.blobs.delete(addr);
      } else {
        this.blobs.set(addr, filtered);
      }
    }
    return removed;
  }

  async deleteAddress(address: string): Promise<void> {
    this.owners.delete(address);
    this.blobs.delete(address);
  }
}
