// Offline text-message outbox.
//
// When `sendEncryptedMessage` fails (network loss, server 5xx, transient
// realtime hiccups) we stash the plaintext in localStorage keyed by
// conversation. The UI shows it inline with a "sending…" indicator; the
// drain loop retries with exponential backoff until it lands.
//
// Attachments are intentionally NOT queued — they're too large to persist
// and require server-side upload that can't be deferred reliably. The
// composer surfaces an immediate error for those.

const STORAGE_KEY = 'chat.outbox.v1';
const MAX_ATTEMPTS = 8;

export interface OutboxItem {
  /** Local-only id (never collides with server UUIDs). */
  id: string;
  conversationId: string;
  text: string;
  replyToId: string | null;
  createdAt: string;
  attempts: number;
  /** Timestamp of the next permitted send attempt. */
  nextAttemptAt: string;
  /** Last error message for surface in the UI. */
  lastError: string | null;
}

type Store = Record<string, OutboxItem[]>; // keyed by conversationId
type Listener = (byConv: Store) => void;

function load(): Store {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return {};
    const parsed: unknown = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return {};
    const out: Store = {};
    for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
      if (Array.isArray(v)) {
        out[k] = v.filter((x): x is OutboxItem => isItem(x));
      }
    }
    return out;
  } catch {
    return {};
  }
}

function isItem(x: unknown): x is OutboxItem {
  if (!x || typeof x !== 'object') return false;
  const o = x as Record<string, unknown>;
  return (
    typeof o.id === 'string' &&
    typeof o.conversationId === 'string' &&
    typeof o.text === 'string' &&
    typeof o.createdAt === 'string'
  );
}

let store: Store = load();
const listeners = new Set<Listener>();

function persist(): void {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
  } catch {
    /* quota exceeded — ignore; in-memory queue still retries */
  }
}

function notify(): void {
  for (const fn of listeners) fn(store);
}

export function subscribeOutbox(fn: Listener): () => void {
  listeners.add(fn);
  fn(store);
  return () => {
    listeners.delete(fn);
  };
}

export function getOutbox(conversationId: string): OutboxItem[] {
  return store[conversationId] ?? [];
}

export function enqueueOutbox(args: {
  conversationId: string;
  text: string;
  replyToId: string | null;
  error: string | null;
}): OutboxItem {
  const item: OutboxItem = {
    id: 'local-' + crypto.randomUUID(),
    conversationId: args.conversationId,
    text: args.text,
    replyToId: args.replyToId,
    createdAt: new Date().toISOString(),
    attempts: 0,
    nextAttemptAt: new Date().toISOString(),
    lastError: args.error,
  };
  const bucket = store[args.conversationId] ?? [];
  store = { ...store, [args.conversationId]: [...bucket, item] };
  persist();
  notify();
  return item;
}

export function removeOutbox(conversationId: string, id: string): void {
  const bucket = store[conversationId];
  if (!bucket) return;
  const next = bucket.filter((x) => x.id !== id);
  if (next.length === bucket.length) return;
  if (next.length === 0) {
    const copy = { ...store };
    delete copy[conversationId];
    store = copy;
  } else {
    store = { ...store, [conversationId]: next };
  }
  persist();
  notify();
}

export function markAttempt(
  conversationId: string,
  id: string,
  error: string | null,
): OutboxItem | null {
  const bucket = store[conversationId];
  if (!bucket) return null;
  const idx = bucket.findIndex((x) => x.id === id);
  if (idx < 0) return null;
  const prev = bucket[idx]!;
  const attempts = prev.attempts + 1;
  // Exponential backoff: 2^n seconds, capped at 60s.
  const delaySec = Math.min(60, Math.pow(2, attempts));
  const next: OutboxItem = {
    ...prev,
    attempts,
    lastError: error,
    nextAttemptAt: new Date(Date.now() + delaySec * 1000).toISOString(),
  };
  const nextBucket = [...bucket];
  nextBucket[idx] = next;
  store = { ...store, [conversationId]: nextBucket };
  persist();
  notify();
  return next;
}

export function shouldGiveUp(item: OutboxItem): boolean {
  return item.attempts >= MAX_ATTEMPTS;
}

export function isDue(item: OutboxItem): boolean {
  return new Date(item.nextAttemptAt).getTime() <= Date.now();
}

export function allConversationsWithItems(): string[] {
  return Object.keys(store);
}
