/**
 * TenantRealtimeDO — per-tenant WebSocket hub via Durable Object.
 *
 * Transport ownership: real-time-infrastructure. One DO per tenant, addressed by
 * name `tenant:{tenantId}`. The DO never queries Neon — events arrive fully formed
 * from the queue consumer (POST /broadcast).
 *
 * Uses the WebSocket Hibernation API (`state.acceptWebSocket`) so idle tenants
 * incur no compute: the DO wakes only to fan out an event or handle a close.
 * Each socket is tagged with its `userId` so `targetUserId`-scoped events
 * (e.g. notification.new) are delivered only to that user's sockets — a tenant
 * broadcast must NOT leak a user-scoped event to other members.
 */
import type { RealtimeEvent } from '@zync/realtime'

export class TenantRealtimeDO implements DurableObject {
  constructor(
    private readonly state: DurableObjectState,
    private readonly env: unknown,
  ) {}

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url)

    // ── POST /fanout — queue consumer delivers an event to fan out ─────────────
    // `/broadcast` remains as a compatibility alias for older callers.
    if (
      request.method === 'POST' &&
      (url.pathname === '/fanout' || url.pathname === '/broadcast')
    ) {
      let event: RealtimeEvent
      try {
        event = (await request.json()) as RealtimeEvent
      } catch {
        return new Response('Bad Request: invalid JSON', { status: 400 })
      }

      const payload = JSON.stringify(event)
      // user-scoped delivery when targetUserId is set; else tenant-wide broadcast
      const sockets = event.targetUserId
        ? this.state.getWebSockets(event.targetUserId)
        : this.state.getWebSockets()

      for (const ws of sockets) {
        try {
          ws.send(payload)
        } catch {
          // Socket errored — hibernation lifecycle will evict it on close
        }
      }

      return new Response(null, { status: 204 })
    }

    // ── GET (WebSocket Upgrade) — proxied from /api/realtime/connect ────────────
    // The connect route authenticates the session cookie and forwards the upgrade
    // with a trusted `X-User-Id` header so the DO can tag the socket for scoping.
    if (
      url.pathname === '/connect' &&
      request.headers.get('Upgrade')?.toLowerCase() === 'websocket'
    ) {
      const userId = request.headers.get('X-User-Id')
      if (!userId) {
        return new Response('Forbidden: missing identity', { status: 403 })
      }

      const pair = new WebSocketPair()
      const [client, server] = Object.values(pair) as [WebSocket, WebSocket]

      // Tag the socket with userId so getWebSockets(userId) can target it.
      this.state.acceptWebSocket(server, [userId])

      return new Response(null, { status: 101, webSocket: client })
    }

    return new Response('Not Found', { status: 404 })
  }

  // Hibernation lifecycle — clients are read-only; inbound frames are ignored.
  async webSocketMessage(): Promise<void> {
    // No client→server messages in this contract; events flow server→client only.
  }

  async webSocketClose(ws: WebSocket, code: number): Promise<void> {
    try {
      ws.close(code)
    } catch {
      // already closed
    }
  }

  async webSocketError(ws: WebSocket): Promise<void> {
    try {
      ws.close(1011)
    } catch {
      // already closed
    }
  }
}
