export interface OutboxEntry {
  clientMsgId: string;
  channelId: string;
  text: string;
  blocks?: unknown;
  parentId?: string | null;
  isBroadcast?: boolean;
  createdAt: string;
  attempts: number;
  status: "pending" | "failed";
}

export interface OutboxStorage {
  load(): Promise<OutboxEntry[]>;
  save(entry: OutboxEntry): Promise<void>;
  remove(clientMsgId: string): Promise<void>;
}

/** For tests and non-browser hosts. */
export class MemoryOutboxStorage implements OutboxStorage {
  private entries = new Map<string, OutboxEntry>();

  async load(): Promise<OutboxEntry[]> {
    return Array.from(this.entries.values());
  }
  async save(entry: OutboxEntry): Promise<void> {
    this.entries.set(entry.clientMsgId, entry);
  }
  async remove(clientMsgId: string): Promise<void> {
    this.entries.delete(clientMsgId);
  }
}

/** Web/PWA persistence (§5.3 / Feature 12.5). Desktop's SQLite-backed
 * implementation lives in apps/desktop once that platform adapter is built
 * (Phase 3) — same OutboxStorage interface, different backing store. */
export class IndexedDbOutboxStorage implements OutboxStorage {
  private readonly dbName = "slackwsh-outbox";
  private readonly storeName = "entries";

  private open(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      const req = indexedDB.open(this.dbName, 1);
      req.onupgradeneeded = () => req.result.createObjectStore(this.storeName, { keyPath: "clientMsgId" });
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
  }

  async load(): Promise<OutboxEntry[]> {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const req = db.transaction(this.storeName, "readonly").objectStore(this.storeName).getAll();
      req.onsuccess = () => resolve(req.result as OutboxEntry[]);
      req.onerror = () => reject(req.error);
    });
  }

  async save(entry: OutboxEntry): Promise<void> {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const req = db.transaction(this.storeName, "readwrite").objectStore(this.storeName).put(entry);
      req.onsuccess = () => resolve();
      req.onerror = () => reject(req.error);
    });
  }

  async remove(clientMsgId: string): Promise<void> {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const req = db.transaction(this.storeName, "readwrite").objectStore(this.storeName).delete(clientMsgId);
      req.onsuccess = () => resolve();
      req.onerror = () => reject(req.error);
    });
  }
}

export type SendFn = (entry: OutboxEntry) => Promise<void>;
export type OutboxStatus = "pending" | "sent" | "failed";
export type StatusListener = (clientMsgId: string, status: OutboxStatus) => void;

/**
 * Optimistic send with an offline outbox (Feature 4.23 / §5.3). Every
 * mutation persists to storage *before* any network attempt — so a page
 * reload or app crash mid-send still replays it. Idempotent by
 * construction: replaying an already-delivered `clientMsgId` hits the
 * server's `UNIQUE(channel_id, client_msg_id)` and returns the original
 * message (I3) rather than creating a duplicate.
 */
export class Outbox {
  constructor(
    private readonly storage: OutboxStorage,
    private readonly send: SendFn,
    private readonly onStatusChange?: StatusListener,
  ) {}

  async enqueue(entry: Omit<OutboxEntry, "attempts" | "status">): Promise<void> {
    const full: OutboxEntry = { ...entry, attempts: 0, status: "pending" };
    await this.storage.save(full);
    this.onStatusChange?.(full.clientMsgId, "pending");
    await this.attempt(full);
  }

  /** Replays every persisted entry in creation order — called on reconnect. */
  async replay(): Promise<void> {
    const entries = (await this.storage.load()).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    for (const entry of entries) {
      await this.attempt(entry);
    }
  }

  private async attempt(entry: OutboxEntry): Promise<void> {
    try {
      await this.send(entry);
      await this.storage.remove(entry.clientMsgId);
      this.onStatusChange?.(entry.clientMsgId, "sent");
    } catch {
      await this.storage.save({ ...entry, attempts: entry.attempts + 1, status: "failed" });
      this.onStatusChange?.(entry.clientMsgId, "failed");
      // Surfacing retry as UI state (rather than throwing here) is a
      // call-site concern — the outbox's job is only to never lose the
      // mutation, per §5.3 ("never silently vanish").
    }
  }
}
