import fetchRetry, { FetchLibrary, RequestInitRetryParams } from 'fetch-retry';
import pino from 'pino';
import {
  ApiProvider,
  CallApiContextParams,
  CallApiOptionsParams,
  ProviderOptions,
  ProviderResponse,
} from 'promptfoo/dist/src/types/providers';

const fetcher = fetchRetry(global.fetch);

export type ExternalProviderOptions = ProviderOptions & {
  config: {
    url: string;
    identifier?: string;
    requestOptions?: Omit<RequestInit, 'body'>;
    retryOptions?: RequestInitRetryParams<FetchLibrary>;
  };
};

export default class ExternalProvider implements ApiProvider {
  protected readonly logger: pino.Logger;

  readonly defaultUrl: string;

  private readonly identifier: string;

  readonly requestOptions: RequestInit;

  private readonly retryOptions: RequestInitRetryParams<FetchLibrary>;

  constructor({ id, label, config }: ExternalProviderOptions) {
    this.defaultUrl = config.url;
    this.identifier = id ?? label ?? 'test-ai-evaluator';

    const requestOptions = config.requestOptions ?? {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    };
    this.requestOptions = {
      method: 'POST',
      ...requestOptions,
      headers: {
        'Content-Type': 'application/json',
        ...requestOptions.headers,
      },
    };

    this.retryOptions = config.retryOptions ?? {
      retries: 5,
      retryDelay(attempt: number) {
        return 2 ** attempt * 1000;
      },
      retryOn: [500, 501, 502, 503, 504],
    };

    this.logger = pino({
      level: process.env.LOG_LEVEL || 'info',
    }).child({
      module: this.constructor.name,
    });
  }

  id() {
    return this.identifier;
  }

  protected getUrl(
    _prompt: string,
    _context?: CallApiContextParams,
    _callApiOptions?: CallApiOptionsParams,
  ) {
    return this.defaultUrl;
  }

  protected getBody(
    prompt: string,
    _context?: CallApiContextParams,
    _callApiOptions?: CallApiOptionsParams,
  ) {
    return JSON.stringify({
      body: prompt,
    });
  }

  protected async getResponse(response: Response): Promise<ProviderResponse> {
    const data = await response.json();

    return { output: data as string };
  }

  public async callApi(
    prompt: string,
    context?: CallApiContextParams,
    callApiOptions?: CallApiOptionsParams,
  ): Promise<ProviderResponse> {
    const request = {
      ...this.requestOptions,
      ...this.retryOptions,
      body: this.getBody(prompt, context, callApiOptions),
    };
    this.logger.debug(
      'Calling endpoint %s with request payload %s',
      this.getUrl(prompt, context, callApiOptions),
      JSON.stringify(request),
    );

    const fetchResponse = await fetcher(
      this.getUrl(prompt, context, callApiOptions),
      request,
    );
    if (fetchResponse.status > 300) {
      this.logger.error('Received error response %s', fetchResponse.statusText);
      throw new Error(fetchResponse.statusText);
    }

    const response = await this.getResponse(fetchResponse);
    this.logger.debug('Received response %s', JSON.stringify(response));

    return response;
  }
}
