import type { ServerWebSocket } from 'bun';
import { getTerminalWsUrl } from './sidecar-server';

// Byte relay between the browser and the pty sidecar's own socket.
//
// This used to be a translating bridge: officer sent pty:init / pty:input / pty:resize over the
// registration socket, subscribed to ONE global output stream, filtered every frame down to this session
// and rewrapped it — double-JSON-encoded — on the way out. All of that was terminal knowledge living in a
// process whose job is authentication. The sidecar serves its own socket now; this file authenticates the
// upgrade (upgradeWs, in server.tsx) and moves frames without reading them.
//
// The frame contract with the browser is unchanged, because the sidecar speaks it directly.

export type TerminalWSData = {
  provider: 'terminal';
  /** Raw query string from the upgrade — sessionId, cwd, cols, rows — passed through untouched. */
  search: string;
};

type UpstreamState = {
  ws: WebSocket | null;
  queue: (string | Uint8Array<ArrayBuffer>)[];
  closed: boolean;
};

const upstreams = new Map<ServerWebSocket<TerminalWSData>, UpstreamState>();

const asPayload = (raw: string | Buffer): string | Uint8Array<ArrayBuffer> =>
  typeof raw === 'string' ? raw : (raw as Uint8Array<ArrayBuffer>);

const closeClient = (ws: ServerWebSocket<TerminalWSData>, code: number, reason: string) => {
  upstreams.delete(ws);
  try {
    ws.close(code, reason);
  } catch {
    /* already closed */
  }
};

export const terminalWebsocket = {
  open(ws: ServerWebSocket<TerminalWSData>) {
    // Registered synchronously so frames that arrive before the upstream is ready are queued, not dropped.
    const state: UpstreamState = { ws: null, queue: [], closed: false };
    upstreams.set(ws, state);

    const base = getTerminalWsUrl();
    if (!base) {
      try {
        ws.send(JSON.stringify({ type: 'output', data: '\r\n[Terminal error] PTY sidecar is not connected\r\n' }));
      } catch {
        /* ignore */
      }
      closeClient(ws, 1011, 'pty sidecar unavailable');
      return;
    }

    const upstream = new WebSocket(`${base}/ws${state.closed ? '' : ws.data.search}`);
    state.ws = upstream;

    upstream.onopen = () => {
      for (const frame of state.queue) upstream.send(frame);
      state.queue.length = 0;
    };
    upstream.onmessage = (ev) => {
      try {
        ws.send(typeof ev.data === 'string' ? ev.data : new Uint8Array(ev.data as ArrayBuffer));
      } catch {
        /* client gone */
      }
    };
    upstream.onclose = () => closeClient(ws, 1000, 'upstream closed');
    upstream.onerror = () => closeClient(ws, 1011, 'upstream error');
  },

  message(ws: ServerWebSocket<TerminalWSData>, raw: string | Buffer) {
    const state = upstreams.get(ws);
    if (!state) return;
    const payload = asPayload(raw);
    if (state.ws && state.ws.readyState === WebSocket.OPEN) state.ws.send(payload);
    else state.queue.push(payload);
  },

  close(ws: ServerWebSocket<TerminalWSData>) {
    const state = upstreams.get(ws);
    upstreams.delete(ws);
    // Closing the upstream detaches this viewer; the shell stays alive in the sidecar, re-attachable.
    try {
      state?.ws?.close();
    } catch {
      /* ignore */
    }
  },

  drain() {},
};
