/**
   * Finds the right index at which to insert a new data approval level. Returns -1 if the new data
   * approval level is a duplicate.
   *
   * @param dataApprovalLevels list of all levels.
   * @param newLevel new level to find the insertion point for.
   * @return index where the new approval level should be inserted, or -1 if the new level is a
   *     duplicate.
   */
  private int getInsertIndex(
      List<DataApprovalLevel> dataApprovalLevels, DataApprovalLevel newLevel) {
    int i = dataApprovalLevels.size() - 1;

    while (i >= 0) {
      DataApprovalLevel test = dataApprovalLevels.get(i);

      int orgLevelDifference = newLevel.getOrgUnitLevel() - test.getOrgUnitLevel();

      if (orgLevelDifference > 0) {
        break;
      }

      if (orgLevelDifference == 0) {
        if (newLevel.levelEquals(test)) {
          return -1;
        }

        if (test.getCategoryOptionGroupSet() == null) {
          break;
        }
      }

      i--;
    }

    return i + 1;
  }
  @Override
  public DataApprovalLevel getLowestDataApprovalLevel(
      OrganisationUnit orgUnit, DataElementCategoryOptionCombo attributeOptionCombo) {
    Set<CategoryOptionGroupSet> cogSets = null;

    if (attributeOptionCombo != null
        && attributeOptionCombo != categoryService.getDefaultDataElementCategoryOptionCombo()) {
      cogSets = new HashSet<>();

      for (DataElementCategoryOption option : attributeOptionCombo.getCategoryOptions()) {
        if (option.getGroupSets() != null) {
          cogSets.addAll(option.getGroupSets());
        }
      }
    }

    int orgUnitLevel = orgUnit.getLevel();

    List<DataApprovalLevel> approvalLevels = getDataApprovalLevelsByOrgUnitLevel(orgUnitLevel);

    for (DataApprovalLevel level : Lists.reverse(approvalLevels)) {
      if (level.getCategoryOptionGroupSet() == null) {
        if (cogSets == null) {
          return level;
        }
      } else if (cogSets != null && cogSets.contains(level.getCategoryOptionGroupSet())) {
        return level;
      }
    }

    return null;
  }
  /**
   * Get the approval level for an organisation unit that is required in order for the user to see
   * the data, assuming user is limited to seeing approved data only from lower approval levels.
   *
   * @param orgUnit organisation unit to test.
   * @param user the user.
   * @param approvalLevels all data approval levels.
   * @return required approval level for user to see the data.
   */
  private int requiredApprovalLevel(
      OrganisationUnit orgUnit, User user, List<DataApprovalLevel> approvalLevels) {
    DataApprovalLevel userLevel = getUserApprovalLevel(orgUnit, user, approvalLevels);

    int totalLevels = approvalLevels.size();

    return userLevel == null
        ? 0
        : userLevel.getLevel() == totalLevels
            ? APPROVAL_LEVEL_UNAPPROVED
            : userLevel.getLevel() + 1;
  }
  @Override
  public boolean canDataApprovalLevelMoveUp(int level) {
    List<DataApprovalLevel> dataApprovalLevels = getAllDataApprovalLevels();

    int index = level - 1;

    if (index <= 0 || index >= dataApprovalLevels.size()) {
      return false;
    }

    DataApprovalLevel test = dataApprovalLevels.get(index);
    DataApprovalLevel previous = dataApprovalLevels.get(index - 1);

    return test.getOrgUnitLevel() == previous.getOrgUnitLevel()
        && previous.getCategoryOptionGroupSet() != null;
  }
  @Override
  public boolean canDataApprovalLevelMoveDown(int level) {
    List<DataApprovalLevel> dataApprovalLevels = getAllDataApprovalLevels();

    int index = level - 1;

    if (index < 0 || index + 1 >= dataApprovalLevels.size()) {
      return false;
    }

    DataApprovalLevel test = dataApprovalLevels.get(index);
    DataApprovalLevel next = dataApprovalLevels.get(index + 1);

    return test.getOrgUnitLevel() == next.getOrgUnitLevel()
        && test.getCategoryOptionGroupSet() != null;
  }
  /**
   * Get the approval level for a user for a given organisation unit. It is assumed that the user
   * has access to the organisation unit (must be checked elsewhere, it is not checked here.) If the
   * organisation unit is above all approval levels, returns null (no approval levels apply.)
   *
   * <p>If users are restricted to viewing approved data only, users may see data from lower levels
   * *only* if it is approved *below* this approval level (higher number approval level). Or, if
   * this method returns the lowest (highest number) approval level, users may see unapproved data.
   *
   * <p>If users have approve/unapprove authority (checked elsewhere, not here), the returned level
   * is the level at which users may approve/unapprove. If users have authority to approve at lower
   * levels, they may approve at levels below the returned level.
   *
   * <p>If users have accept/unaccept authority (checked elsewhere, not here), users may
   * accept/unaccept at the level just *below* this level.
   *
   * @param orgUnit organisation unit to test.
   * @param user the user.
   * @param approvalLevels app data approval levels.
   * @return approval level for user.
   */
  private DataApprovalLevel getUserApprovalLevel(
      OrganisationUnit orgUnit, User user, List<DataApprovalLevel> approvalLevels) {
    int userOrgUnitLevel = orgUnit.getLevel();

    DataApprovalLevel userLevel = null;

    for (DataApprovalLevel level : approvalLevels) {
      if (level.getOrgUnitLevel() >= userOrgUnitLevel
          && securityService.canRead(level)
          && canReadCOGS(user, level.getCategoryOptionGroupSet())) {
        userLevel = level;
        break;
      }
    }

    return userLevel;
  }
  @Override
  public Set<OrganisationUnitLevel> getOrganisationUnitApprovalLevels() {
    Set<OrganisationUnitLevel> orgUnitLevels = new HashSet<>();

    List<DataApprovalLevel> dataApprovalLevels = dataApprovalLevelStore.getAllDataApprovalLevels();

    for (DataApprovalLevel level : dataApprovalLevels) {
      OrganisationUnitLevel orgUnitLevel =
          organisationUnitService.getOrganisationUnitLevelByLevel(level.getOrgUnitLevel());

      if (orgUnitLevel != null) {
        orgUnitLevels.add(orgUnitLevel);
      }
    }

    return orgUnitLevels;
  }
  @Override
  public List<DataApprovalLevel> getAllDataApprovalLevels() {
    List<DataApprovalLevel> dataApprovalLevels = dataApprovalLevelStore.getAllDataApprovalLevels();

    for (DataApprovalLevel dataApprovalLevel : dataApprovalLevels) {
      int ouLevelNumber = dataApprovalLevel.getOrgUnitLevel();

      OrganisationUnitLevel ouLevel =
          organisationUnitService.getOrganisationUnitLevelByLevel(ouLevelNumber);

      String ouLevelName =
          ouLevel != null ? ouLevel.getName() : "Organisation unit level " + ouLevelNumber;

      dataApprovalLevel.setOrgUnitLevelName(ouLevelName);
    }

    return dataApprovalLevels;
  }
  @Override
  public List<DataApprovalLevel> getUserDataApprovalLevels() {
    UserCredentials userCredentials = currentUserService.getCurrentUser().getUserCredentials();

    boolean mayApprove = userCredentials.isAuthorized(DataApproval.AUTH_APPROVE);
    boolean mayApproveAtLowerLevels =
        userCredentials.isAuthorized(DataApproval.AUTH_APPROVE_LOWER_LEVELS);
    boolean mayAcceptAtLowerLevels =
        userCredentials.isAuthorized(DataApproval.AUTH_ACCEPT_LOWER_LEVELS);

    if (!mayApprove && !mayApproveAtLowerLevels && !mayAcceptAtLowerLevels) {
      return new ArrayList<>();
    }

    int lowestNumberOrgUnitLevel = getCurrentUsersLowestNumberOrgUnitLevel();

    boolean canSeeAllDimensions =
        CollectionUtils.isEmpty(userService.getCoDimensionConstraints(userCredentials))
            && CollectionUtils.isEmpty(userService.getCogDimensionConstraints(userCredentials));

    List<DataApprovalLevel> approvalLevels = getAllDataApprovalLevels();
    List<DataApprovalLevel> userDataApprovalLevels = new ArrayList<>();

    boolean addLevel = false;

    for (DataApprovalLevel approvalLevel : approvalLevels) {
      if (!addLevel && approvalLevel.getOrgUnitLevel() >= lowestNumberOrgUnitLevel) {
        CategoryOptionGroupSet cogs = approvalLevel.getCategoryOptionGroupSet();

        addLevel =
            securityService.canRead(approvalLevel) && cogs == null
                ? canSeeAllDimensions
                : (securityService.canRead(cogs)
                    && !CollectionUtils.isEmpty(categoryService.getCategoryOptionGroups(cogs)));
      }

      if (addLevel) {
        userDataApprovalLevels.add(approvalLevel);
      }
    }

    return userDataApprovalLevels;
  }
  @Override
  public boolean dataApprovalLevelExists(DataApprovalLevel level) {
    List<DataApprovalLevel> dataApprovalLevels = getAllDataApprovalLevels();

    for (DataApprovalLevel dataApprovalLevel : dataApprovalLevels) {
      if (level.levelEquals(dataApprovalLevel)) {
        return true;
      }
    }

    return false;
  }
  @Override
  public DataApprovalLevel getHighestDataApprovalLevel(OrganisationUnit orgUnit) {
    int orgUnitLevel = orgUnit.getLevel();

    DataApprovalLevel levelAbove = null;

    int levelAboveOrgUnitLevel = 0;

    List<DataApprovalLevel> userApprovalLevels = getUserDataApprovalLevels();

    for (DataApprovalLevel level : userApprovalLevels) {
      log.debug("Get highest data approval level: " + level.getName());

      if (level.getOrgUnitLevel() == orgUnitLevel) {
        return level; // Exact match on org unit level.
      } else if (level.getOrgUnitLevel() > levelAboveOrgUnitLevel) {
        levelAbove = level; // Must be first matching approval level for this org unit level.

        levelAboveOrgUnitLevel = level.getOrgUnitLevel();
      }
    }

    return levelAbove; // Closest ancestor above, or null if none.
  }
  @Override
  public Map<OrganisationUnit, Integer> getUserReadApprovalLevels(DataApprovalLevel approvalLevel) {
    Map<OrganisationUnit, Integer> map = new HashMap<>();

    User user = currentUserService.getCurrentUser();

    Collection<OrganisationUnit> orgUnits = user.getDataViewOrganisationUnits();

    if (orgUnits == null || orgUnits.isEmpty()) {
      orgUnits = organisationUnitService.getRootOrganisationUnits();
    }

    for (OrganisationUnit orgUnit : orgUnits) {
      map.put(orgUnit, approvalLevel.getLevel());
    }

    return map;
  }
  @Override
  public void deleteDataApprovalLevel(DataApprovalLevel dataApprovalLevel) {
    List<DataApprovalLevel> dataApprovalLevels = getAllDataApprovalLevels();

    int index = dataApprovalLevel.getLevel() - 1;

    if (index >= 0 && index < dataApprovalLevels.size()) {
      dataApprovalLevelStore.delete(dataApprovalLevel);

      dataApprovalLevels.remove(index);

      // Move up from here to end, to avoid duplicate level in database.

      for (int i = index; i < dataApprovalLevels.size(); i++) {
        update(dataApprovalLevels.get(i), i);
      }
    }
  }
  @Override
  public boolean prepareAddDataApproval(DataApprovalLevel level) {
    List<DataApprovalLevel> dataApprovalLevels = getAllDataApprovalLevels();

    int index = getInsertIndex(dataApprovalLevels, level);

    if (index < 0) {
      return false;
    }

    dataApprovalLevels.add(index, level);

    // Move down from end to here, to avoid duplicate level in database.

    for (int i = dataApprovalLevels.size() - 1; i > index; i--) {
      update(dataApprovalLevels.get(i), i);
    }

    level.setLevel(index + 1);

    return true;
  }
  /**
   * Updates a data approval level object by setting the level to correspond with the list index,
   * setting the updated date to now, and updating the object on disk.
   *
   * @param dataApprovalLevel data approval level to update
   * @param index index of the object (used to set the level.)
   */
  private void update(DataApprovalLevel dataApprovalLevel, int index) {
    dataApprovalLevel.setLevel(index + 1);

    dataApprovalLevelStore.update(dataApprovalLevel);
  }
  @Override
  public int addDataApprovalLevel(DataApprovalLevel approvalLevel, int level) {
    approvalLevel.setLevel(level);

    return dataApprovalLevelStore.save(approvalLevel);
  }