package io.aiven.klaw.service;

import io.aiven.klaw.config.ManageDatabase;
import io.aiven.klaw.dao.Approval;
import io.aiven.klaw.dao.Team;
import io.aiven.klaw.dao.UserInfo;
import io.aiven.klaw.error.KlawBadRequestException;
import io.aiven.klaw.error.KlawException;
import io.aiven.klaw.helpers.HandleDbRequests;
import io.aiven.klaw.model.enums.ApprovalType;
import io.aiven.klaw.model.enums.RequestEntityType;
import io.aiven.klaw.model.enums.RequestOperationType;
import io.aiven.klaw.service.MailUtils.MailType;
import java.util.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class ApprovalService {

  private final Map<String, List<Approval>> topicApprovals;

  private final Map<String, List<Approval>> aclApprovals;

  private final Map<String, List<Approval>> schemaApprovals;

  private final Map<String, List<Approval>> connectApprovals;

  @Autowired private MailUtils mailService;
  @Autowired private CommonUtilsService commonUtilsService;
  @Autowired ManageDatabase manageDatabase;

  @Value("${klaw.approvals.multiApproval:false}")
  private boolean allowMultiApproval;

  public ApprovalService(
      Map<String, List<Approval>> topicApprovals,
      Map<String, List<Approval>> aclApprovals,
      Map<String, List<Approval>> schemaApprovals,
      Map<String, List<Approval>> connectApprovals) {
    this.topicApprovals = topicApprovals;
    this.aclApprovals = aclApprovals;
    this.schemaApprovals = schemaApprovals;
    this.connectApprovals = connectApprovals;
  }

  private List<Approval> getApprovers(
      RequestEntityType entityType, RequestOperationType operationType, String envName)
      throws KlawException {

    Map<String, List<Approval>> requestToApprover = getEntityTypeApproverMap(entityType);
    List<Approval> approvals = null;

    if (envName != null) {
      approvals =
          requestToApprover.get(String.join(".", operationType.name(), envName.toUpperCase()));
    }
    if (approvals == null || approvals.isEmpty()) {
      // Env specific approvers do not exist so return all approvers for that type of operationType.
      // Always return a copy
      approvals = requestToApprover.get(operationType.name());
      // Always return a deep copy
    }
    return copyApprovalForReturn(approvals);
  }

  private static List<Approval> copyApprovalForReturn(List<Approval> approvals) {
    return approvals.stream().map(Approval::new).toList();
  }

  public List<Approval> getApprovalsForRequest(
      RequestEntityType entityType,
      RequestOperationType operationType,
      String envName,
      Integer resourceOwnerTeamId,
      Integer aclOwnerId,
      int tenantId)
      throws KlawException {
    List<Approval> approvals = getApprovers(entityType, operationType, envName);
    for (Approval app : approvals) {
      if (app.getApprovalType() == ApprovalType.TOPIC_TEAM_OWNER
          || app.getApprovalType() == ApprovalType.CONNECTOR_TEAM_OWNER) {
        app.setRequiredApprover(
            manageDatabase.getTeamNameFromTeamId(tenantId, resourceOwnerTeamId));
      } else if (app.getApprovalType() == ApprovalType.ACL_TEAM_OWNER) {
        app.setRequiredApprover(manageDatabase.getTeamNameFromTeamId(tenantId, aclOwnerId));
      }
    }
    return approvals;
  }

  private Map<String, List<Approval>> getEntityTypeApproverMap(RequestEntityType entityType)
      throws KlawException {
    switch (entityType) {
      case TOPIC:
        return topicApprovals;
      case ACL:
        return aclApprovals;
      case SCHEMA:
        return schemaApprovals;
      case CONNECTOR:
        return connectApprovals;
    }
    throw new KlawException("Unexpected Entity Type requested for Approval");
  }

  public void sendEmailToApprovers(
      String userName,
      String resourceName,
      String acl,
      String reasonToDecline,
      MailType mailType,
      List<Approval> approvals,
      int tenantId) {

    Set<Integer> approverIds = getApproverIds(approvals, tenantId);

    for (Integer approverId : approverIds) {

      mailService.sendMail(
          resourceName,
          acl,
          reasonToDecline,
          userName,
          userName,
          approverId,
          manageDatabase.getHandleDbRequests(),
          mailType,
          commonUtilsService.getLoginUrl());
    }
  }

  public boolean isRequestFullyApproved(List<Approval> approvals) throws KlawException {
    return getRemainingApprovals(approvals).isEmpty();
  }

  private static List<Approval> getRemainingApprovals(List<Approval> approvals) {
    return approvals.stream()
        .filter(
            app -> StringUtils.isBlank(app.getApproverName()) || app.getApproverName().isEmpty())
        .toList();
  }

  /**
   * Check if a user has the ability to add an approvval to this list of approvals
   *
   * @param approvals The full list of approvals
   * @param username The username of the user to check
   * @return true if there is an approval that the user fits the requirements for to approve
   */
  public boolean isUserAQualifiedOutstandingApprover(List<Approval> approvals, String username) {
    Optional<UserInfo> user =
        manageDatabase.selectAllCachedUserInfo().stream()
            .filter(u -> u.getUsername().equals(username))
            .findFirst();
    if (user.isEmpty()) {
      return false;
    }
    // check that user has not foundapproved any request before potentially as a member of another
    // team
    // Then check to see if their current team has the ability to approve any of the requests.
    return approvals.stream()
            .noneMatch(
                app ->
                    !StringUtils.isBlank(app.getApproverName())
                        && app.getApproverName().equals(username))
        && !approvals.stream()
            .filter(
                app ->
                    app.getRequiredApprover()
                        .equals(
                            manageDatabase.getTeamNameFromTeamId(
                                user.get().getTenantId(), user.get().getTeamId())))
            .toList()
            .isEmpty();
  }

  public List<Approval> addApproval(
      List<Approval> approvals, String userName, Integer resourceOwnerId, Integer aclOwnerId)
      throws KlawBadRequestException {

    // If this user has already approved once they can not approve a second time.
    if (approvals.stream()
        .anyMatch(
            app ->
                !StringUtils.isBlank(app.getApproverName())
                    && app.getApproverName().equals(userName))) {
      throw new KlawBadRequestException(
          String.format(
              "User %s has already provided one approval, another approver is required to complete the approval process.",
              userName));
    }

    List<Approval> remaining = getRemainingApprovals(approvals);

    Optional<UserInfo> user =
        manageDatabase.selectAllCachedUserInfo().stream()
            .filter(u -> u.getUsername().equals(userName))
            .findFirst();
    Set<ApprovalType> completedApprovalTypes = new HashSet<>();

    if (user.isPresent()) {
      for (Approval approval : remaining) {
        if ((completedApprovalTypes.isEmpty() || allowMultiApproval)
            && completedApprovalTypes.add(approval.getApprovalType())) {
          switch (approval.getApprovalType()) {
            case TOPIC_TEAM_OWNER, CONNECTOR_TEAM_OWNER -> isResourceApprovalSatisfied(
                user.get(), resourceOwnerId, approval, approvals);

            case ACL_TEAM_OWNER -> isAclApprovalSatisfied(
                user.get(), aclOwnerId, approval, approvals);

            case TEAM -> isTeamApprovalSatisfied(user.get(), approval, approvals);
          }
        } else {
          log.debug(
              "User {} has already provided one approval of type {} another approver is required to complete the approval process.",
              user.get().getUsername(),
              approval.getApprovalType());
        }
      }
    }
    return approvals;
  }

  private void isAclApprovalSatisfied(
      UserInfo user, int aclOwnerId, Approval approval, List<Approval> approvals) {
    log.info("isAclApprovalSatisfied {} == {}", user.getTeamId(), aclOwnerId);
    if (user.getTeamId() == aclOwnerId) {
      addApproverToApprovalList(user, approval, approvals, user.getTenantId());
    }
  }

  private void isResourceApprovalSatisfied(
      UserInfo user, int resourceOwnerId, Approval approval, List<Approval> approvals) {
    log.info("isResourceApprovalSatisfied {} == {}", user.getTeamId(), resourceOwnerId);
    if (user.getTeamId() == resourceOwnerId) {
      addApproverToApprovalList(user, approval, approvals, user.getTenantId());
    }
  }

  private void isTeamApprovalSatisfied(UserInfo user, Approval approval, List<Approval> approvals) {

    if (Objects.equals(
        manageDatabase.getTeamNameFromTeamId(user.getTenantId(), user.getTeamId()),
        approval.getRequiredApprover())) {
      addApproverToApprovalList(user, approval, approvals, user.getTenantId());
    }
  }

  private void addApproverToApprovalList(
      UserInfo user, Approval approval, List<Approval> approvals, int tenantId) {
    try {

      approvals.stream()
          .filter(app -> app.getApprovalId().equals(approval.getApprovalId()))
          .findFirst()
          .ifPresent(
              approve -> {
                // Add the Approval Details
                approve.setApproverName(user.getUsername());
                approve.setApproverTeamId(user.getTeamId());
                approve.setApproverTeamName(
                    manageDatabase.getTeamNameFromTeamId(tenantId, user.getTeamId()));
              });

    } catch (Exception ex) {
      log.error("Exception caught while adding approver to the approval list.");
    }
  }

  public Set<String> getApproverEmails(List<Approval> approvals, int tenantId) {

    return getApproverTeamEmails(
        manageDatabase.getHandleDbRequests(), getApproverIds(approvals, tenantId), tenantId);
  }

  public Set<Integer> getApproverIds(List<Approval> approvals, int tenantId) {
    Set<Integer> approverIds = new HashSet<>();

    for (Approval approver : approvals) {
      CollectionUtils.addIgnoreNull(
          approverIds,
          manageDatabase.getTeamIdFromTeamName(tenantId, approver.getRequiredApprover()));
    }

    return approverIds;
  }

  public Set<String> getApproverTeamEmails(
      HandleDbRequests dbHandle, Set<Integer> teamIds, int tenantId) {
    Set<String> emailAddresses = new HashSet<>();
    for (Integer teamId : teamIds) {
      CollectionUtils.addIgnoreNull(emailAddresses, getTeamMail(dbHandle, tenantId, teamId));
    }
    return emailAddresses;
  }

  private static String getTeamMail(HandleDbRequests dbHandle, int tenantId, Integer teamId) {
    Team team = dbHandle.getTeamDetails(teamId, tenantId);
    return team != null ? team.getTeammail() : null;
  }
}
