module Killbill
  module Plugin
    module ActiveMerchant
      module ActiveRecord
        require 'active_record'
        require 'active_merchant'
        require 'time'
        require 'killbill/helpers/active_merchant/active_record/models/helpers'

        class PaymentMethod < ::ActiveRecord::Base

          extend ::Killbill::Plugin::ActiveMerchant::Helpers

          self.abstract_class = true
          # See Response#from_response
          self.record_timestamps = false

          @@quotes_cache = build_quotes_cache

          def self.from_response(kb_account_id, kb_payment_method_id, kb_tenant_id, cc_or_token, response, options, extra_params = {}, model = PaymentMethod)
            # See Response#from_response
            current_time = Time.now.utc
            model.new({
                          :kb_account_id        => kb_account_id,
                          :kb_payment_method_id => kb_payment_method_id,
                          :kb_tenant_id         => kb_tenant_id,
                          :token                => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? response.authorization : (cc_or_token || response.authorization),
                          :cc_first_name        => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? cc_or_token.first_name : extra_params[:cc_first_name],
                          :cc_last_name         => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? cc_or_token.last_name : extra_params[:cc_last_name],
                          :cc_type              => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? cc_or_token.brand : extra_params[:cc_type],
                          :cc_exp_month         => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? cc_or_token.month : extra_params[:cc_exp_month],
                          :cc_exp_year          => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? cc_or_token.year : extra_params[:cc_exp_year],
                          :cc_last_4            => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? cc_or_token.last_digits : extra_params[:cc_last_4],
                          :cc_number            => cc_or_token.kind_of?(::ActiveMerchant::Billing::CreditCard) ? cc_or_token.number : nil,
                          :address1             => (options[:billing_address] || {})[:address1],
                          :address2             => (options[:billing_address] || {})[:address2],
                          :city                 => (options[:billing_address] || {})[:city],
                          :state                => (options[:billing_address] || {})[:state],
                          :zip                  => (options[:billing_address] || {})[:zip],
                          :country              => (options[:billing_address] || {})[:country],
                          :created_at           => current_time,
                          :updated_at           => current_time
                      }.merge!(extra_params.compact)) # Don't override with nil values
          end

          def self.from_kb_account_id(kb_account_id, kb_tenant_id)
            if kb_tenant_id.nil?
              where("kb_account_id = #{@@quotes_cache[kb_account_id]} AND kb_tenant_id is NULL AND is_deleted = #{@@quotes_cache[false]}")
            else
              where("kb_account_id = #{@@quotes_cache[kb_account_id]} AND kb_tenant_id = #{@@quotes_cache[kb_tenant_id]} AND is_deleted = #{@@quotes_cache[false]}")
            end
          end

          def self.from_kb_account_id_and_token(token, kb_account_id, kb_tenant_id)
            from_kb_account_id(kb_account_id, kb_tenant_id).where("token = #{@@quotes_cache[token]}")
          end

          def self.from_kb_payment_method_id(kb_payment_method_id, kb_tenant_id)
            if kb_tenant_id.nil?
              payment_methods = where("kb_payment_method_id = #{@@quotes_cache[kb_payment_method_id]} AND kb_tenant_id is NULL AND is_deleted = #{@@quotes_cache[false]}")
            else
              payment_methods = where("kb_payment_method_id = #{@@quotes_cache[kb_payment_method_id]} AND kb_tenant_id = #{@@quotes_cache[kb_tenant_id]} AND is_deleted = #{@@quotes_cache[false]}")
            end
            raise "No payment method found for payment method #{kb_payment_method_id} and tenant #{kb_tenant_id}" if payment_methods.empty?
            raise "Kill Bill payment method #{kb_payment_method_id} mapping to multiple active plugin payment methods" if payment_methods.size > 1
            payment_methods[0]
          end

          def self.mark_as_deleted!(kb_payment_method_id, kb_tenant_id)
            payment_method = from_kb_payment_method_id(kb_payment_method_id, kb_tenant_id)
            payment_method.is_deleted = true
            payment_method.save!(shared_activerecord_options)
          end

          # Override in your plugin if needed
          def self.search_where_clause(t, search_key)
            where_clause =     t[:kb_account_id].eq(search_key)
                           .or(t[:kb_payment_method_id].eq(search_key))
                           .or(t[:token].eq(search_key))
                           .or(t[:cc_type].eq(search_key))
                           .or(t[:state].eq(search_key))
                           .or(t[:zip].eq(search_key))
                           .or(t[:cc_first_name].matches("%#{search_key}%"))
                           .or(t[:cc_last_name].matches("%#{search_key}%"))
                           .or(t[:address1].matches("%#{search_key}%"))
                           .or(t[:address2].matches("%#{search_key}%"))
                           .or(t[:city].matches("%#{search_key}%"))
                           .or(t[:country].matches("%#{search_key}%"))

            # Coming from Kill Bill, search_key will always be a String. Check to see if it represents a numeric for numeric-only fields
            if search_key.is_a?(Numeric) or search_key.to_s =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/
              where_clause = where_clause.or(t[:cc_exp_month].eq(search_key))
                                         .or(t[:cc_exp_year].eq(search_key))
                                         .or(t[:cc_last_4].eq(search_key))
            end

            where_clause
          end

          # VisibleForTesting
          def self.search_query(search_key, kb_tenant_id, offset = nil, limit = nil)
            t = self.arel_table

            if kb_tenant_id.nil?
              query = t.where(search_where_clause(t, search_key))
              .order(t[:id])
            else
              query = t.where(search_where_clause(t, search_key).and(t[:kb_tenant_id].eq(kb_tenant_id)))
              .order(t[:id])
            end

            if offset.blank? and limit.blank?
              # true is for count distinct
              query.project(t[:id].count(true))
            else
              query.skip(offset) unless offset.blank?
              query.take(limit) unless limit.blank?
              query.project(t[Arel.star])
              # Not chainable
              query.distinct
            end
            query
          end

          def self.search(search_key, kb_tenant_id, offset = 0, limit = 100)
            pagination = Killbill::Plugin::Model::Pagination.new
            pagination.current_offset = offset
            pagination.total_nb_records = self.count_by_sql(self.search_query(search_key, kb_tenant_id))
            pagination.max_nb_records = self.count
            pagination.next_offset = (!pagination.total_nb_records.nil? && offset + limit >= pagination.total_nb_records) ? nil : offset + limit
            # Reduce the limit if the specified value is larger than the number of records
            actual_limit = [pagination.max_nb_records, limit].min
            pagination.iterator = StreamyResultSet.new(actual_limit) do |offset,limit|
              self.find_by_sql(self.search_query(search_key, kb_tenant_id, offset, limit))
                  .map(&:to_payment_method_plugin)
            end
            pagination
          end

          # Override in your plugin if needed
          def external_payment_method_id
            token
          end

          # Override in your plugin if needed
          def is_default
            false
          end

          def to_payment_method_plugin
            properties = []
            properties << create_plugin_property('token', external_payment_method_id)
            properties << create_plugin_property('ccFirstName', cc_first_name)
            properties << create_plugin_property('ccLastName', cc_last_name)
            properties << create_plugin_property('ccName', cc_name)
            properties << create_plugin_property('ccType', cc_type)
            properties << create_plugin_property('ccExpirationMonth', cc_exp_month)
            properties << create_plugin_property('ccExpirationYear', cc_exp_year)
            properties << create_plugin_property('ccLast4', cc_last_4)
            properties << create_plugin_property('ccNumber', cc_number)
            properties << create_plugin_property('address1', address1)
            properties << create_plugin_property('address2', address2)
            properties << create_plugin_property('city', city)
            properties << create_plugin_property('state', state)
            properties << create_plugin_property('zip', zip)
            properties << create_plugin_property('country', country)

            pm_plugin = Killbill::Plugin::Model::PaymentMethodPlugin.new
            pm_plugin.kb_payment_method_id = kb_payment_method_id
            pm_plugin.external_payment_method_id = external_payment_method_id
            pm_plugin.is_default_payment_method = is_default
            pm_plugin.properties = properties

            pm_plugin
          end

          def to_payment_method_info_plugin
            pm_info_plugin = Killbill::Plugin::Model::PaymentMethodInfoPlugin.new
            pm_info_plugin.account_id = kb_account_id
            pm_info_plugin.payment_method_id = kb_payment_method_id
            pm_info_plugin.is_default = is_default
            pm_info_plugin.external_payment_method_id = external_payment_method_id
            pm_info_plugin
          end

          def cc_name
            if cc_first_name and cc_last_name
              "#{cc_first_name} #{cc_last_name}"
            elsif cc_first_name
              cc_first_name
            elsif cc_last_name
              cc_last_name
            else
              nil
            end
          end

          private

          def create_plugin_property(key, value)
            prop = Killbill::Plugin::Model::PluginProperty.new
            prop.key = key
            prop.value = value
            prop
          end
        end
      end
    end
  end
end