// deno-lint-ignore-file
// Copyright 2018-2026 the Deno authors. MIT license.
(function () {
const { core } = __bootstrap;
const { destroy, destroyer } = core.loadExtScript(
  "ext:deno_node/internal/streams/destroy.js",
);
const finished =
  core.loadExtScript("ext:deno_node/internal/streams/end-of-stream.js").default;
const {
  isDestroyed,
  isReadable,
  isReadableEnded,
  isWritable,
  isWritableEnded,
} = core.loadExtScript("ext:deno_node/internal/streams/utils.js");
const { ReadableStream, WritableStream } = core.loadExtScript(
  "ext:deno_node/stream/web.js",
);
const {
  validateBoolean,
  validateObject,
  validateOneOf,
} = core.loadExtScript("ext:deno_node/internal/validators.mjs");
const {
  kEmptyObject,
  normalizeEncoding,
} = core.loadExtScript("ext:deno_node/internal/util.mjs");
const {
  AbortError,
  ERR_INVALID_ARG_VALUE,
  ERR_INVALID_ARG_TYPE,
} = core.loadExtScript("ext:deno_node/internal/errors.ts");
const lazyProcess = core.createLazyLoader("node:process");
const { Buffer } = core.loadExtScript("ext:deno_node/internal/buffer.mjs");
const { isAnyArrayBuffer } = core.loadExtScript(
  "ext:deno_node/internal/util/types.ts",
);
const lazyStream = core.createLazyLoader("node:stream");

function isWritableStream(object) {
  return object instanceof WritableStream;
}

function isReadableStream(object) {
  return object instanceof ReadableStream;
}

// Normalize a `writev` completion value to the first real error, or `undefined`
// when every write succeeded. `Promise.all` fulfils with an array (one slot per
// chunk, holes for successes), while a rejection or synchronous throw settles
// with a single scalar error.
function firstWritevError(error) {
  if (Array.isArray(error)) {
    return error.find((e) => e);
  }
  return error == null ? undefined : error;
}

function newStreamReadableFromReadableStream(
  readableStream,
  options = kEmptyObject,
) {
  if (!isReadableStream(readableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      "readableStream",
      "ReadableStream",
      readableStream,
    );
  }

  validateObject(options, "options");
  const {
    highWaterMark,
    encoding,
    objectMode = false,
    signal,
  } = options;

  if (encoding !== undefined && !Buffer.isEncoding(encoding)) {
    throw new ERR_INVALID_ARG_VALUE(encoding, "options.encoding");
  }
  validateBoolean(objectMode, "options.objectMode");

  const reader = readableStream.getReader();
  let closed = false;

  const readable = new (lazyStream().Readable)({
    objectMode,
    highWaterMark,
    encoding,
    signal,

    read() {
      reader.read().then(
        (chunk) => {
          if (chunk.done) {
            readable.push(null);
          } else {
            readable.push(chunk.value);
          }
        },
        (error) => destroy.call(readable, error),
      );
    },

    destroy(error, callback) {
      function done() {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          lazyProcess().default.nextTick(() => {
            throw error;
          });
        }
      }

      if (!closed) {
        reader.cancel(error).then(done, done);
        return;
      }

      done();
    },
  });

  reader.closed.then(
    () => {
      closed = true;
    },
    (error) => {
      closed = true;
      destroy.call(readable, error);
    },
  );

  return readable;
}

function newStreamWritableFromWritableStream(
  writableStream,
  options = kEmptyObject,
) {
  if (!isWritableStream(writableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      "writableStream",
      "WritableStream",
      writableStream,
    );
  }

  validateObject(options, "options");
  const {
    highWaterMark,
    decodeStrings = true,
    objectMode = false,
    signal,
  } = options;

  validateBoolean(objectMode, "options.objectMode");
  validateBoolean(decodeStrings, "options.decodeStrings");

  const writer = writableStream.getWriter();
  let closed = false;

  const writable = new (lazyStream().Writable)({
    highWaterMark,
    objectMode,
    decodeStrings,
    signal,

    writev(chunks, callback) {
      function done(error) {
        try {
          callback(firstWritevError(error));
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          lazyProcess().default.nextTick(() => destroy.call(writable, error));
        }
      }

      // If the fulfillment arrow throws synchronously (e.g. `writer.write`
      // throwing before it returns a promise), the sibling `done` rejection
      // handler never sees it, so `.catch(done)` routes that failure too.
      writer.ready.then(
        () =>
          Promise.all(
            chunks.map((data) => writer.write(data.chunk)),
          ).then(done, done),
        done,
      ).catch(done);
    },

    write(chunk, encoding, callback) {
      if (typeof chunk === "string" && decodeStrings && !objectMode) {
        chunk = Buffer.from(chunk, encoding);
        chunk = new Uint8Array(
          chunk.buffer,
          chunk.byteOffset,
          chunk.byteLength,
        );
      }

      function done(error) {
        try {
          callback(error);
        } catch (error) {
          destroy.call(writable, error);
        }
      }

      writer.ready.then(
        () => writer.write(chunk).then(done, done),
        done,
      );
    },

    destroy(error, callback) {
      function done() {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          lazyProcess().default.nextTick(() => {
            throw error;
          });
        }
      }

      if (!closed) {
        if (error != null) {
          writer.abort(error).then(done, done);
        } else {
          writer.close().then(done, done);
        }
        return;
      }

      done();
    },

    final(callback) {
      function done(error) {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          lazyProcess().default.nextTick(() => destroy.call(writable, error));
        }
      }

      if (!closed) {
        writer.close().then(done, done);
      }
    },
  });

  writer.closed.then(
    () => {
      closed = true;
    },
    (error) => {
      closed = true;
      destroy.call(writable, error);
    },
  );

  return writable;
}

function newStreamDuplexFromReadableWritablePair(
  pair,
  options = kEmptyObject,
) {
  validateObject(pair, "pair");
  const {
    readable: readableStream,
    writable: writableStream,
  } = pair;

  if (!isReadableStream(readableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      "pair.readable",
      "ReadableStream",
      readableStream,
    );
  }
  if (!isWritableStream(writableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      "pair.writable",
      "WritableStream",
      writableStream,
    );
  }

  validateObject(options, "options");
  const {
    allowHalfOpen = false,
    objectMode = false,
    encoding,
    decodeStrings = true,
    highWaterMark,
    signal,
  } = options;

  validateBoolean(objectMode, "options.objectMode");
  if (encoding !== undefined && !Buffer.isEncoding(encoding)) {
    throw new ERR_INVALID_ARG_VALUE(encoding, "options.encoding");
  }

  const writer = writableStream.getWriter();
  const reader = readableStream.getReader();
  let writableClosed = false;
  let readableClosed = false;

  const duplex = new (lazyStream().Duplex)({
    allowHalfOpen,
    highWaterMark,
    objectMode,
    encoding,
    decodeStrings,
    signal,

    writev(chunks, callback) {
      function done(error) {
        try {
          callback(firstWritevError(error));
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          lazyProcess().default.nextTick(() => destroy.call(duplex, error));
        }
      }

      // If the fulfillment arrow throws synchronously (e.g. `writer.write`
      // throwing before it returns a promise), the sibling `done` rejection
      // handler never sees it, so `.catch(done)` routes that failure too.
      writer.ready.then(
        () =>
          Promise.all(
            chunks.map((data) => writer.write(data.chunk)),
          ).then(done, done),
        done,
      ).catch(done);
    },

    write(chunk, encoding, callback) {
      if (typeof chunk === "string" && decodeStrings && !objectMode) {
        chunk = Buffer.from(chunk, encoding);
        chunk = new Uint8Array(
          chunk.buffer,
          chunk.byteOffset,
          chunk.byteLength,
        );
      }

      function done(error) {
        try {
          callback(error);
        } catch (error) {
          destroy.call(duplex, error);
        }
      }

      writer.ready.then(
        () => writer.write(chunk).then(done, done),
        done,
      );
    },

    final(callback) {
      function done(error) {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          lazyProcess().default.nextTick(() => destroy.call(duplex, error));
        }
      }

      if (!writableClosed) {
        writer.close().then(done, done);
      }
    },

    read() {
      reader.read().then(
        (chunk) => {
          if (chunk.done) {
            duplex.push(null);
          } else {
            duplex.push(chunk.value);
          }
        },
        (error) => destroy.call(duplex, error),
      );
    },

    destroy(error, callback) {
      function done() {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          lazyProcess().default.nextTick(() => {
            throw error;
          });
        }
      }

      async function closeWriter() {
        if (!writableClosed) {
          await writer.abort(error);
        }
      }

      async function closeReader() {
        if (!readableClosed) {
          await reader.cancel(error);
        }
      }

      if (!writableClosed || !readableClosed) {
        Promise.all([
          closeWriter(),
          closeReader(),
        ]).then(done, done);
        return;
      }

      done();
    },
  });

  writer.closed.then(
    () => {
      writableClosed = true;
    },
    (error) => {
      writableClosed = true;
      readableClosed = true;
      destroy.call(duplex, error);
    },
  );

  reader.closed.then(
    () => {
      readableClosed = true;
    },
    (error) => {
      writableClosed = true;
      readableClosed = true;
      destroy.call(duplex, error);
    },
  );

  return duplex;
}

function newReadableStreamFromStreamReadable(
  streamReadable,
  options = kEmptyObject,
) {
  // Not using the internal/streams/utils isReadableNodeStream utility
  // here because it will return false if streamReadable is a Duplex
  // whose readable option is false. For a Duplex that is not readable,
  // we want it to pass this check but return a closed ReadableStream.
  if (typeof streamReadable?._readableState !== "object") {
    throw new ERR_INVALID_ARG_TYPE(
      "streamReadable",
      "stream.Readable",
      streamReadable,
    );
  }
  validateObject(options, "options");
  if (options.type !== undefined) {
    validateOneOf(options.type, "options.type", ["bytes", undefined]);
  }

  if (isDestroyed(streamReadable) || !isReadable(streamReadable)) {
    const readable = new ReadableStream();
    readable.cancel();
    return readable;
  }

  const objectMode = streamReadable.readableObjectMode;
  const highWaterMark = streamReadable.readableHighWaterMark;
  const isByteStream = options?.type === "bytes";

  const evaluateStrategyOrFallback = (strategy) => {
    // A byte stream already measures its queue in bytes and the spec forbids
    // giving it a size algorithm, so only the highWaterMark applies.
    if (isByteStream) {
      return { highWaterMark };
    }

    // If there is a strategy available, use it
    if (strategy) {
      return strategy;
    }

    if (objectMode) {
      // When running in objectMode explicitly but no strategy, we just fall
      // back to CountQueuingStrategy
      return new CountQueuingStrategy({ highWaterMark });
    }

    // Outside of objectMode `highWaterMark` is a byte count, so the queue has
    // to be measured in bytes as well. With the default size algorithm (1 per
    // chunk) `controller.desiredSize` only reaches 0 after `highWaterMark`
    // *chunks*, so `onData` below never pauses the source and the whole
    // stream ends up buffered in memory instead of flowing with backpressure.
    //
    // Node uses a plain `ByteLengthQueuingStrategy` here, whose size algorithm
    // reads `chunk.byteLength`. That is `undefined` for the strings a
    // non-objectMode Readable emits once an encoding is set, which makes the
    // enqueue throw `ERR_INVALID_ARG_VALUE`; fall back to the string length
    // for those instead.
    return {
      highWaterMark,
      size(chunk) {
        return chunk.byteLength ?? chunk.length;
      },
    };
  };

  const strategy = evaluateStrategyOrFallback(options?.strategy);

  let controller;

  function onData(chunk) {
    // Copy the Buffer to detach it from the pool.
    if (Buffer.isBuffer(chunk) && !objectMode) {
      chunk = new Uint8Array(chunk);
    }
    controller.enqueue(chunk);
    if (controller.desiredSize <= 0) {
      streamReadable.pause();
    }
  }

  streamReadable.pause();

  let isCanceled = false;

  // Only watch the readable side: a Duplex exposed via Readable.toWeb should
  // close the ReadableStream once its readable side ends, even if the writable
  // side is still open (otherwise the reader would hang waiting on the writable
  // half to finish).
  const cleanup = finished(streamReadable, { writable: false }, (error) => {
    if (error?.code === "ERR_STREAM_PREMATURE_CLOSE") {
      const err = new AbortError(undefined, { cause: error });
      error = err;
    }

    cleanup();
    // This is a protection against non-standard, legacy streams
    // that happen to emit an error event again after finished is called.
    streamReadable.on("error", () => {});
    if (error) {
      return controller.error(error);
    }
    if (isCanceled) {
      return;
    }
    controller.close();
  });

  streamReadable.on("data", onData);

  const underlyingSource = {
    start(c) {
      controller = c;
    },

    pull() {
      streamReadable.resume();
    },

    cancel(reason) {
      isCanceled = true;
      destroyer(streamReadable, reason);
    },
  };
  if (isByteStream) {
    underlyingSource.type = "bytes";
  }
  return new ReadableStream(underlyingSource, strategy);
}

function newWritableStreamFromStreamWritable(streamWritable) {
  // Not using the internal/streams/utils isWritableNodeStream utility
  // here because it will return false if streamWritable is a Duplex
  // whose writable option is false. For a Duplex that is not writable,
  // we want it to pass this check but return a closed WritableStream.
  // We check if the given stream is a stream.Writable or http.OutgoingMessage
  const checkIfWritableOrOutgoingMessage = streamWritable &&
    typeof streamWritable?.write === "function" &&
    typeof streamWritable?.on === "function";
  if (!checkIfWritableOrOutgoingMessage) {
    throw new ERR_INVALID_ARG_TYPE(
      "streamWritable",
      "stream.Writable",
      streamWritable,
    );
  }

  if (isDestroyed(streamWritable) || !isWritable(streamWritable)) {
    const writable = new WritableStream();
    writable.close();
    return writable;
  }

  const highWaterMark = streamWritable.writableHighWaterMark;
  const strategy = streamWritable.writableObjectMode
    ? new CountQueuingStrategy({ highWaterMark })
    : { highWaterMark };

  let controller;
  let backpressurePromise;
  let closed;

  function onDrain() {
    if (backpressurePromise !== undefined) {
      backpressurePromise.resolve();
    }
  }

  const cleanup = finished(streamWritable, (error) => {
    if (error?.code === "ERR_STREAM_PREMATURE_CLOSE") {
      const err = new AbortError(undefined, { cause: error });
      error = err;
    }

    cleanup();
    // This is a protection against non-standard, legacy streams
    // that happen to emit an error event again after finished is called.
    streamWritable.on("error", () => {});
    if (error != null) {
      if (backpressurePromise !== undefined) {
        backpressurePromise.reject(error);
      }
      // If closed is not undefined, the error is happening
      // after the WritableStream close has already started.
      // We need to reject it here.
      if (closed !== undefined) {
        closed.reject(error);
        closed = undefined;
      }
      controller.error(error);
      controller = undefined;
      return;
    }

    if (closed !== undefined) {
      closed.resolve();
      closed = undefined;
      return;
    }
    controller.error(new AbortError());
    controller = undefined;
  });

  streamWritable.on("drain", onDrain);

  return new WritableStream({
    start(c) {
      controller = c;
    },

    async write(chunk) {
      // A non-object-mode Writable only accepts BufferSource views. Wrap a bare
      // ArrayBuffer (or SharedArrayBuffer) in a Uint8Array so it can be written
      // without a copy. Node only unwraps ArrayBuffer here; we also unwrap
      // SharedArrayBuffer so both are usable.
      if (!streamWritable.writableObjectMode && isAnyArrayBuffer(chunk)) {
        chunk = new Uint8Array(chunk);
      }
      if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) {
        backpressurePromise = Promise.withResolvers();
        return backpressurePromise.promise.finally(() => {
          backpressurePromise = undefined;
        });
      }
    },

    abort(reason) {
      destroy.call(streamWritable, reason);
    },

    close() {
      if (closed === undefined && !isWritableEnded(streamWritable)) {
        closed = Promise.withResolvers();
        streamWritable.end();
        return closed.promise;
      }

      controller = undefined;
      return Promise.resolve();
    },
  }, strategy);
}

let dep0201Warned = false;

function newReadableWritablePairFromDuplex(
  duplex,
  options = kEmptyObject,
) {
  // Not using the internal/streams/utils isWritableNodeStream and
  // isReadableNodestream utilities here because they will return false
  // if the duplex was created with writable or readable options set to
  // false. Instead, we'll check the readable and writable state after
  // and return closed WritableStream or closed ReadableStream as
  // necessary.
  if (
    typeof duplex?._writableState !== "object" ||
    typeof duplex?._readableState !== "object"
  ) {
    throw new ERR_INVALID_ARG_TYPE("duplex", "stream.Duplex", duplex);
  }

  if (isDestroyed(duplex)) {
    const writable = new WritableStream();
    const readable = new ReadableStream();
    writable.close();
    readable.cancel();
    return { readable, writable };
  }

  const writable = isWritable(duplex)
    ? newWritableStreamFromStreamWritable(duplex)
    : new WritableStream();

  if (!isWritable(duplex)) {
    writable.close();
  }

  // `option.type` is a deprecated alias for `option.readableType`
  // matches Node's `lib/internal/webstreams/adapters.js#L655` DEP0201
  if (options.readableType == null && options.type != null) {
    if (!dep0201Warned) {
      dep0201Warned = true;
      lazyProcess().default.emitWarning(
        "Passing 'options.type' to Duplex.toWeb() is deprecated. " +
          "To specify the ReadableStream type, use 'options.readableType'.",
        "DeprecationWarning",
        "DEP0201",
      );
    }
  }

  const readableType = options?.readableType || options?.type;
  const readableOptions = readableType ? { type: readableType } : kEmptyObject;

  const readable = isReadable(duplex)
    ? newReadableStreamFromStreamReadable(duplex, readableOptions)
    : new ReadableStream();

  if (!isReadable(duplex)) {
    readable.cancel();
  }

  return { writable, readable };
}

return {
  newReadableStreamFromStreamReadable,
  newWritableStreamFromStreamWritable,
  newStreamReadableFromReadableStream,
  newStreamWritableFromWritableStream,
  newStreamDuplexFromReadableWritablePair,
  newReadableWritablePairFromDuplex,
};
})();
