import { AesPkcs5 } from "./AesPkcs5";
import { IConnection } from "./IConnection";
import { IEncryptionPassword } from "./IEncryptionPassword";
import { IFetchRoute } from "./IFetchRoute";
import { IPropagation } from "./IPropagation";
import { FetcherBase } from "./internal/FetcherBase";

/**
 * Utility class for `fetch` functions used in `@nestia/sdk` with encryption.
 *
 * `EncryptedFetcher` is a utility class designed for SDK functions generated by
 * [`@nestia/sdk`](https://nestia.io/docs/sdk/sdk), interacting with the remote
 * HTTP API encrypted by AES-PKCS algorithm. In other words, this is a
 * collection of dedicated `fetch()` functions for `@nestia/sdk` with
 * encryption.
 *
 * For reference, `EncryptedFetcher` class being used only when target
 * controller method is encrypting body data by `@EncryptedRoute` or
 * `@EncryptedBody` decorators. If those decorators are not used,
 * {@link PlainFetcher} class would be used instead.
 *
 * @author Jeongho Nam - https://github.com/samchon
 */
export namespace EncryptedFetcher {
  /**
   * Fetch function only for `HEAD` method.
   *
   * @param connection Connection information for the remote HTTP server
   * @param route Route information about the target API
   * @returns Nothing because of `HEAD` method
   */
  export function fetch(
    connection: IConnection,
    route: IFetchRoute<"HEAD">,
  ): Promise<void>;

  /**
   * Fetch function only for `GET` method.
   *
   * @param connection Connection information for the remote HTTP server
   * @param route Route information about the target API
   * @returns Response body data from the remote API
   */
  export function fetch<Output>(
    connection: IConnection,
    route: IFetchRoute<"GET">,
  ): Promise<Output>;

  /**
   * Fetch function for the `POST`, `PUT`, `PATCH` and `DELETE` methods.
   *
   * @param connection Connection information for the remote HTTP server
   * @param route Route information about the target API
   * @returns Response body data from the remote API
   */
  export function fetch<Input, Output>(
    connection: IConnection,
    route: IFetchRoute<"POST" | "PUT" | "PATCH" | "DELETE">,
    input?: Input,
    stringify?: (input: Input) => string,
  ): Promise<Output>;

  export async function fetch<Input, Output>(
    connection: IConnection,
    route: IFetchRoute<"DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT">,
    input?: Input,
    stringify?: (input: Input) => string,
  ): Promise<Output> {
    if (
      (route.request?.encrypted === true || route.response?.encrypted) &&
      connection.encryption === undefined
    )
      throw new Error(
        "Error on EncryptedFetcher.fetch(): the encryption password has not been configured.",
      );
    const closure =
      typeof connection.encryption === "function"
        ? (direction: "encode" | "decode") =>
            (
              headers: Record<string, IConnection.HeaderValue | undefined>,
              body: string,
            ) =>
              (connection.encryption as IEncryptionPassword.Closure)({
                headers,
                body,
                direction,
              })
        : () => () => connection.encryption as IEncryptionPassword;

    return FetcherBase.request({
      className: "EncryptedFetcher",
      encode:
        route.request?.encrypted === true
          ? (input, headers) => {
              const p: IEncryptionPassword = closure("encode")(headers, input);
              return AesPkcs5.encrypt(
                (stringify ?? JSON.stringify)(input),
                p.key,
                p.iv,
              );
            }
          : (input) => input,
      decode:
        route.response?.encrypted === true
          ? (input, headers) => {
              const p: IEncryptionPassword = closure("decode")(headers, input);
              const s: string = AesPkcs5.decrypt(input, p.key, p.iv);
              return s.length ? JSON.parse(s) : s;
            }
          : (input) => input,
    })(connection, route, input, stringify);
  }

  export function propagate<Output extends IPropagation<any, any>>(
    connection: IConnection,
    route: IFetchRoute<"GET" | "HEAD">,
  ): Promise<Output>;

  export function propagate<Input, Output extends IPropagation<any, any>>(
    connection: IConnection,
    route: IFetchRoute<"DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT">,
    input?: Input,
    stringify?: (input: Input) => string,
  ): Promise<Output>;

  export async function propagate<Input, Output extends IPropagation<any, any>>(
    connection: IConnection,
    route: IFetchRoute<"DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT">,
    input?: Input,
    stringify?: (input: Input) => string,
  ): Promise<Output> {
    if (
      (route.request?.encrypted === true || route.response?.encrypted) &&
      connection.encryption === undefined
    )
      throw new Error(
        "Error on EncryptedFetcher.propagate(): the encryption password has not been configured.",
      );
    const closure =
      typeof connection.encryption === "function"
        ? (direction: "encode" | "decode") =>
            (
              headers: Record<string, IConnection.HeaderValue | undefined>,
              body: string,
            ) =>
              (connection.encryption as IEncryptionPassword.Closure)({
                headers,
                body,
                direction,
              })
        : () => () => connection.encryption as IEncryptionPassword;

    return FetcherBase.propagate({
      className: "EncryptedFetcher",
      encode:
        route.request?.encrypted === true
          ? (input, headers) => {
              const p: IEncryptionPassword = closure("encode")(headers, input);
              return AesPkcs5.encrypt(
                (stringify ?? JSON.stringify)(input),
                p.key,
                p.iv,
              );
            }
          : (input) => input,
      decode:
        route.response?.encrypted === true
          ? (input, headers) => {
              const p: IEncryptionPassword = closure("decode")(headers, input);
              const s: string = AesPkcs5.decrypt(input, p.key, p.iv);
              return s.length ? JSON.parse(s) : s;
            }
          : (input) => input,
    })(connection, route, input, stringify) as Promise<Output>;
  }
}
