import { 
  IHttpRequest, HttpRequest, IRequestLog, IProjectExecutionLog, Headers, IApplication, 
  IRequestProxyInit, IProxyResult, IHttpProjectProxyInit, 
  IApiError, ApiError, IHttpProjectStoreProxyInit 
} from "@api-client/core/build/browser.js";
import { Events } from "../../events/Events.js";
import { ProxyBindings } from "../base/ProxyBindings.js";

export interface IWebProxyInit {
  /**
   * Whether to use the current store environment to send the authentication credentials
   * with the proxy request. This is required when using the `net-store`.
   * 
   * @default false
   */
  authenticate?: boolean;
}

/**
 * A class that acts as an HTTP proxy for the application.
 * Since the Project runner and the CoreEngine are strictly NodeJS modules, this needs to proxy the request to the server that supports 
 * this API to make the request.
 * 
 * When using the proxy, the logic first sends request to the proxy with initialization values: the request data, authorization, and configuration.
 * In response, the proxy server responds with a URL (or a path) to use to send the body. The method of the request is used with the second request.
 * The proxy detects payload for requests other than GET and HEAD.
 */
export class WebProxyBindings extends ProxyBindings {
  /**
   * Whether to use the current store environment to send the authentication credentials
   * with the proxy request. This is required when using the `net-store`.
   */
  protected authenticate = false;

  /**
   * @param app The application information
   * @param proxyUrl The base URI to the HTTP proxy. 
   * @param authenticate Whether to use the current store environment to send authentication credentials.
   */
  constructor(app: IApplication, public proxyUrl: string, init: IWebProxyInit = {}) {
    super(app);
    if (typeof init.authenticate === 'boolean') {
      this.authenticate = init.authenticate;
    }
  }

  protected async listHeaders(): Promise<Record<string, string>> {
    const result: Record<string, string> = {
      'content-type': 'application/json',
    };
    if (this.authenticate) {
      const env = await Events.Store.Global.getEnv();
      if (!env || !env.token) {
        throw new Error(`You are not authorized in the store to make a proxy request.`);
      }
      result.authorization = `Bearer ${env.token}, ${env.location}`;
    }
    return result;
  }

  protected async proxy(body: IRequestProxyInit | IHttpProjectProxyInit | IHttpProjectStoreProxyInit): Promise<IProxyResult> {
    const copy = { ...body };
    const { signal } = copy;
    delete copy.signal;

    const headers = await this.listHeaders();
    const init: RequestInit = {
      body: JSON.stringify(body),
      method: 'POST',
      headers,
      cache: 'no-cache',
      signal,
    };
    const response = await fetch(this.proxyUrl, init);
    if (response.status !== 200) {
      const error = await response.json() as IApiError;
      throw new ApiError(error);
    }
    return response.json() as Promise<IProxyResult>;
  }

  async coreRequest(init: IRequestProxyInit): Promise<IProxyResult<IRequestLog>> {
    return this.proxy(init) as Promise<IProxyResult<IRequestLog>>;
  }

  async coreHttpProject(init: IHttpProjectProxyInit | IHttpProjectStoreProxyInit): Promise<IProxyResult<IProjectExecutionLog>> {
    return this.proxy(init) as Promise<IProxyResult<IProjectExecutionLog>>;
  }

  async httpSend(request: IHttpRequest, init: RequestInit = {}): Promise<Response> {
    const config: RequestInit = { ...init };
    if (request.headers) {
      if (!config.headers) {
        config.headers = {};
      }
      const parser = new Headers(request.headers);
      const headers = config.headers as Record<string, string>;
      parser.forEach((value: string, name: string) => {
        if (headers[name]) {
          headers[name] += `, ${value}`;
        } else {
          headers[name] = value;
        }
      });
    }
    if (request.method) {
      config.method = request.method;
    }
    if (request.payload) {
      const r = new HttpRequest(request);
      const payload = await r.readPayload();
      config.body = payload;
    }
    return fetch(request.url, config);
  }
}
