package io.aiven.klaw.validation;

import static io.aiven.klaw.error.KlawErrorMessages.*;
import static io.aiven.klaw.helpers.KwConstants.ORDER_OF_TOPIC_ENVS;

import io.aiven.klaw.dao.Topic;
import io.aiven.klaw.model.enums.PermissionType;
import io.aiven.klaw.model.enums.RequestOperationType;
import io.aiven.klaw.model.enums.RequestStatus;
import io.aiven.klaw.model.requests.TopicRequestModel;
import io.aiven.klaw.model.response.EnvParams;
import io.aiven.klaw.service.CommonUtilsService;
import io.aiven.klaw.service.MailUtils;
import io.aiven.klaw.service.TopicControllerService;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

@Slf4j
public class TopicRequestValidatorImpl
    implements ConstraintValidator<TopicRequestValidator, TopicRequestModel> {

  @Autowired private CommonUtilsService commonUtilsService;
  @Autowired private MailUtils mailService;
  @Autowired private TopicControllerService topicControllerService;

  @Value("${klaw.validation.min.size.topicName:3}")
  private int minimumTopicNameSize;

  private PermissionType permissionType;

  @Override
  public void initialize(TopicRequestValidator constraintAnnotation) {
    this.permissionType = constraintAnnotation.getPermissionType();
  }

  @Override
  public boolean isValid(
      TopicRequestModel topicRequestModel, ConstraintValidatorContext constraintValidatorContext) {

    String userName = topicControllerService.getUserName();

    if (permissionType.equals(PermissionType.REQUEST_CREATE_TOPICS)) {
      // Verify if topic request type is Create/Promote
      if (RequestOperationType.CREATE != topicRequestModel.getRequestOperationType()
          && RequestOperationType.PROMOTE != topicRequestModel.getRequestOperationType()
          && topicRequestModel.getRequestId() == null) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_101);
        return false;
      }
    } else if (permissionType.equals(PermissionType.REQUEST_EDIT_TOPICS)) {
      if (RequestOperationType.UPDATE != topicRequestModel.getRequestOperationType()) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_102);
        return false;
      }
    } else {
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_103);
      return false;
    }

    // tenant filtering
    if (!commonUtilsService.getEnvsFromUserId(userName).contains(topicRequestModel.getEnvironment())
        && RequestOperationType.PROMOTE != topicRequestModel.getRequestOperationType()) {
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_104);
      return false;
    }

    // check for null topic name
    if (topicRequestModel.getTopicname() == null) {
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_105);
      return false;
    }

    // check for empty/whitespaces on topic name
    if (topicRequestModel.getTopicname().isBlank()) {
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_106);
      return false;
    }

    if (RequestOperationType.CREATE.equals(topicRequestModel.getRequestOperationType())
        && (topicRequestModel.getDescription() == null
            || topicRequestModel.getDescription().length() < 1
            || topicRequestModel.getDescription().length() > 100)) {
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_127);

      return false;
    }

    // verify tenant config exists
    int tenantId = commonUtilsService.getTenantId(userName);
    String syncCluster;
    try {
      syncCluster = topicControllerService.getSyncCluster(tenantId);
    } catch (Exception e) {
      log.error("Tenant Configuration not found. " + tenantId, e);
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_107);
      return false;
    }

    // Verify if topic requesting team exists
    Integer teamId = commonUtilsService.getTeamId(userName);
    if (null == teamId || teamId == 0) {
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_108);
      return false;
    }

    // Check if topic is owned by a different team, applicable on update requests
    List<Topic> topics =
        commonUtilsService.getTopicsForTopicName(topicRequestModel.getTopicname(), tenantId);
    if (topics != null
        && !topics.isEmpty()
        && !Objects.equals(
            topics
                .get(0) // as there could be only one owner team for topic, with topic name being
                // unique for tenant, getting the first element.
                .getTeamId(),
            commonUtilsService.getTeamId(userName))) {
      updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_109);
      return false;
    }

    // validation on promotion of a topic
    if (!checkIfPromotionOfTopic(
        topics, tenantId, topicRequestModel, syncCluster, constraintValidatorContext)) return false;

    if (!validateTopicConfigParameters(topicRequestModel, constraintValidatorContext)) return false;

    // Verify if topic request already exists
    if (topics != null && topicRequestModel.getRequestId() == null) {
      if (topicControllerService.existsTopicRequests(topicRequestModel, tenantId)) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_110);
        return false;
      }
    } else {
      // Editing an existing topic request of type DELETE/CLAIM is not possible.
      if (topicRequestModel.getRequestOperationType().equals(RequestOperationType.CLAIM)
          || topicRequestModel.getRequestOperationType().equals(RequestOperationType.DELETE)) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_122);
        return false;
      }

      // only requests in created state can be edited
      if (!topicControllerService
          .getTopicRequestFromTopicId(topicRequestModel.getRequestId(), tenantId)
          .getRequestStatus()
          .equals(RequestStatus.CREATED.value)) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_123);
        return false;
      }
    }

    // Check if topic exists on update/delete requests
    if (RequestOperationType.UPDATE == topicRequestModel.getRequestOperationType()
        || RequestOperationType.DELETE == topicRequestModel.getRequestOperationType()) {
      if (topics == null
          || topics.isEmpty()
          || topics.stream()
              .noneMatch(
                  topic -> topic.getEnvironment().equals(topicRequestModel.getEnvironment()))) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_124);
        return false;
      }
    }

    // Check if topic exists on claim requests
    if (RequestOperationType.CLAIM == topicRequestModel.getRequestOperationType()) {
      if (topics == null || topics.isEmpty()) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_124);
        return false;
      }
    }

    // Check if topic exists on cluster
    // Ignore topic exists check if Update request
    if (RequestOperationType.UPDATE != topicRequestModel.getRequestOperationType()) {
      boolean topicExists = false;
      if (topics != null) {
        topicExists =
            topics.stream()
                .anyMatch(
                    topicEx ->
                        Objects.equals(
                            topicEx.getEnvironment(), topicRequestModel.getEnvironment()));
      }
      if (topicExists) {
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_111);
        return false;
      }
    }

    return true;
  }

  private boolean checkIfPromotionOfTopic(
      List<Topic> topics,
      int tenantId,
      TopicRequestModel topicRequestModel,
      String syncCluster,
      ConstraintValidatorContext constraintValidatorContext) {
    String orderOfEnvs = commonUtilsService.getEnvProperty(tenantId, ORDER_OF_TOPIC_ENVS);
    boolean promotionOrderCheck = false;
    if (null != orderOfEnvs) {
      promotionOrderCheck = checkInPromotionOrder(topicRequestModel.getEnvironment(), orderOfEnvs);
    }

    if (topics != null && !topics.isEmpty()) {
      if (promotionOrderCheck) {
        int devTopicFound =
            (int)
                topics.stream()
                    .filter(topic -> Objects.equals(topic.getEnvironment(), syncCluster))
                    .count();
        if (devTopicFound != 1) {
          if (topicControllerService.getEnvDetails(syncCluster) == null) {
            updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_112);
          } else {
            updateConstraint(
                constraintValidatorContext,
                String.format(
                    TOPICS_VLD_ERR_113,
                    topicControllerService.getEnvDetails(syncCluster).getName()));
          }
          return false;
        }
      }
    } else if (!Objects.equals(topicRequestModel.getEnvironment(), syncCluster)) {
      if (promotionOrderCheck) {
        updateConstraint(
            constraintValidatorContext,
            String.format(
                TOPICS_VLD_ERR_114, topicControllerService.getEnvDetails(syncCluster).getName()));
        return false;
      }
    }

    return true;
  }

  private boolean validateTopicConfigParameters(
      TopicRequestModel topicRequestReq, ConstraintValidatorContext constraintValidatorContext) {

    EnvParams params =
        topicControllerService.getEnvDetails(topicRequestReq.getEnvironment()).getParams();
    if (params != null) {
      String topicPrefix = getValueOrDefault(params.getTopicPrefix(), "");
      String topicSuffix = getValueOrDefault(params.getTopicSuffix(), "");
      String topicRegex = getValueOrDefault(params.getTopicRegex(), "");
      String topicName = topicRequestReq.getTopicname();
      try {
        if (!params.isApplyRegex()) {
          if (topicPrefix != null && !topicPrefix.isBlank() && !topicName.startsWith(topicPrefix)) {
            log.error(
                "Topic prefix {} does not match. {}", topicPrefix, topicRequestReq.getTopicname());
            updateConstraint(
                constraintValidatorContext,
                String.format(TOPICS_VLD_ERR_115, topicRequestReq.getTopicname()));
            return false;
          }

          if (topicSuffix != null && !topicSuffix.isBlank() && !topicName.endsWith(topicSuffix)) {
            log.error(
                "Topic suffix {} does not match. {}", topicSuffix, topicRequestReq.getTopicname());
            updateConstraint(
                constraintValidatorContext,
                String.format(TOPICS_VLD_ERR_116, topicRequestReq.getTopicname()));
            return false;
          }
          // It will return before here if any other validation is not met.
          topicName = StringUtils.removeStart(topicName, topicPrefix);
          // if the prefix is set it will be removed by now, so we just need to see if there was any
          // over lap in the prefix.
          if (topicPrefix != null
              && topicSuffix != null
              && topicName.length() < topicSuffix.length()) {
            log.error(
                "Topic Suffix and Topic Prefix overlap there is a requirement for {} characters minimum to be unique between the prefix and suffix.",
                topicRequestReq.getTopicname(),
                minimumTopicNameSize);
            updateConstraint(
                constraintValidatorContext,
                String.format(
                    TOPICS_VLD_ERR_120, topicRequestReq.getTopicname(), minimumTopicNameSize));

            return false;
          }
          topicName = StringUtils.removeEnd(topicName, topicSuffix);

          // Check topic name without prefix or suffix meets minimum length requirements
          if (topicName.length() < minimumTopicNameSize) {
            log.error(
                "Topic name: {} is not long enough when prefix and suffix's are excluded. {} characters minimum are required to be unique.",
                topicRequestReq.getTopicname(),
                minimumTopicNameSize);
            updateConstraint(
                constraintValidatorContext,
                String.format(
                    TOPICS_VLD_ERR_119, topicRequestReq.getTopicname(), minimumTopicNameSize));
            return false;
          }

        } else {
          if (topicRegex != null
              && !topicRegex.isBlank()
              && !isRegexAMatch(topicRequestReq, topicRegex)) {

            log.error(
                "Topic Regex {} does not match. {}", topicRegex, topicRequestReq.getTopicname());
            updateConstraint(
                constraintValidatorContext,
                String.format(TOPICS_VLD_ERR_118, topicRequestReq.getTopicname()));
            return false;
          }
        }

      } catch (Exception e) {
        log.error("Unable to set topic partitions, setting default from properties.", e);
        updateConstraint(constraintValidatorContext, TOPICS_VLD_ERR_117);
        return false;
      }
    }
    return true;
  }

  // TODO Review rej and see if this would provide a better experience for Klaw.
  private boolean isRegexAMatch(TopicRequestModel topicRequestReq, String topicRegex) {
    Pattern p = Pattern.compile(topicRegex);
    Matcher m = p.matcher(topicRequestReq.getTopicname());
    return m.matches();
  }

  private static String getValueOrDefault(List<String> params, String defaultValue) {
    return (params != null && params.size() > 0) ? params.get(0) : defaultValue;
  }

  private void updateConstraint(
      ConstraintValidatorContext constraintValidatorContext, String errorMessage) {
    constraintValidatorContext
        .buildConstraintViolationWithTemplate(errorMessage)
        .addConstraintViolation()
        .disableDefaultConstraintViolation();
  }

  private boolean checkInPromotionOrder(String envId, String orderOfEnvs) {
    List<String> orderedEnv = Arrays.asList(orderOfEnvs.split(","));
    return orderedEnv.contains(envId);
  }
}
