/** copied from https://github.com/langchain-ai/langgraphjs/tree/main/libs/sdk/src/utils */
// in this case don't quite match.
type IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;

/*
 * Support async iterator syntax for ReadableStreams in all environments.
 * Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
 */
export class IterableReadableStream<T> extends ReadableStream<T> implements IterableReadableStreamInterface<T> {
    /** @ts-ignore */
    public reader: ReadableStreamDefaultReader<T>;

    ensureReader() {
        if (!this.reader) {
            this.reader = this.getReader();
        }
    }

    async next(): Promise<IteratorResult<T>> {
        this.ensureReader();
        try {
            const result = await this.reader.read();
            if (result.done) {
                this.reader.releaseLock(); // release lock when stream becomes closed
                return {
                    done: true,
                    value: undefined,
                };
            } else {
                return {
                    done: false,
                    value: result.value,
                };
            }
        } catch (e) {
            this.reader.releaseLock(); // release lock when stream becomes errored
            throw e;
        }
    }

    async return(): Promise<IteratorResult<T>> {
        this.ensureReader();
        // If wrapped in a Node stream, cancel is already called.
        if (this.locked) {
            const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
            this.reader.releaseLock(); // release lock first
            await cancelPromise; // now await it
        }
        return { done: true, value: undefined };
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    async throw(e: any): Promise<IteratorResult<T>> {
        this.ensureReader();
        if (this.locked) {
            const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
            this.reader.releaseLock(); // release lock first
            await cancelPromise; // now await it
        }
        throw e;
    }

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore Not present in Node 18 types, required in latest Node 22
    async [Symbol.asyncDispose]() {
        await this.return();
    }

    [Symbol.asyncIterator]() {
        return this;
    }

    static fromReadableStream<T>(stream: ReadableStream<T>) {
        // From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream
        const reader = stream.getReader();
        return new IterableReadableStream<T>({
            start(controller) {
                return pump();
                function pump(): Promise<T | undefined> {
                    return reader.read().then(({ done, value }) => {
                        // When no more data needs to be consumed, close the stream
                        if (done) {
                            controller.close();
                            return;
                        }
                        // Enqueue the next data chunk into our target stream
                        controller.enqueue(value);
                        return pump();
                    });
                }
            },
            cancel() {
                reader.releaseLock();
            },
        });
    }

    static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {
        return new IterableReadableStream<T>({
            async pull(controller) {
                const { value, done } = await generator.next();
                // When no more data needs to be consumed, close the stream
                if (done) {
                    controller.close();
                }
                // Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled
                controller.enqueue(value);
            },
            async cancel(reason) {
                await generator.return(reason);
            },
        });
    }
}
