/**
 * Base class for WebSocket-backed Durable Objects (live notifications).
 *
 * Uses the Cloudflare WebSocket Hibernation API: subclasses do NOT addEventListener
 * on the socket. Instead the runtime calls webSocketMessage/Close/Error on the DO
 * after the socket has been accepted via ctx.acceptWebSocket(ws, tags). This lets
 * the DO hibernate between messages and only spin up when traffic arrives — key
 * for staying under Workers Free CPU/duration limits with many idle connections.
 *
 * Concrete subclasses (UserSessionDO, TopicDO) implement onMessage and optionally
 * override onClose/onError. They must call acceptHibernated(ws, tags) in their
 * fetch handler when accepting an Upgrade — this base does NOT auto-accept since
 * the upgrade decision (auth, capacity, topic existence) belongs to the subclass.
 */
import { DurableObject } from 'cloudflare:workers';
import type { MultidealEnv } from '../lib/env.js';

export abstract class BaseWebSocketDO<
  E extends MultidealEnv = MultidealEnv,
> extends DurableObject<E> {
  /**
   * Handle a message from a hibernated WebSocket. Subclasses MUST implement.
   * Throwing closes the socket with 1011 ("internal") after onError fires.
   */
  protected abstract onMessage(ws: WebSocket, raw: string | ArrayBuffer): Promise<void>;

  /**
   * Optional close hook. Default: no-op. Override to release subscriptions, etc.
   */
  protected onClose(
    _ws: WebSocket,
    _code: number,
    _reason: string,
    _wasClean: boolean,
  ): Promise<void> {
    return Promise.resolve();
  }

  /**
   * Optional error hook. Default: no-op. Override for structured logging.
   */
  protected onError(_ws: WebSocket, _err: unknown): Promise<void> {
    return Promise.resolve();
  }

  // ─── Hibernation API runtime hooks ─────────────────────────────────────────
  // The Cloudflare runtime invokes these directly on the DO instance after
  // ctx.acceptWebSocket(ws, tags) has been called. Do NOT rename — names are
  // part of the runtime contract.

  override async webSocketMessage(ws: WebSocket, raw: string | ArrayBuffer): Promise<void> {
    try {
      await this.onMessage(ws, raw);
    } catch (err) {
      await this.onError(ws, err);
      try {
        ws.close(1011, 'internal');
      } catch (err) {
        void err;
        // socket already closed — swallow
      }
    }
  }

  override async webSocketClose(
    ws: WebSocket,
    code: number,
    reason: string,
    wasClean: boolean,
  ): Promise<void> {
    await this.onClose(ws, code, reason, wasClean);
  }

  override async webSocketError(ws: WebSocket, err: unknown): Promise<void> {
    await this.onError(ws, err);
  }

  // ─── Helpers for subclasses ────────────────────────────────────────────────

  /**
   * Accept a hibernating WebSocket. Tags are used by ctx.getWebSockets(tag)
   * to look sockets up by user/session/topic without scanning every socket.
   */
  protected acceptHibernated(ws: WebSocket, tags?: string[]): void {
    this.ctx.acceptWebSocket(ws, tags);
  }

  /**
   * Return every currently-live WebSocket attached to this DO (including
   * hibernated ones — the runtime wakes them on demand).
   */
  protected liveSockets(): WebSocket[] {
    return this.ctx.getWebSockets();
  }

  /**
   * Broadcast a JSON envelope to every live socket on this DO. Send failures
   * close the offending socket with 1011 and move on — broadcast must not
   * become an O(n) failure cascade for one bad peer.
   */
  protected broadcast(envelope: unknown): void {
    const text = JSON.stringify(envelope);
    for (const ws of this.ctx.getWebSockets()) {
      try {
        ws.send(text);
      } catch (err) {
        void err;
        try {
          ws.close(1011, 'send_fail');
        } catch (err) {
          void err;
          // socket already closed — swallow
        }
      }
    }
  }
}
