import { connect as netConnect, type Socket } from 'node:net'
import { connect as tlsConnect, type TLSSocket } from 'node:tls'
import { dockerJson, dockerFetch, type DockerHost } from './client.js'

const DEFAULT_SHELL_CMD = ['/bin/sh', '-c', 'if command -v bash >/dev/null 2>&1; then exec bash; else exec sh; fi']

/** Opens a `docker exec` session and hands back the raw hijacked socket.
 *
 * The Docker Engine API doesn't expose exec I/O over a normal request/response or websocket -
 * `POST /exec/{id}/start` "hijacks" the underlying HTTP connection: after writing the response
 * headers, the daemon switches the same TCP socket to a raw bidirectional byte stream (the
 * process's stdin/stdout/stderr, interleaved, since this is opened with Tty:true). There's no
 * Node http client option for that, so we open the socket ourselves and write the HTTP request
 * by hand, then treat everything after the blank line that ends the response headers as the
 * exec session's raw stream. */
export async function openExecStream(
  host: DockerHost,
  containerId: string,
  cols: number,
  rows: number,
): Promise<{ socket: Socket | TLSSocket; execId: string }> {
  const { Id: execId } = await dockerJson<{ Id: string }>(host, `/containers/${containerId}/exec`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      AttachStdin: true,
      AttachStdout: true,
      AttachStderr: true,
      Tty: true,
      Cmd: DEFAULT_SHELL_CMD,
      ConsoleSize: [rows, cols],
    }),
  })

  const url = new URL(host.baseUrl)
  const useTls = url.protocol === 'https:'
  const port = Number(url.port) || (useTls ? 443 : 80)

  const socket = await new Promise<Socket | TLSSocket>((resolve, reject) => {
    const onError = (err: Error) => reject(err)
    const s = useTls
      ? tlsConnect({ host: url.hostname, port }, () => resolve(s))
      : netConnect({ host: url.hostname, port }, () => resolve(s))
    s.once('error', onError)
  })

  const body = JSON.stringify({ Detach: false, Tty: true })
  const request =
    `POST /exec/${execId}/start HTTP/1.1\r\n` +
    `Host: ${url.hostname}\r\n` +
    `Connection: Upgrade\r\n` +
    `Upgrade: tcp\r\n` +
    `Content-Type: application/json\r\n` +
    `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`
  socket.write(request)

  return { socket, execId }
}

/** Strips the HTTP response headers Docker sends before switching the socket to a raw stream,
 * forwarding everything after the blank line that ends them. Returns a passthrough function to
 * wrap the socket's first few 'data' events with. */
export function stripHijackHeaders(onData: (chunk: Buffer) => void): (chunk: Buffer) => void {
  let headersDone = false
  let pending = Buffer.alloc(0)
  return (chunk: Buffer) => {
    if (headersDone) {
      onData(chunk)
      return
    }
    pending = Buffer.concat([pending, chunk])
    const idx = pending.indexOf('\r\n\r\n')
    if (idx === -1) return
    headersDone = true
    const rest = pending.subarray(idx + 4)
    pending = Buffer.alloc(0)
    if (rest.length > 0) onData(rest)
  }
}

export async function resizeExec(host: DockerHost, execId: string, cols: number, rows: number): Promise<void> {
  await dockerFetch(host, `/exec/${execId}/resize?h=${rows}&w=${cols}`, { method: 'POST' })
}
