// The contents of this file were taken from the AWS CDK provider framework.
// https://github.com/aws/aws-cdk/blob/c52ff08cfd1515d35feb93bcba34a3231a94985c/packages/aws-cdk-lib/custom-resources/lib/provider-framework/runtime/cfn-response.ts

/* eslint-disable prefer-arrow/prefer-arrow-functions */
/* eslint-disable func-style */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import * as url from 'url';
import { httpRequest } from './outbound';
import { log, withRetries } from './util';

export const CREATE_FAILED_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::CREATE_FAILED';
export const MISSING_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID';

export interface CloudFormationResponseOptions {
  readonly reason?: string;
  readonly noEcho?: boolean;
}

export interface CloudFormationEventContext {
  StackId: string;
  RequestId: string;
  PhysicalResourceId?: string;
  LogicalResourceId: string;
  ResponseURL: string;
  Data?: any;
}

export async function submitResponse(
  status: 'SUCCESS' | 'FAILED',
  event: CloudFormationEventContext,
  options: CloudFormationResponseOptions = {},
) {
  const json: AWSLambda.CloudFormationCustomResourceResponse = {
    Status: status,
    Reason: options.reason || status,
    StackId: event.StackId,
    RequestId: event.RequestId,
    PhysicalResourceId: event.PhysicalResourceId || MISSING_PHYSICAL_ID_MARKER,
    LogicalResourceId: event.LogicalResourceId,
    NoEcho: options.noEcho,
    Data: event.Data,
  };

  log('submit response to cloudformation', json);

  const responseBody = JSON.stringify(json);

  const parsedUrl = url.parse(event.ResponseURL);

  const retryOptions = {
    attempts: 5,
    sleep: 1000,
  };
  await withRetries(retryOptions, httpRequest)(
    {
      hostname: parsedUrl.hostname,
      path: parsedUrl.path,
      method: 'PUT',
      headers: {
        'content-type': '',
        'content-length': Buffer.byteLength(responseBody, 'utf8'),
      },
    },
    responseBody,
  );
}

export function safeHandler(block: (event: any, context?: any) => Promise<void>) {
  return async (event: any, context: any) => {
    // ignore DELETE event when the physical resource ID is the marker that
    // indicates that this DELETE is a subsequent DELETE to a failed CREATE
    // operation.
    if (event.RequestType === 'Delete' && event.PhysicalResourceId === CREATE_FAILED_PHYSICAL_ID_MARKER) {
      console.log('ignoring DELETE event caused by a failed CREATE event');
      await submitResponse('SUCCESS', event);
      return;
    }

    try {
      await block(event, context);
    } catch (e: any) {
      // tell waiter state machine to retry
      if (e instanceof Retry) {
        console.log('retry requested by handler');
        throw e;
      }

      if (!event.PhysicalResourceId) {
        // special case: if CREATE fails, which usually implies, we usually don't
        // have a physical resource id. in this case, the subsequent DELETE
        // operation does not have any meaning, and will likely fail as well. to
        // address this, we use a marker so the provider framework can simply
        // ignore the subsequent DELETE.
        if (event.RequestType === 'Create') {
          console.log('CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored');
          event.PhysicalResourceId = CREATE_FAILED_PHYSICAL_ID_MARKER;
        } else {
          // otherwise, if PhysicalResourceId is not specified, something is
          // terribly wrong because all other events should have an ID.
          console.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify({ ...event, ResponseURL: '...' })}`);
        }
      }

      // this is an actual error, fail the activity altogether and exist.
      await submitResponse('FAILED', event, {
        reason: e.message,
      });
    }
  };
}

export class Retry extends Error {}
