# EffectiveDeviseUsr
#
# Mark your user model with devise_for then effective_devise_user

module EffectiveDeviseUser
  extend ActiveSupport::Concern

  module Base
    def effective_devise_user
      include ::EffectiveDeviseUser
    end
  end

  included do
    effective_resource do
      encrypted_password      :string
      reset_password_token    :string
      reset_password_sent_at  :datetime
      remember_created_at     :datetime
      sign_in_count           :integer
      current_sign_in_at      :datetime
      last_sign_in_at         :datetime
      current_sign_in_ip      :inet
      last_sign_in_ip         :inet

      # Devise invitable attributes
      invitation_token        :string
      invitation_created_at   :datetime
      invitation_sent_at      :datetime
      invitation_accepted_at  :datetime
      invitation_limit        :integer
      invited_by_type         :string
      invited_by_id           :integer
      invitations_count       :integer

      # Omniauth
      uid                     :string
      provider                :string

      access_token            :string
      refresh_token           :string
      token_expires_at        :datetime

      name                    :string
      avatar_url              :string
    end

    # Devise invitable ignores model validations, so we manually check for duplicate email addresses.
    before_save(if: -> { new_record? && invitation_sent_at.present? }) do
      if email.blank?
        self.errors.add(:email, "can't be blank")
        raise("email can't be blank")
      end

      if self.class.where(email: email.downcase.strip).exists?
        self.errors.add(:email, 'has already been taken')
        raise("email has already been taken")
      end
    end

    # Clear the provider if an oauth signed in user resets password
    before_save(if: -> { persisted? && encrypted_password_changed? }) do
      assign_attributes(provider: nil, access_token: nil, refresh_token: nil, token_expires_at: nil)
    end
  end

  module ClassMethods
    def effective_devise_user?; true; end

    def permitted_sign_up_params # Should contain all fields as per views/users/_sign_up_fields
      raise('please define a self.permitted_sign_up_params')
      [:email, :password, :password_confirmation, :first_name, :last_name, :name, :login]
    end

    def from_omniauth(auth, params)
      invitation_token = (params.presence || {})['invitation_token']

      email = (auth.info.email.presence || "#{auth.uid}@#{auth.provider}.none").downcase
      image = auth.info.image
      name = auth.info.name || auth.dig(:extra, :raw_info, :login)

      user = if invitation_token
        find_by_invitation_token(invitation_token, false) || raise(ActiveRecord::RecordNotFound)
      else
        where(uid: auth.uid).or(where(email: email)).first || self.new()
      end

      user.assign_attributes(
        uid: auth.uid,
        provider: auth.provider,
        email: email,
        avatar_url: image,
        name: name,
        first_name: (auth.info.first_name.presence || name.split(' ').first.presence || 'First'),
        last_name: (auth.info.last_name.presence || name.split(' ').last.presence || 'Last')
      )

      if auth.respond_to?(:credentials)
        user.assign_attributes(
          access_token: auth.credentials.token,
          refresh_token: auth.credentials.refresh_token,
          token_expires_at: Time.zone.at(auth.credentials.expires_at), # We are given integer datetime e.g. '1549394077'
        )
      end

      # Make a password
      user.password = Devise.friendly_token[0, 20] if user.encrypted_password.blank?

      # Devise Invitable
      invitation_token ? user.accept_invitation! : user.save!

      # Devise Confirmable
      user.confirm if user.respond_to?(:confirm)

      user
    end

    # https://github.com/heartcombo/devise/blob/master/lib/devise/models/recoverable.rb#L134
    def send_reset_password_instructions(attributes = {})
      recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
      return recoverable unless recoverable.persisted?

      # Add custom errors and require a confirmation if previous sign in was provider
      if recoverable.provider.present? && attributes[:confirm_new_password].blank?
        recoverable.errors.add(:email, "previous sign in was with #{recoverable.provider}")
        recoverable.errors.add(:confirm_new_password, 'please confirm to proceed')
      end

      recoverable.send_reset_password_instructions if recoverable.errors.blank?
      recoverable
    end

  end

  # EffectiveDeviseUser Instance Methods

  def reinvite!
    invite!
  end

  def active_for_authentication?
    super && (respond_to?(:archived?) ? !archived? : true)
  end

  # Allow users to sign in even if they have a pending invitation
  def block_from_invitation?
    false
  end

  def inactive_message
    (respond_to?(:archived?) && archived?) ? :archived : super
  end

  # Any password will work in development or mode
  def valid_password?(password)
    Rails.env.development? || Rails.env.staging? || super
  end

  # Send devise & devise_invitable emails via active job
  def send_devise_notification(notification, *args)
    raise('expected args Hash') unless args.respond_to?(:last) && args.last.kind_of?(Hash)

    if defined?(Tenant)
      tenant = Tenant.current || raise('expected a current tenant')
      args.last[:tenant] ||= tenant
    end

    devise_mailer.send(notification, self, *args).deliver_now
  end

end