// Copyright 2018-2026 the Deno authors. MIT license.
// deno-lint-ignore-file no-explicit-any deno-internal/prefer-primordials
(function () {
const { core } = __bootstrap;
const { PipeWrap, TLSWrap } = core.ops;
const { kReadBytesOrError, streamBaseState } = core.loadExtScript(
  "ext:deno_node/internal_binding/stream_wrap.ts",
);
// Use Symbol.for to access symbols from js_stream_socket.js
// without importing it (avoids circular dependency).
const kJSStreamHandle = Symbol.for("kJSStreamHandle");
const kOwner = Symbol.for("kJSStreamOwner");

function installNativeOnread(res: TLSWrap, nativeHandle: any) {
  nativeHandle.onread = function (
    buf: ArrayBuffer | Uint8Array | undefined,
  ) {
    const nread = streamBaseState[kReadBytesOrError];
    if (nread > 0 && buf) {
      // LibUvStreamWrap passes an ArrayBuffer; convert to Uint8Array for receive()
      const data = buf instanceof ArrayBuffer
        ? new Uint8Array(buf, 0, nread)
        : buf.subarray(0, nread);
      res.receive(data);
    } else if (nread < 0) {
      // EOF or error - stop native TCP reads and unref the handle.
      // Without this, the libuv handle keeps a ref on the event loop
      // and prevents process exit after the TLS connection ends.
      nativeHandle.readStop();
      nativeHandle.unref();
      res.emitEof();
    }
  };
}

function attachNativeHandle(res: TLSWrap, nativeHandle: any) {
  const attachResult = nativeHandle instanceof PipeWrap
    ? res.attachPipe(nativeHandle)
    : res.attach(nativeHandle);
  if (attachResult !== 0) {
    throw new Error(`TLS wrap attach failed: ${attachResult}`);
  }

  installNativeOnread(res, nativeHandle);
  res._nativeTcpHandle = nativeHandle;
}

/**
 * Create a TLSWrap that intercepts an underlying stream handle.
 * Mirrors Node's `internalBinding('tls_wrap').wrap(handle, context, isServer)`.
 *
 * @param handle - The underlying stream handle (TCP CppGC object or JSStreamSocket handle)
 * @param context - SecureContext object { ca, cert, key, rejectUnauthorized }
 * @param isServer - Whether this is a server-side TLS connection
 * @param servername - SNI hostname for client connections
 */
function wrap(
  handle: any,
  context: any,
  isServer: boolean,
  servername?: string,
): TLSWrap {
  const kind = isServer ? 1 : 0;
  // TODO(@nathanwhit): use a proper async_id from async_hooks
  const asyncId = 0;
  const res = new TLSWrap(kind, asyncId);

  // initClientTls/initServerTls take the SecureContext JS object directly
  // and build the rustls config from its { ca, cert, key } properties.
  // The actual TLS connection is deferred until start() so that
  // setALPNProtocols can modify the config first.
  const initResult = isServer
    ? res.initServerTls(context)
    : res.initClientTls(servername || "", context);
  if (initResult !== 0) {
    const err = new Error("unsupported protocol");
    // rustls cannot negotiate TLSv1.0/TLSv1.1, so surface the closest
    // OpenSSL-style error instead of hanging the socket.
    (err as Error & { code?: string }).code = "ERR_SSL_UNSUPPORTED_PROTOCOL";
    // Store the init error on the handle so the caller can detect and
    // report it instead of throwing synchronously.  A throw from here
    // surfaces as an uncaught exception when the TLSSocket is created
    // inside a native libuv callback (e.g. server_connection_cb) because
    // there is no native TryCatch scope on the call stack.
    res._initError = err;
    return res;
  }

  const nativeHandle = handle;
  res._attachNativeHandle = (nativeHandle: any) =>
    attachNativeHandle(res, nativeHandle);
  res._installNativeOnread = (nativeHandle: any) =>
    installNativeOnread(res, nativeHandle);

  if (nativeHandle[kJSStreamHandle]) {
    // JS-backed stream (e.g. JSStreamSocket wrapping a Duplex).
    // Use attachJsStream instead of attach -I/O goes through JS callbacks.
    const attachResult = res.attachJsStream();
    if (attachResult !== 0) {
      throw new Error(`TLS wrap attach (JS stream) failed: ${attachResult}`);
    }

    // Pull-based encrypted output: instead of Rust calling a JS
    // callback (which causes reentrancy issues), Rust buffers encrypted
    // data and JS drains it after each operation that may produce output.
    //
    // The pump mirrors Node's TLSWrap::EncOut + OnStreamAfterWrite loop
    // (src/crypto/crypto_tls.cc): hand encrypted bytes to the underlying
    // stream, wait for that write to complete, then drive cycle() (the
    // ClearIn + EncOut analog) to push any remaining cleartext, write
    // more encrypted bytes if produced, and finally fire InvokeQueued for
    // the cleartext WriteWrap's oncomplete. For JS-backed streams there
    // is no enc_write_cb (libuv write completion) so cycle() is only
    // driven here; without it the cleartext write callback never fires
    // and writes deadlock (issue #33907). Gating cycle() on the
    // underlying Duplex's _write callback also propagates backpressure,
    // matching Node's behavior.
    const jsStreamOwner = nativeHandle[kOwner];
    let flushPending = false;
    let writeInFlight = false;
    const pump = () => {
      if (writeInFlight || !jsStreamOwner?.stream) return;
      let data = res.drainEncOut();
      if (!data || data.byteLength === 0) {
        // No buffered encrypted output. Drive cycle() to fire
        // InvokeQueued for any queued cleartext write and to produce
        // more encrypted output if clear_in() was rate-limited.
        res.readBuffer(new Uint8Array(0));
        data = res.drainEncOut();
        if (!data || data.byteLength === 0) return;
      }
      writeInFlight = true;
      jsStreamOwner.stream.write(new Uint8Array(data), () => {
        writeInFlight = false;
        pump();
      });
    };
    const flushEncOut = () => {
      if (flushPending) return;
      flushPending = true;
      // Defer to a microtask so synchronous feedback loops through
      // DuplexPair don't reenter cycle() while it's still running.
      Promise.resolve().then(() => {
        flushPending = false;
        pump();
      });
    };

    // For JS-backed streams the native `shutdown` op buffers the TLS
    // close_notify into `pending_enc_out` but never flushes it (the uv path
    // relies on `uv_shutdown`, a no-op here), so the peer hangs waiting for an
    // EOF. Wrap `shutdown` to drain the close_notify through the pump; the
    // peer's TLS layer turns that into an EOF. We deliberately do NOT end the
    // underlying stream: a half-open peer (e.g. `tls.connect({ socket })` that
    // calls `.end()` but is still reading the reply) must keep its read side
    // open, and ending its transport here would race that read.
    const origShutdown = res.shutdown;
    if (typeof origShutdown === "function") {
      res.shutdown = function (...args: any[]) {
        const ret = origShutdown.apply(res, args);
        flushEncOut();
        return ret;
      };
    }

    // Wire up readBuffer/emitEOF: JSStreamSocket calls these when the
    // underlying Duplex stream produces data or ends.
    // After each call, schedule a drain of encrypted output.
    nativeHandle.readBuffer = (chunk: Uint8Array) => {
      res.readBuffer(chunk);
      flushEncOut();
    };
    nativeHandle.emitEOF = () => {
      res.emitEof();
      flushEncOut();
    };

    // Store flush function on the TLSWrap so _start() can drain
    // after initiating the TLS handshake.
    res._flushEncOut = flushEncOut;
  } else {
    // Native stream (TCP or Pipe handle).
    attachNativeHandle(res, nativeHandle);
  }

  // Store the JS handle reference so Rust can call JS callbacks (onhandshakedone, etc.)
  res.setHandle(res);

  return res;
}

const _defaultExport = { TLSWrap, wrap };

return {
  TLSWrap,
  wrap,
  default: _defaultExport,
};
})();
