// Import libs
import qs from 'qs';

// Import dce-commonkit
import {
  ErrorWithCode,
} from 'dce-commonkit';

// Import data signer
import { signRequest } from '../dataSigner';

// Import shared types
import ExpressKitErrorCode from '../../types/ExpressKitErrorCode';

/*------------------------------------------------------------------------*/
/* ----------------------------- Credentials ---------------------------- */
/*------------------------------------------------------------------------*/

/*
DCEKIT_CROSS_SERVER_CREDENTIALS format:
|host:key:secret||host:key:secret|...
*/

const credentials: {
  host: string,
  key: string,
  secret: string,
}[] = [];

/*------------------------------------------------------------------------*/
/* ------------------------------- Helpers ------------------------------ */
/*------------------------------------------------------------------------*/

/**
 * Parse a credentials string into an array of credentials
 * @author Gabe Abrams
 * @param credentialsStr the credentials string to parse
 * @returns an array of credentials
 */
const parseCredentials = (credentialsStr: string) => {
  credentialsStr
    // Replace multiple | with a single one
    .replace(/\|+/g, '|')
    // Split by |
    .split('|')
    // Remove empty strings
    .filter((str) => {
      return str.trim().length > 0;
    })
    // Process each credential
    .forEach((str) => {
      // Split by :
      const parts = str.split(':');

      // Check for errors
      if (parts.length !== 3) {
        throw new ErrorWithCode(
          'Invalid DCEKIT_CROSS_SERVER_CREDENTIALS format. Each credential must be in the format |host:key:secret|',
          ExpressKitErrorCode.InvalidCrossServerCredentialsFormat,
        );
      }

      // Add the credential to the array
      // (don't add duplicates)
      const alreadyExists = credentials.find((cred) => {
        return (
          cred.host === parts[0].trim()
        );
      });
      if (!alreadyExists) {
        credentials.push({
          host: parts[0].trim(),
          key: parts[1].trim(),
          secret: parts[2].trim(),
        });
      }
    });
};

// Auto-parse credentials from env variable
parseCredentials(process.env.DCEKIT_CROSS_SERVER_CREDENTIALS ?? '');

/*------------------------------------------------------------------------*/
/* ------------------------------- Helpers ------------------------------ */
/*------------------------------------------------------------------------*/

/**
 * Get the credential to use for the request to another server
 * @author Gabe Abrams
 * @param host the host of the other server
 * @return the credential to use
 */
const getCrossServerCredential = (host: string) => {
  // Find the credential
  const credential = credentials.find((cred) => {
    return cred.host.toLowerCase() === host.toLowerCase();
  });
  if (!credential) {
    throw new ErrorWithCode(
      'Cannot send cross-server signed request there was no credential that matched the host that the request is being sent to.',
      ExpressKitErrorCode.CrossServerNoCredentialsToSignWith,
    );
  }

  // Return credential
  return credential;
};

/*------------------------------------------------------------------------*/
/* -------------------------------- Main -------------------------------- */
/*------------------------------------------------------------------------*/

/**
 * Sends and retries an http request
 * @author Gabriel Abrams
 * @param opts object containing all arguments
 * @param opts.path path to send request to
 * @param [opts.host] host to send request to
 * @param [opts.method=GET] http method to use
 * @param [opts.params] body/data to include in the request
 * @param [opts.responseType=JSON] expected response type
 * @param [opts.dceKitCrossServerCredentials] additional cross-server credentials
 *   that aren't in the env var list
 * @returns { body, status, headers } on success
 */
const sendServerToServerRequest = async (
  opts: {
    path: string,
    host?: string,
    method?: ('GET' | 'POST' | 'PUT' | 'DELETE'),
    params?: { [k in string]: any },
    responseType?: 'Text' | 'JSON',
    dceKitCrossServerCredentials?: string,
  },
): Promise<{
  body: any,
  status: number,
  headers: { [k in string]: any },
}> => {
  // If additional credentials were provided, parse them and add to the list
  if (opts.dceKitCrossServerCredentials) {
    parseCredentials(opts.dceKitCrossServerCredentials);
  }

  // Process method
  const method: ('GET' | 'POST' | 'PUT' | 'DELETE') = (opts.method || 'GET');

  // Encode objects within params
  let params: {
    [k in string]: any
  } | undefined;
  if (opts.params) {
    params = {};
    Object.entries(opts.params).forEach(([key, val]) => {
      if (typeof val === 'object' && !Array.isArray(val)) {
        (params as any)[key] = JSON.stringify(val);
      } else {
        (params as any)[key] = val;
      }
    });
  }

  // Get cross-server credential
  const credential = getCrossServerCredential(opts.host);

  // Sign the request, get new params
  params = await signRequest({
    method: opts.method,
    path: opts.path,
    params: params ?? {},
    key: credential.key,
    secret: credential.secret,
  });

  // Stringify parameters
  const stringifiedParams = qs.stringify(
    params || {},
    {
      encodeValuesOnly: true,
      arrayFormat: 'brackets',
    },
  );

  // Create url (include query if GET)
  const query = (method === 'GET' ? `?${stringifiedParams}` : '');
  let url;
  if (!opts.host) {
    // No host included at all. Just send to a path
    url = `${opts.path}${query}`;
  } else {
    url = `https://${opts.host}${opts.path}${query}`;
  }

  // Update headers
  const headers: {
    [k: string]: any,
  } = {};
  let data: string | null | { [k: string]: any } | undefined = null;
  if (!headers['Content-Type']) {
    // Form encoded
    headers['Content-Type'] = 'application/x-www-form-urlencoded';
    // Add data if applicable
    data = (method !== 'GET' ? stringifiedParams : null);
  } else {
    // JSON encode
    data = params;
  }

  // Encode data
  let encodedData: URLSearchParams | string | undefined;
  if (data) {
    if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
      encodedData = new URLSearchParams(params);
    } else {
      encodedData = JSON.stringify(data);
    }
  }

  // Send request
  try {
    const response = await fetch(
      url,
      {
        method,
        mode: 'cors',
        headers: headers ?? {},
        body: (
          (method !== 'GET' && encodedData)
            ? encodedData
            : undefined
        ),
        redirect: 'follow',
      },
    );

    // Get headers map
    const responseHeaders: {
      [k in string]: string
    } = {};
    response.headers.forEach((value, key) => {
      responseHeaders[key] = value;
    });

    // Process response based on responseType
    try {
      // Parse response
      let responseBody: any;
      if (
        opts.responseType
        && opts.responseType === 'Text'
      ) {
        // Response type is text
        responseBody = await response.text();
      } else {
        // Response type is JSON
        responseBody = await response.json();
      }

      // Return response
      return {
        body: responseBody,
        status: response.status,
        headers: responseHeaders,
      };
    } catch (err) {
      throw new ErrorWithCode(
        `Failed to parse response as ${opts.responseType}: ${(err as any)?.message}`,
        ExpressKitErrorCode.ResponseParseError,
      );
    }
  } catch (err) {
    // Self-signed certificate error:
    if ((err as any)?.message?.includes('self signed certificate')) {
      throw new ErrorWithCode(
        'We refused to send a request because the receiver has self-signed certificates.',
        ExpressKitErrorCode.SelfSigned,
      );
    }

    // No tries left
    throw new ErrorWithCode(
      `We encountered an error when trying to send a network request. If this issue persists, contact an admin. Error: ${(err as any)?.message}`,
      ExpressKitErrorCode.NotConnected,
    );
  }
};

export default sendServerToServerRequest;
