module Devise
  module Models

    # Recoverable takes care of reseting the user password and send reset instructions.
    #
    # ==Options
    #
    # Recoverable adds the following options to devise_for:
    #
    #   * +reset_password_keys+: the keys you want to use when recovering the password for an account
    #
    # == Examples
    #
    #   # resets the user password and save the record, true if valid passwords are given, otherwise false
    #   User.find(1).reset_password!('password123', 'password123')
    #
    #   # only resets the user password, without saving the record
    #   user = User.find(1)
    #   user.reset_password('password123', 'password123')
    #
    #   # creates a new token and send it with instructions about how to reset the password
    #   User.find(1).send_reset_password_instructions
    #
    module Recoverable
      extend ActiveSupport::Concern

      def self.required_fields(klass)
        [:reset_password_sent_at, :reset_password_token]
      end

      # Update password saving the record and clearing token. Returns true if
      # the passwords are valid and the record was saved, false otherwise.
      def reset_password!(new_password, new_password_confirmation)
        self.password = new_password
        self.password_confirmation = new_password_confirmation

        if valid?
          clear_reset_password_token
          after_password_reset
        end

        save
      end

      # Resets reset password token and send reset password instructions by email
      def send_reset_password_instructions
        generate_reset_password_token! if should_generate_reset_token?
        send_devise_notification(:reset_password_instructions)
      end

      # Checks if the reset password token sent is within the limit time.
      # We do this by calculating if the difference between today and the
      # sending date does not exceed the confirm in time configured.
      # Returns true if the resource is not responding to reset_password_sent_at at all.
      # reset_password_within is a model configuration, must always be an integer value.
      #
      # Example:
      #
      #   # reset_password_within = 1.day and reset_password_sent_at = today
      #   reset_password_period_valid?   # returns true
      #
      #   # reset_password_within = 5.days and reset_password_sent_at = 4.days.ago
      #   reset_password_period_valid?   # returns true
      #
      #   # reset_password_within = 5.days and reset_password_sent_at = 5.days.ago
      #   reset_password_period_valid?   # returns false
      #
      #   # reset_password_within = 0.days
      #   reset_password_period_valid?   # will always return false
      #
      def reset_password_period_valid?
        reset_password_sent_at && reset_password_sent_at.utc >= self.class.reset_password_within.ago
      end

      protected

        def should_generate_reset_token?
          reset_password_token.nil? || !reset_password_period_valid?
        end

        # Generates a new random token for reset password
        def generate_reset_password_token
          self.reset_password_token = self.class.reset_password_token
          self.reset_password_sent_at = Time.now.utc
          self.reset_password_token
        end

        # Resets the reset password token with and save the record without
        # validating
        def generate_reset_password_token!
          generate_reset_password_token && save(:validate => false)
        end

        # Removes reset_password token
        def clear_reset_password_token
          self.reset_password_token = nil
          self.reset_password_sent_at = nil
        end

        def after_password_reset
        end

      module ClassMethods
        # Attempt to find a user by its email. If a record is found, send new
        # password instructions to it. If not user is found, returns a new user
        # with an email not found error.
        # Attributes must contain the user email
        def send_reset_password_instructions(attributes={})
          recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
          recoverable.send_reset_password_instructions if recoverable.persisted?
          recoverable
        end

        # Generate a token checking if one does not already exist in the database.
        def reset_password_token
          generate_token(:reset_password_token)
        end

        # Attempt to find a user by its reset_password_token to reset its
        # password. If a user is found and token is still valid, reset its password and automatically
        # try saving the record. If not user is found, returns a new user
        # containing an error in reset_password_token attribute.
        # Attributes must contain reset_password_token, password and confirmation
        def reset_password_by_token(attributes={})
          recoverable = find_or_initialize_with_error_by(:reset_password_token, attributes[:reset_password_token])
          if recoverable.persisted?
            if recoverable.reset_password_period_valid?
              recoverable.reset_password!(attributes[:password], attributes[:password_confirmation])
            else
              recoverable.errors.add(:reset_password_token, :expired)
            end
          end
          recoverable
        end

        Devise::Models.config(self, :reset_password_keys, :reset_password_within)
      end
    end
  end
end
