/**
   * Allows users to selectively exclude files from the transfer request.
   *
   * @param requestId The id of the transfer request being modified
   * @param ids The ids of the files that should not be included in the request
   * @param user The user who is completing the transfer request
   * @throws ForbiddenException If the user does not own this request
   * @throws NotFoundException If the request cannot be found
   * @throws BadRequestException If any of the given file IDs are not part of the request. The valid
   *     files that were found, however, will still be removed even if this exception is thrown.
   */
  @ApiMethod(name = "user.request.remove", httpMethod = HttpMethod.POST)
  public void removeFilesFromTransfer(
      @Named("request") long requestId, @Named("ids") Set<String> ids, User user)
      throws ForbiddenException, NotFoundException, BadRequestException {
    TransferRequest request = getRequest(requestId, user);

    ApiUtil.splitStringArguments(ids);
    log.info("Request started with " + request.getFiles().size() + " files.");
    log.info("Processing id list of size " + ids.size());

    List<TransferableFile> toRemove = new ArrayList<>();
    for (TransferableFile file : request.getFiles()) {
      if (ids.contains(file.getFileId())) {
        toRemove.add(file);
        ids.remove(file.getFileId());

        if (ids.size() == 0) {
          break;
        }
      }
    }

    request.getFiles().removeAll(toRemove);
    log.info("Request now contains " + request.getFiles().size() + " files.");
    OfyService.ofy().save().entity(request).now();
    OfyService.ofy().clear();
    if (!ids.isEmpty()) {
      throw new BadRequestException(String.format(Constants.Error.REMOVE_UNKNOWN_FILE_IDS, ids));
    }
  }
  /**
   * Deletes a transfer request
   *
   * @param requestId The ids of the transfer request to remove
   * @param user The user who is completing the transfer request
   * @throws ForbiddenException If the user does not own this request
   * @throws NotFoundException If the request cannot be found
   * @throws BadRequestException If any of the given file IDs are not part of the request. The valid
   *     files that were found, however, will still be removed even if this exception is thrown.
   */
  @ApiMethod(name = "user.request.delete", httpMethod = HttpMethod.POST)
  public void delete(@Named("request") long requestId, User user)
      throws ForbiddenException, NotFoundException, BadRequestException {
    TransferRequest request = getRequest(requestId, user);

    OfyService.ofy().delete().entity(request).now();
    OfyService.ofy().clear();
  }
  /**
   * Returns details about the given transfer request, including a list of included files.
   *
   * @param requestId The ID of the request
   * @param user The user who is completing the transfer request
   * @return The transfer request
   * @throws ForbiddenException If the user does not own this request
   * @throws NotFoundException If the request cannot be found
   */
  @ApiMethod(name = "user.request.get", httpMethod = HttpMethod.GET)
  public TransferRequest getRequest(@Named("request") long requestId, User user)
      throws ForbiddenException, NotFoundException {
    if (user == null) {
      throw new ForbiddenException(Constants.Error.AUTH_REQUIRED);
    }
    TransferRequest request =
        OfyService.ofy().load().type(TransferRequest.class).id(requestId).now();
    OfyService.ofy().clear();

    if (request == null) {
      throw new NotFoundException(
          String.format(Constants.Error.TRANSFER_REQUEST_NOT_FOUND, requestId));
    }

    if (request.getTargetUser().getEmail().toLowerCase().equals(user.getEmail().toLowerCase())) {
      log.info(request.getTargetUser() + " == " + user.getUserId());
      return request;
    } else {
      log.info(request.getTargetUser().getEmail() + " != " + user.getEmail());
      throw new ForbiddenException(Constants.Error.TRANSFER_REQUEST_INCORRECT_USER);
    }
  }
  /**
   * Incrementally completes a transfer request.
   *
   * <p>Google drive has an API rate limit of 10 requests per second, and a maximum request duration
   * of 60 seconds. At the theoretical maximum, 600 requests could be completed in this time span,
   * however in practice this value is likely much lower. As such, an incremental approach is
   * required to allow for such large requests. The returned TransferRequest will contain the
   * updated state after all requests that took place this session have completed.
   *
   * <p>The limit parameter can be used to help implement a progress bar, as the total number of
   * requests is known and the number of requests per call is configurable. To get a higher
   * precision, use a smaller limit.
   *
   * @param requestId The id of the transfer request
   * @param limit The maximum number of files to transfer during this request. This is required to
   *     ensure we do not exceed the rate limit imposed by the Drive API. If not specified, files
   *     will be transferred until the request is forced to end.
   * @param user The user who is completing the transfer request
   * @return the current state of the transfer request, with successfully transfered files removed.
   * @throws BadRequestException If the requested limit is greater than 600
   * @throws ForbiddenException If the user is not authorized to complete this transfer request
   * @throws NotFoundException If the transfer request cannot be found
   */
  @ApiMethod(name = "user.request.accept", httpMethod = HttpMethod.POST)
  public TransferRequest acceptRequest(
      @Named("request") long requestId, @Named("limit") @Nullable Integer limit, User user)
      throws BadRequestException, ForbiddenException, NotFoundException,
          InternalServerErrorException {

    TransferRequest request = getRequest(requestId, user);
    if (limit == null) {
      limit = 600;
    } else if (limit > 600 || limit <= 0) {
      throw new BadRequestException(
          String.format(Constants.Error.INVALID_TRANSFER_LIMIT, limit, 0, 600));
    }

    final Drive service = Utils.createDriveFromUser(user);
    final List<TransferableFile> success = new ArrayList<>();
    final Permission owner = new Permission();
    owner.setRole("owner");
    owner.setType("user");
    owner.setValue(request.getRequestingUser().getEmail());

    BatchRequest updateBatch = service.batch();
    final BatchRequest insertBatch = service.batch();

    for (final TransferableFile file : request.getFiles()) {

      try {
        service
            .permissions()
            .update(file.getFileId(), request.getRequestingUser().getPermission(), owner)
            .setTransferOwnership(true)
            .queue(
                updateBatch,
                new JsonBatchCallback<Permission>() {
                  @Override
                  public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders)
                      throws IOException {
                    log.log(
                        Level.INFO,
                        "Could not update ownership of file "
                            + file.getFileId()
                            + " ("
                            + file.getFileName()
                            + ").  Attempting to insert permission",
                        e);
                    service
                        .permissions()
                        .insert(file.getFileId(), owner)
                        .queue(
                            insertBatch,
                            new JsonBatchCallback<Permission>() {
                              @Override
                              public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders)
                                  throws IOException {
                                log.log(
                                    Level.SEVERE,
                                    "Could not transfer ownership of file "
                                        + file.getFileId()
                                        + " ("
                                        + file.getFileName()
                                        + ")",
                                    e);
                                log.log(Level.WARNING, e.getMessage() + ": " + e.getErrors());
                              }

                              @Override
                              public void onSuccess(
                                  Permission permission, HttpHeaders responseHeaders)
                                  throws IOException {
                                if (success.size() <= 5) {
                                  if (success.size() == 5) {
                                    log.info("...");
                                  } else {
                                    log.info(
                                        "Transferred file "
                                            + file.getFileId()
                                            + " ("
                                            + file.getFileName()
                                            + ")");
                                  }
                                }
                                success.add(file);
                              }
                            });
                  }

                  @Override
                  public void onSuccess(Permission permission, HttpHeaders responseHeaders)
                      throws IOException {
                    success.add(file);
                  }
                });
        success.add(file);
      } catch (IOException e) {
        throw new InternalServerErrorException(Constants.Error.FAILED_DRIVE_REQUEST, e);
      }

      if (updateBatch.size() >= limit) {
        break;
      }
    }

    try {
      updateBatch.execute();
    } catch (IOException e) {
      log.log(Level.SEVERE, "Error executing batch request", e);
    }

    if (insertBatch.size() > 0) {
      log.log(
          Level.INFO,
          "There were failures.  Attempting to insert " + insertBatch.size() + " new permissions");
      try {
        insertBatch.execute();
      } catch (IOException e) {
        log.log(Level.SEVERE, "Error executing batch request", e);
      }
    }

    request.getFiles().removeAll(success);

    // If there are no files left, delete the request, otherwise save it for future completion.
    if (request.getFiles().isEmpty()) {
      OfyService.ofy().delete().entity(request).now();
    } else {
      OfyService.ofy().save().entity(request).now();
    }
    OfyService.ofy().clear();
    return request;
  }