package com.amazon.synthetics.group;

import com.amazon.synthetics.group.Utils.Constants;
import com.amazonaws.arn.Arn;
import java.util.List;
import java.util.Map;
import software.amazon.awssdk.services.synthetics.SyntheticsClient;
import software.amazon.awssdk.services.synthetics.model.AssociateResourceRequest;
import software.amazon.awssdk.services.synthetics.model.DisassociateResourceRequest;
import software.amazon.awssdk.services.synthetics.model.GetGroupRequest;
import software.amazon.awssdk.services.synthetics.model.GetGroupResponse;
import software.amazon.awssdk.services.synthetics.model.Group;
import software.amazon.awssdk.services.synthetics.model.ListGroupResourcesRequest;
import software.amazon.awssdk.services.synthetics.model.ListGroupResourcesResponse;
import software.amazon.awssdk.services.synthetics.model.ResourceNotFoundException;
import software.amazon.awssdk.services.synthetics.model.ValidationException;
import software.amazon.awssdk.regions.Region;
import software.amazon.cloudformation.Action;
import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
import software.amazon.cloudformation.exceptions.CfnResourceConflictException;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.Logger;
import software.amazon.cloudformation.proxy.OperationStatus;
import software.amazon.cloudformation.proxy.ProgressEvent;
import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;


/**
 * Base class for the functionality that could be shared across Create/Read/Update/Delete/List Handlers
  */
public abstract class BaseHandlerStd extends BaseHandler<CallbackContext> {
  //Adding default region for testing purposes
  protected Region region = Region.US_WEST_2;
  private final Action action;
  private GroupLogger logger;

  protected AmazonWebServicesClientProxy webServiceProxy;
  protected ResourceHandlerRequest<ResourceModel> request;
  protected CallbackContext callbackContext;
  protected ResourceModel model;
  protected ProxyClient<SyntheticsClient> proxyClient;
  // This is to handle making multiregion calls for associate resource and dissociate resource
  protected Map<Region, ProxyClient<SyntheticsClient>> proxyClientMap;

  public BaseHandlerStd(Action action) {
    this.action = action;
  }

  @Override
  public final ProgressEvent<ResourceModel, CallbackContext> handleRequest(
      final AmazonWebServicesClientProxy proxy,
      final ResourceHandlerRequest<ResourceModel> request,
      final CallbackContext callbackContext,
      final Logger logger
  ) {
    region = request.getRegion()!= null ? Region.of(request.getRegion()) : Region.US_WEST_2;
    proxyClientMap = ClientBuilder.getClientMap(proxy);
    return handleRequest(proxy, request, callbackContext, proxyClientMap, logger);
  }

  /**
   * This handleRequest with a proxy client is required to run unit tests
   * @param proxy
   * @param request
   * @param callbackContext
   * @param proxyClientMap
   * @param logger
   * @return
   */
  protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
      final AmazonWebServicesClientProxy proxy,
      final ResourceHandlerRequest<ResourceModel> request,
      final CallbackContext callbackContext,
      final Map<Region, ProxyClient<SyntheticsClient>> proxyClientMap,
      final Logger logger) {
    this.webServiceProxy = proxy;
    this.request = request;
    this.callbackContext = callbackContext != null ? callbackContext : CallbackContext.builder().build();
    this.model = request.getDesiredResourceState();
    this.logger = new GroupLogger(logger, action, request.getAwsAccountId(), callbackContext, model);
    this.proxyClientMap = proxyClientMap;
    this.proxyClient = proxyClientMap.get(region);

    log(Constants.INVOKING_HANDLER_MSG);
    ProgressEvent<ResourceModel, CallbackContext> response;
    try {
      response = handleRequest();
    } catch (Exception e) {
      log(e);
      throw e;
    }
    log(Constants.INVOKING_HANDLER_FINISHED_MSG);
    return response;

  }

  /**
   * Overridden in every handler based on the action
   * @return
   */
  protected abstract ProgressEvent<ResourceModel, CallbackContext> handleRequest();

  protected void log(String message) {
    logger.log(message);
  }

  protected void log(Exception exception) {
    logger.log(exception);
  }

  /**
   * Wrapper to call getGroup with Synthetics client and handle
   * the response/ error
   * @return Group
   */
  protected Group getGroupOrThrow() {
    try {
      log(Constants.GET_GROUP_CALL);
      GetGroupRequest getGroupRequest = com.amazon.synthetics.group.Translator.translateToReadRequest(model);
      GetGroupResponse getGroupResponse = webServiceProxy.injectCredentialsAndInvokeV2(getGroupRequest,
          proxyClient.client()::getGroup);
      return getGroupResponse.group();
    } catch (final ValidationException e) {
      throw new CfnInvalidRequestException(e.getMessage());
    } catch (ResourceNotFoundException e) {
      throw new CfnResourceConflictException(ResourceModel.TYPE_NAME, model.getName(), e.getMessage(), e);
    } catch (final Exception e) {
      throw new CfnGeneralServiceException(e.getMessage());
    }
  }

  /**
   * Wrapper to call getGroupResources api with Synthetics client and handle the response/error
   * @return List</String>: List of resource arns associated with the group
   */
  protected List<String> getGroupResourcesOrThrow() {
    try {
      log(Constants.LIST_GROUP_RESOURCES_CALL);
      ListGroupResourcesRequest listGroupResourcesRequest = ListGroupResourcesRequest.builder()
          .groupIdentifier(model.getName())
          .build();
      ListGroupResourcesResponse listGroupResourcesResponse = webServiceProxy.injectCredentialsAndInvokeV2(listGroupResourcesRequest,
          proxyClient.client()::listGroupResources);
      return listGroupResourcesResponse.resources();
    } catch (final ValidationException e) {
      throw new CfnInvalidRequestException(e.getMessage());
    } catch (ResourceNotFoundException e) {
      throw new CfnResourceConflictException(ResourceModel.TYPE_NAME, model.getName(), e.getMessage(), e);
    } catch (final Exception e) {
      throw new CfnGeneralServiceException(e.getMessage());
    }
  }

  /**
   * Wrapper around associateResource call to Synthetics client and handle the response/error
   * @param canaryArn: ResourceArn
   */
  protected void addAssociatedResource(String canaryArn) {
    // Translate resource model to create group request
    // call associate resource request
    log(Constants.MAKING_ADD_ASSOCIATE);
    try {
      Arn resourceArn = Arn.fromString(canaryArn);
      AssociateResourceRequest associateResourceRequest = AssociateResourceRequest.builder()
          .resourceArn(canaryArn)
          .groupIdentifier(model.getName())
          .build();
      webServiceProxy.injectCredentialsAndInvokeV2(associateResourceRequest,
          proxyClientMap.get(Region.of(resourceArn.getRegion())).client()::associateResource);
    } catch (final ValidationException e) {
      throw new CfnInvalidRequestException(e.getMessage());
    } catch (ResourceNotFoundException e) {
      throw new CfnResourceConflictException(ResourceModel.TYPE_NAME, canaryArn, e.getMessage(), e);
    } catch (final Exception e) {
      throw new CfnGeneralServiceException(e.getMessage());
    }
  }

  /**
   * Wrapper around associateResource call to Synthetics client and handle the response/error
   * @param canaryArn: ResourceArn
   */
  protected void removeAssociatedResource(String canaryArn) {
    log(Constants.MAKING_REMOVE_ASSOCIATE);
    try {
      Arn resourceArn = Arn.fromString(canaryArn);
      DisassociateResourceRequest disassociateResourceRequest = DisassociateResourceRequest.builder()
          .groupIdentifier(model.getName())
          .resourceArn(canaryArn)
          .build();
      webServiceProxy.injectCredentialsAndInvokeV2(disassociateResourceRequest, proxyClientMap.get(Region.of(resourceArn.getRegion())).client()::disassociateResource);
    } catch (final ValidationException e) {
      throw new CfnInvalidRequestException(e.getMessage());
    } catch (ResourceNotFoundException e) {
      throw new CfnResourceConflictException(ResourceModel.TYPE_NAME, canaryArn, e.getMessage(), e);
    } catch (final Exception e) {
      throw new CfnGeneralServiceException(e.getMessage());
    }
  }

  /**
   * Function to add AssociatedResources list. It determines which resource will be added in one round based on
   * AddResourceListIndex in the callbackContext
   * @param useResourceDiffList: boolean to indicate which list should be used to add (for update request this is true,
   *                           for create this is false
   * @return send back an in progress event
   */
  protected ProgressEvent<ResourceModel, CallbackContext> addAssociatedResources(boolean useResourceDiffList) {
    int index = callbackContext.getAddResourceListIndex();
    if (useResourceDiffList) {
      addAssociatedResource(callbackContext.getAddResourceList().get(index));
    } else {
      addAssociatedResource(model.getResourceArns().get(index));
    }
    callbackContext.setAddResourceListIndex(index + 1);
    return ProgressEvent.<ResourceModel, CallbackContext>builder()
        .resourceModel(model)
        .callbackContext(callbackContext)
        .callbackDelaySeconds(Constants.DEFAULT_CALLBACK_DELAY_SECONDS)
        .message(Constants.ADDING_RESOURCES_IN_PROGRESS)
        .status(OperationStatus.IN_PROGRESS)
        .build();
  }

  /**
   * Function to remove AssociatedResources list. It determines which resource will be added in one round based on
   * RemoveResourceListIndex in the callbackContext
   * @return send back an in progress event
   */
  protected ProgressEvent<ResourceModel, CallbackContext> removeAssociatedResources() {
    int index = callbackContext.getRemoveResourceListIndex();
    removeAssociatedResource(callbackContext.getRemoveResourceList().get(index));

    callbackContext.setRemoveResourceListIndex(index + 1);
    return ProgressEvent.<ResourceModel, CallbackContext>builder()
        .resourceModel(model)
        .callbackContext(callbackContext)
        .callbackDelaySeconds(Constants.DEFAULT_CALLBACK_DELAY_SECONDS)
        .message(Constants.REMOVING_RESOURCES_IN_PROGRESS)
        .status(OperationStatus.IN_PROGRESS)
        .build();
  }
}