import { PassThrough } from 'stream';
import { EventEmitter } from 'events';
import fetchPonyfill from 'fetch-ponyfill';
import { Configuration, DoFetchBody } from './common';
import { createTextCompletion, CreateTextCompletionParams } from './features/text-completion';
import { CreateChatCompletionParams, createChatCompletion } from './features/chat-completion';
import {
  CreateImageParams,
  createImage,
  CreateImageEditParams,
  createImageEdit,
  CreateImageVariationParams,
  createImageVariation,
} from './features/image';
import { CreateEditParams, createEdit } from './features/edit';
import { listModels } from './features/model';

export type DataEventCallback<D> = (data: D) => void;

export type ErrorEventCallback = (event: Error) => void;

export interface PlatformEventEmitter extends NodeJS.EventEmitter {
  createChatCompletion(params: CreateChatCompletionParams): void;
  createImage(params: CreateImageParams): void;
  createImageEdit(params: CreateImageEditParams): void;
  createImageVariation(params: CreateImageVariationParams): void;
  createCompletion(params: CreateTextCompletionParams): void;
  createEdit(params: CreateEditParams): void;
  listModels(): void;
  on<D>(event: 'data', callback: DataEventCallback<D>): this;
  on(event: 'end', callback: () => void): this;
  on(event: 'error', callback: ErrorEventCallback): this;
}

export class PlatformEventEmitterImpl extends EventEmitter implements PlatformEventEmitter {
  readonly createCompletion: PlatformEventEmitter['createCompletion'];

  readonly createImage: PlatformEventEmitter['createImage'];

  readonly createImageEdit: PlatformEventEmitter['createImageEdit'];

  readonly createImageVariation: PlatformEventEmitter['createImageVariation'];

  readonly createChatCompletion: PlatformEventEmitter['createChatCompletion'];

  readonly createEdit: PlatformEventEmitter['createEdit'];

  readonly listModels: PlatformEventEmitter['listModels'];

  constructor(configParams: Configuration) {
    super();
    const platformHeaders: Record<string, string> = {
      Authorization: `Bearer ${configParams.apiKey}`,
    };

    if (configParams.organizationId) {
      platformHeaders['OpenAI-Organization'] = configParams.organizationId;
    }

    const { fetch: fetchInstance } = fetchPonyfill();
    const doFetch = (method: string, path: string, body?: DoFetchBody) => {
      const requestHeaders = {
        ...platformHeaders,
      };

      let theBody: BodyInit;

      if (
        body instanceof FormData
        || body instanceof URLSearchParams
      ) {
        theBody = body;
      } else {
        theBody = JSON.stringify(body);
        requestHeaders['Content-Type'] = 'application/json';
      }

      const theFetchParams = {
        method,
        headers: requestHeaders,
        body: theBody,
      };

      const url = new URL(
        `/${configParams.apiVersion}${path}`,
        configParams.baseUrl ?? 'https://api.openai.com',
      ).toString();

      this.emit('start', {
        ...theFetchParams,
        url,
      });

      return fetchInstance(url, theFetchParams);
    };

    const consumeStream = async (response: Response) => {
      // eslint-disable-next-line no-restricted-syntax
      for await (const chunk of response.body as unknown as PassThrough) {
        const chunkStringMaybeMultiple = chunk.toString();
        const chunkStrings = chunkStringMaybeMultiple
          .trim()
          .split('\n')
          .filter((chunkString: string) => chunkString.length > 0);
        chunkStrings.forEach((chunkString: string) => {
          const dataRaw = chunkString.split('data: ').at(1);
          if (!dataRaw) {
            return;
          }
          if (dataRaw === '[DONE]') {
            return;
          }
          const data = JSON.parse(dataRaw);
          this.emit('data', data);
        });
      }
    };
    this.listModels = listModels.bind(this, doFetch);
    this.createImage = createImage.bind(this, doFetch);
    this.createImageVariation = createImageVariation.bind(this, doFetch);
    this.createImageEdit = createImageEdit.bind(this, doFetch);
    this.createCompletion = createTextCompletion.bind(this, doFetch, consumeStream);
    this.createChatCompletion = createChatCompletion.bind(this, doFetch, consumeStream);
    this.createEdit = createEdit.bind(this, doFetch);
  }
}
