// Type definitions for zapier-platform-core
// Project: Zapier's Platform Core
// Definitions by: David Brownman <https://davidbrownman.com>

/// <reference types="node" />
/// <reference types="node-fetch" />

import { Agent } from 'http';
import { Headers } from 'node-fetch';
import type { Request } from './schemas.generated';

// The EXPORTED OBJECT
export const version: string;
export const tools: { env: { inject: (filename?: string) => void } };
export const console: Console;
export const errors: ErrorsModule;

// see: https://github.com/zapier/zapier-platform-cli/issues/339#issue-336888249
//
// The runtime (`resolveMethodPath` + `execute.js`) accepts either a perform
// function or a request-template object as the first argument. The function
// overload is kept first so `T` is still inferred from the perform's return
// type at call sites; the request-template overload returns `Promise<unknown>`
// because the platform returns `response.data` whose shape isn't expressed in
// the `Request` type. See PDE-7153.
export const createAppTester: (
  appRaw: object,
  options?: { customStoreKey?: string },
) => {
  <T, B extends Bundle>(
    func: (z: ZObject, bundle: B) => T | Promise<T>,
    bundle?: DeepPartial<B>, // partial so we don't have to make a full bundle in tests
    clearZcacheBeforeUse?: boolean,
  ): Promise<T>; // appTester always returns a promise
  <B extends Bundle = Bundle>(
    request: Request,
    bundle?: DeepPartial<B>,
    clearZcacheBeforeUse?: boolean,
  ): Promise<unknown>;
};

/** Recursively make all properties of an object optional. */
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] }
  : T;

type HttpMethod =
  | 'GET'
  | 'POST'
  | 'PUT'
  | 'PATCH'
  | 'DELETE'
  | 'OPTIONS'
  | 'HEAD';

export interface Bundle<
  $InputData extends Record<string, unknown> = Record<string, unknown>,
> {
  authData: { [x: string]: string };
  inputData: $InputData;
  inputDataRaw: { [x: string]: string };
  meta: {
    isBulkRead: boolean;

    /**
     * If true, this poll is being used to populate a dynamic dropdown.
     * You only need to return the fields you specified (such as id and
     * name), though returning everything is fine too.
     */
    isFillingDynamicDropdown: boolean;

    /**
     * If true, this run was initiated manually via the Zap Editor.
     */
    isLoadingSample: boolean;

    /**
     * If true, the results of this poll will be used to initialize the
     * deduplication list rather than trigger a zap. You should grab as
     * many items as possible.
     */
    isPopulatingDedupe: boolean;

    /**
     * (legacy property) If true, the poll was triggered by a user
     * testing their account (via clicking “test” or during setup). We
     * use this data to populate the auth label, but it’s mostly used to
     * verify we made a successful authenticated request
     *
     * @deprecated
     */
    isTestingAuth: boolean;

    /**
     * The number of items you should fetch. -1 indicates there’s no
     * limit. Build this into your calls insofar as you are able.
     */
    limit: number;

    /**
     * Used in paging to uniquely identify which page of results should
     * be returned.
     */
    page: number;

    /**
     * When a create is called as part of a search-or-create step,
     * this will be the key of the search.
     */
    withSearch?: string;

    /**
     * The timezone the user has configured for their account or
     * specific automation. Received as TZ identifier, such as
     * “America/New_York”.
     */
    timezone: string | null;

    /** @deprecated */
    zap?: {
      /** @deprecated */
      id: string;
      /** @deprecated */
      user: {
        /** @deprecated use meta.timezone instead. */
        timezone: string;
      };
    };

    /**
     * A token, such as a pagination cursor, that means this run should
     * continue pulling results. Currently only used for search
     * pagination. Set by previous invocations by returning `{results:
     * […], paging_token: '…'}`
     */
    paging_token?: string;

    /**
     * Contains metadata about the input fields, optionally provided
     * by the inputField.meta property. Useful for storing extra data
     * in dynamically created input fields.
     */
    inputFields: {
      [fieldKey: string]: { [metaKey: string]: string | number | boolean };
    };
  };
  rawRequest?: Partial<{
    method: HttpMethod;
    querystring: string;
    headers: { [x: string]: string };
    content: string;
  }>;
  cleanedRequest?:
    | Partial<{
        method: HttpMethod;
        querystring: { [x: string]: string };
        headers: { [x: string]: string };
        content: { [x: string]: string };
      }>
    | any;
  outputData?: object;
  subscribeData?: { id: string };
  targetUrl?: string;
}

// Error class types that match the runtime structure from src/errors.js
type ErrorConstructor = new (message?: string) => Error;
type AppErrorConstructor = new (
  message: string,
  code?: string,
  status?: number,
) => Error;
type ThrottledErrorConstructor = new (message: string, delay?: number) => Error;
type ResponseErrorConstructor = new (response: HttpResponse) => Error;

interface ErrorsModule {
  Error: AppErrorConstructor;
  HaltedError: ErrorConstructor;
  ExpiredAuthError: ErrorConstructor;
  RefreshAuthError: ErrorConstructor;
  ThrottledError: ThrottledErrorConstructor;
  ResponseError: ResponseErrorConstructor;
  CheckError: ErrorConstructor;
  DehydrateError: ErrorConstructor;
  MethodDoesNotExist: ErrorConstructor;
  NotImplementedError: ErrorConstructor;
  RequireModuleError: ErrorConstructor;
  StashedBundleError: ErrorConstructor;
  StopRequestError: ErrorConstructor;
  handleError: (...args: any[]) => never;
}

// copied http stuff from external typings
export interface HttpRequestOptions {
  agent?: Agent;
  body?: string | Buffer | NodeJS.ReadableStream | Record<string, any>;
  compress?: boolean;
  follow?: number;
  form?: any;
  headers?: { [name: string]: string };
  json?: any;
  method?: HttpMethod;
  params?: Record<string, any>;
  raw?: boolean;
  redirect?: 'manual' | 'error' | 'follow';
  removeMissingValuesFrom?: {
    params?: boolean;
    body?: boolean;
  };
  size?: number;
  timeout?: number;
  url?: string;
  skipThrowForStatus?: boolean;

  /**
   * Contains the characters that you want left unencoded in the query
   * params (`req.params`). If unspecified, `z.request()` will
   * percent-encode non-ascii characters and these reserved characters:
   * ``:$/?#[]@$&+,;=^@`\``.
   */
  skipEncodingChars?: string;

  /**
   * This is a special field that can be used to pass data to
   * middleware. It is not sent with the request, but is available in
   * the `response` object that middleware receives. This is useful for
   * things like `prefixErrorMessage` fields etc.
   */
  middlewareData?: Record<string, any>;
}

type HttpRequestOptionsWithUrl = HttpRequestOptions & { url: string };

interface BaseHttpResponse {
  status: number;
  headers: Headers;
  getHeader(key: string): string | undefined;
  throwForStatus(): void;
  skipThrowForStatus: boolean;
  request: HttpRequestOptions;
}

export interface HttpResponse<T = any> extends BaseHttpResponse {
  content: string;
  data: T;
  /** @deprecated Since v10.0.0. Use `data` instead. */
  json?: T;
}

export interface RawHttpResponse<T = any> extends BaseHttpResponse {
  body: NodeJS.ReadableStream;
  buffer(): Promise<Buffer>;
  json(): Promise<T>;
  text(): Promise<string>;
}

type DehydrateFunc = <T extends { [x: string]: any }>(
  func: (z: ZObject, bundle: Bundle<T>) => any,
  inputData?: T,
  cacheExpiration?: number,
) => string;

export interface ZObject {
  request: {
    // most specific overloads go first
    <T = any>(
      url: string,
      options: HttpRequestOptions & { raw: true },
    ): Promise<RawHttpResponse<T>>;
    <T = any>(
      options: HttpRequestOptionsWithUrl & { raw: true },
    ): Promise<RawHttpResponse<T>>;

    <T = any>(
      url: string,
      options?: HttpRequestOptions,
    ): Promise<HttpResponse<T>>;
    <T = any>(options: HttpRequestOptionsWithUrl): Promise<HttpResponse<T>>;
  };

  console: Console;

  dehydrate: DehydrateFunc;
  dehydrateFile: DehydrateFunc;

  cursor: {
    get: () => Promise<string>;
    set: (cursor: string) => Promise<null>;
  };
  generateCallbackUrl: () => string;

  /**
   * turns a file or request into a file into a publicly accessible url
   */
  stashFile: {
    (
      input: string | Buffer | NodeJS.ReadableStream,
      knownLength?: number,
      filename?: string,
      contentType?: string,
    ): string;
    (input: Promise<RawHttpResponse>): string;
    (input: Promise<string>): string;
  };

  JSON: {
    /**
     * Acts a lot like regular `JSON.parse`, but throws a nice error for improper json input
     */
    parse: (text: string) => any;
    stringify: typeof JSON.stringify;
  };

  /**
   * Easily hash data using node's crypto package
   * @param algorithm probably 'sha256', see [this](https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm_options) for more options
   * @param data the data you want to hash
   * @param encoding defaults to 'hex'
   * @param input_encoding defaults to 'binary'
   */
  hash: (
    algorithm: string,
    data: string,
    encoding?: string,
    input_encoding?: string,
  ) => string;

  errors: ErrorsModule;

  cache: {
    get: (key: string) => Promise<any>;
    set: (
      key: string,
      value: any,
      ttl?: number,
      scope?: string[],
      nx?: boolean,
    ) => Promise<boolean | null>;
    delete: (key: string) => Promise<boolean>;
  };
}

/**
 * A function that takes (z, bundle) and does something. The specific
 * Perform types from the functions module are now preferred.
 *
 * @template $InputData The shape of data in the `bundle.inputData` object.
 * @template $Return The return type of the function.
 *
 * @deprecated Use the relevant perform functions from the functions module.
 */
export type PerformFunction<
  $InputData extends Record<string, any> = Record<string, any>,
  $Return = any,
> = (z: ZObject, bundle: Bundle<$InputData>) => Promise<$Return>;

export type BeforeRequestMiddleware = (
  request: HttpRequestOptionsWithUrl,
  z: ZObject,
  bundle: Bundle,
) => HttpRequestOptionsWithUrl | Promise<HttpRequestOptionsWithUrl>;

export type AfterResponseMiddleware = (
  response: HttpResponse,
  z: ZObject,
  bundle: Bundle,
) => HttpResponse | Promise<HttpResponse>;

export interface BufferedItem<InputData = { [x: string]: any }> {
  inputData: InputData;
  meta: {
    id: string;
    [x: string]: any;
  };
}

export interface BufferedBundle<InputData = { [x: string]: any }> {
  authData: { [x: string]: string };
  buffer: BufferedItem<InputData>[];
  groupedBy: { [x: string]: string };
}

interface PerformBufferSuccessItem {
  outputData: { [x: string]: any };
  error?: string;
}

interface PerformBufferErrorItem {
  outputData?: { [x: string]: any };
  error: string;
}

export type PerformBufferResultItem =
  | PerformBufferSuccessItem
  | PerformBufferErrorItem;

export interface PerformBufferResult {
  [id: string]: PerformBufferResultItem;
}

export const performBuffer: (
  z: ZObject,
  bundle: BufferedBundle,
) => Promise<PerformBufferResult>;
