/**
   * Removes a user from the ageing process (makes them active), ONLY if they do not still have
   * overdue invoices.
   *
   * @param user user to make active
   * @param excludedInvoiceId invoice id to ignore when determining if the user CAN be made active
   * @param executorId executor id
   */
  public void removeUser(UserDTO user, Integer executorId, Integer excludedInvoiceId) {
    Date now = new Date();

    // validate that the user actually needs a status change
    if (user.getStatus().getId() != UserDTOEx.STATUS_ACTIVE) {
      LOG.debug("User " + user.getId() + " is already active, no need to remove from ageing.");
      return;
    }

    // validate that the user does not still have overdue invoices
    try {
      if (new InvoiceBL().isUserWithOverdueInvoices(user.getUserId(), now, excludedInvoiceId)) {
        LOG.debug(
            "User " + user.getId() + " still has overdue invoices, cannot remove from ageing.");
        return;
      }
    } catch (SQLException e) {
      LOG.error("Exception occurred checking for overdue invoices.", e);
      return;
    }

    // make the status change.
    LOG.debug("Removing user " + user.getUserId() + " from ageing (making active).");
    UserStatusDTO status = new UserStatusDAS().find(UserDTOEx.STATUS_ACTIVE);
    setUserStatus(user, status, now, null);
  }
  /**
   * Moves a user one step forward in the ageing process (move from active -> suspended etc.). The
   * user will only be moved if they have spent long enough in their present status.
   *
   * @param steps ageing steps
   * @param user user to age
   * @param today today's date
   * @return the resulting ageing step for the user after ageing
   */
  public AgeingEntityStepDTO ageUser(
      Set<AgeingEntityStepDTO> steps, UserDTO user, Date today, Integer executorId) {
    LOG.debug("Ageing user " + user.getId());

    Integer currentStatusId = user.getStatus().getId();
    UserStatusDTO nextStatus = null;
    AgeingEntityStepDTO ageingStep = null;

    if (currentStatusId.equals(UserDTOEx.STATUS_ACTIVE)) {
      // send welcome message (initial step after active).
      nextStatus = getNextAgeingStep(steps, UserDTOEx.STATUS_ACTIVE);

    } else {
      // user already in the ageing process
      ageingStep = new AgeingEntityStepDAS().findStep(user.getEntity().getId(), currentStatusId);

      if (ageingStep != null) {
        // determine the next ageing step
        if (isAgeingRequired(user, ageingStep, today)) {
          nextStatus = getNextAgeingStep(steps, currentStatusId);
          LOG.debug(
              "User "
                  + user.getId()
                  + " needs to be aged to '"
                  + getStatusDescription(nextStatus)
                  + "'");
        }

      } else {
        // User is in a non-existent ageing status... Either the status was removed or
        // the data is bad. As a workaround, just move to the next status.
        nextStatus = getNextAgeingStep(steps, currentStatusId);
        LOG.warn(
            "User "
                + user.getId()
                + " is in an invalid ageing step. Moving to '"
                + getStatusDescription(nextStatus)
                + "'");
      }
    }

    // set status
    if (nextStatus != null) {
      setUserStatus(user, nextStatus, today, null);

    } else {
      LOG.debug("Next status is null, no further ageing steps are available.");
      eLogger.warning(
          user.getEntity().getId(),
          user.getUserId(),
          user.getUserId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.NO_FURTHER_STEP,
          Constants.TABLE_BASE_USER);
    }

    return ageingStep;
  }
  protected boolean sendAgeingNotification(
      UserDTO user, UserStatusDTO oldStatus, UserStatusDTO newStatus) {
    try {
      MessageDTO message =
          new NotificationBL()
              .getAgeingMessage(
                  user.getEntity().getId(),
                  user.getLanguage().getId(),
                  newStatus.getId(),
                  user.getId());

      INotificationSessionBean notification =
          (INotificationSessionBean) Context.getBean(Context.Name.NOTIFICATION_SESSION);
      notification.notify(user, message);

    } catch (NotificationNotFoundException e) {
      LOG.warn(
          "Failed to send ageing notification. Entity "
              + user.getEntity().getId()
              + " does not have an ageing message configured for status '"
              + getStatusDescription(newStatus)
              + "'.");
      return false;
    }
    return true;
  }
  /**
   * This method sends an email to the given user with the link to reset his password
   *
   * @param user the user
   */
  @Override
  public void resetPassword(UserDTO user) {
    ResetPasswordCodeDAS resetCodeDAS = new ResetPasswordCodeDAS();
    // find previous passwordCode

    ResetPasswordCodeDTO resetCode = resetCodeDAS.findByUser(user);
    if (resetCode == null) {
      resetCode = new ResetPasswordCodeDTO();
      resetCode.setUser(user);
      resetCode.setDateCreated(new Date());
      resetCode.setToken(RandomStringUtils.random(32, true, true));
      resetCodeDAS.save(resetCode);
      resetCodeDAS.flush();
    } else {
      DateTime dateResetCode = new DateTime(resetCode.getDateCreated());
      DateTime today = DateTime.now();
      Duration duration = new Duration(dateResetCode, today);
      Long minutesDifference = duration.getStandardMinutes();
      Long expirationMinutes =
          PreferenceBL.getPreferenceValueAsIntegerOrZero(
                      user.getEntity().getId(),
                      CommonConstants.PREFERENCE_FORGOT_PASSWORD_EXPIRATION)
                  .longValue()
              * 60;
      if (minutesDifference > expirationMinutes) {
        resetCodeDAS.delete(resetCode);
        resetCodeDAS.flush();
        resetCode = new ResetPasswordCodeDTO();
        resetCode.setUser(user);
        resetCode.setDateCreated(new Date());
        resetCode.setToken(RandomStringUtils.random(32, true, true));
        resetCodeDAS.save(resetCode);
      }
    }

    try {
      new UserBL()
          .sendLostPassword(
              user.getCompany().getId(), user.getId(), 1, generateLink(resetCode.getToken()));

    } catch (SessionInternalError e) {
      LOG.error("Exception while sending notification : " + e.getMessage());
      throw new SessionInternalError("forgotPassword.notification.not.found");
    } catch (NotificationNotFoundException e) {
      e.printStackTrace();
    }
  }
  /**
   * Review all users for the given day, and age those that have outstanding invoices over the set
   * number of days for an ageing step.
   *
   * @param steps ageing steps
   * @param today today's date
   * @param executorId executor id
   */
  public void reviewAllUsers(
      Integer entityId, Set<AgeingEntityStepDTO> steps, Date today, Integer executorId) {
    LOG.debug("Reviewing users for entity " + entityId + " ...");

    // go over all the users already in the ageing system
    for (UserDTO user : new UserDAS().findAgeing(entityId)) {
      ageUser(steps, user, today, executorId);
    }

    // go over the active users with payable invoices
    try {
      UserDAS userDas = new UserDAS();
      InvoiceDAS invoiceDas = new InvoiceDAS();

      CachedRowSet users = new UserBL().findActiveWithOpenInvoices(entityId);

      while (users.next()) {
        Integer userId = users.getInt(1);
        UserDTO user = userDas.find(userId);
        int gracePeriod = getGracePeriod(entityId);

        LOG.debug(
            "Reviewing invoices for user "
                + user.getId()
                + " using a grace period of "
                + gracePeriod
                + " days.");

        for (InvoiceDTO invoice : invoiceDas.findProccesableByUser(user)) {
          if (isInvoiceOverdue(invoice, user, gracePeriod, today)) {
            ageUser(steps, user, today, executorId);
            break;
          }
        }
      }

    } catch (SQLException e) {
      LOG.error("Failed to fetch users with payable invoices.", e);

    } catch (NamingException e) {
      LOG.error("Exception fetching users with payable invoices.", e);
    }
  }
  /**
   * Here we will have the initial credentials for a user be created
   *
   * @param user
   */
  @Override
  public void createPassword(UserDTO user) {
    ResetPasswordCodeDAS resetCodeDAS = new ResetPasswordCodeDAS();

    ResetPasswordCodeDTO resetCode = new ResetPasswordCodeDTO();
    resetCode.setUser(user);
    resetCode.setDateCreated(new Date());
    resetCode.setToken(RandomStringUtils.random(32, true, true));
    resetCodeDAS.save(resetCode);

    try {
      new UserBL()
          .sendCredentials(
              user.getCompany().getId(), user.getId(), 1, generateLink(resetCode.getToken()));
    } catch (SessionInternalError e) {
      LOG.error(e.getMessage(), e);
      throw new SessionInternalError("Exception while sending notification : " + e.getMessage());
    } catch (NotificationNotFoundException e) {
      LOG.error(e.getMessage(), e);
      throw new SessionInternalError("createCredentials.notification.not.found");
    }
  }
  protected boolean performAgeingCallback(
      UserDTO user, UserStatusDTO oldStatus, UserStatusDTO newStatus) {
    String url = null;
    try {
      PreferenceBL pref = new PreferenceBL();
      pref.set(user.getEntity().getId(), Constants.PREFERENCE_URL_CALLBACK);
      url = pref.getString();

    } catch (EmptyResultDataAccessException e) {
      /* ignore, no callback preference configured */
    }

    if (url != null && url.length() > 0) {
      try {
        LOG.debug("Performing ageing HTTP callback for URL: " + url);

        // cook the parameters to be sent
        NameValuePair[] data = new NameValuePair[6];
        data[0] = new NameValuePair("cmd", "ageing_update");
        data[1] = new NameValuePair("user_id", String.valueOf(user.getId()));
        data[2] = new NameValuePair("login_name", user.getUserName());
        data[3] = new NameValuePair("from_status", String.valueOf(oldStatus.getId()));
        data[4] = new NameValuePair("to_status", String.valueOf(newStatus.getId()));
        data[5] = new NameValuePair("can_login", String.valueOf(newStatus.getCanLogin()));

        // make the call
        HttpClient client = new HttpClient();
        client.setConnectionTimeout(30000);
        PostMethod post = new PostMethod(url);
        post.setRequestBody(data);
        client.executeMethod(post);

      } catch (Exception e) {
        LOG.error("Exception occurred posting ageing HTTP callback for URL: " + url, e);
        return false;
      }
    }
    return true;
  }
  /**
   * Sets the user status to the given "aged" status. If the user status is already set to the aged
   * status no changes will be made. This method also performs an HTTP callback and sends a
   * notification message when a status change is made.
   *
   * <p>If the user becomes suspended and can no longer log-in to the system, all of their active
   * orders will be automatically suspended.
   *
   * <p>If the user WAS suspended and becomes active (and can now log-in to the system), any
   * automatically suspended orders will be re-activated.
   *
   * @param user user
   * @param status status to set
   * @param today today's date
   * @param executorId executor id
   */
  public void setUserStatus(UserDTO user, UserStatusDTO status, Date today, Integer executorId) {
    // only set status if the new "aged" status is different from the users current status
    if (status.getId() == user.getStatus().getId()) {
      return;
    }

    LOG.debug("Setting user " + user.getId() + " status to '" + getStatusDescription(status) + "'");

    if (executorId != null) {
      // this came from the gui
      eLogger.audit(
          executorId,
          user.getId(),
          Constants.TABLE_BASE_USER,
          user.getId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.STATUS_CHANGE,
          user.getStatus().getId(),
          null,
          null);
    } else {
      // this is from a process, no executor involved
      eLogger.auditBySystem(
          user.getCompany().getId(),
          user.getId(),
          Constants.TABLE_BASE_USER,
          user.getId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.STATUS_CHANGE,
          user.getStatus().getId(),
          null,
          null);
    }

    // make the change
    boolean couldLogin = user.getStatus().getCanLogin() == 1;
    UserStatusDTO oldStatus = user.getStatus();

    user.setUserStatus(status);
    user.setLastStatusChange(today);

    // status changed to deleted, remove user
    if (status.getId() == UserDTOEx.STATUS_DELETED) {
      LOG.debug("Deleting user " + user.getId());
      new UserBL(user.getId()).delete(executorId);
      return;
    }

    // status changed from active to suspended
    // suspend customer orders
    if (couldLogin && status.getCanLogin() == 0) {
      LOG.debug("User " + user.getId() + " cannot log-in to the system. Suspending active orders.");

      OrderBL orderBL = new OrderBL();
      ScrollableResults orders =
          new OrderDAS().findByUser_Status(user.getId(), Constants.ORDER_STATUS_ACTIVE);

      while (orders.next()) {
        OrderDTO order = (OrderDTO) orders.get()[0];
        orderBL.set(order);
        orderBL.setStatus(executorId, Constants.ORDER_STATUS_SUSPENDED_AGEING);
      }

      orders.close();
    }

    // status changed from suspended to active
    // re-active suspended customer orders
    if (!couldLogin && status.getCanLogin() == 1) {
      LOG.debug(
          "User "
              + user.getId()
              + " can now log-in to the system. Activating previously suspended orders.");

      OrderBL orderBL = new OrderBL();
      ScrollableResults orders =
          new OrderDAS().findByUser_Status(user.getId(), Constants.ORDER_STATUS_SUSPENDED_AGEING);

      while (orders.next()) {
        OrderDTO order = (OrderDTO) orders.get()[0];
        orderBL.set(order);
        orderBL.setStatus(executorId, Constants.ORDER_STATUS_ACTIVE);
      }

      orders.close();
    }

    // perform callbacks and notifications
    performAgeingCallback(user, oldStatus, status);
    sendAgeingNotification(user, oldStatus, status);

    // emit NewUserStatusEvent
    NewUserStatusEvent event =
        new NewUserStatusEvent(
            user.getCompany().getId(), user.getId(), oldStatus.getId(), status.getId());
    EventManager.process(event);
  }