module Killbill::PaypalExpress
  class PaypalExpressResponse < ActiveRecord::Base
    has_one :paypal_express_transaction
    attr_accessible :api_call,
                    :kb_payment_id,
                    :message,
                    # transaction_id, authorization_id (reauthorization) or refund_transaction_id
                    :authorization,
                    :fraud_review,
                    :test,
                    :token,
                    :payer_id,
                    :billing_agreement_id,
                    :payer_name,
                    :payer_email,
                    :payer_country,
                    :contact_phone,
                    :ship_to_address_name,
                    :ship_to_address_company,
                    :ship_to_address_address1,
                    :ship_to_address_address2,
                    :ship_to_address_city,
                    :ship_to_address_state,
                    :ship_to_address_country,
                    :ship_to_address_zip,
                    :ship_to_address_phone,
                    :receiver_info_business,
                    :receiver_info_receiver,
                    :receiver_info_receiverid,
                    :payment_info_transactionid,
                    :payment_info_parenttransactionid,
                    :payment_info_receiptid,
                    :payment_info_transactiontype,
                    :payment_info_paymenttype,
                    :payment_info_paymentdate,
                    :payment_info_grossamount,
                    :payment_info_feeamount,
                    :payment_info_taxamount,
                    :payment_info_exchangerate,
                    :payment_info_paymentstatus,
                    :payment_info_pendingreason,
                    :payment_info_reasoncode,
                    :payment_info_protectioneligibility,
                    :payment_info_protectioneligibilitytype,
                    :payment_info_shipamount,
                    :payment_info_shiphandleamount,
                    :payment_info_shipdiscount,
                    :payment_info_insuranceamount,
                    :payment_info_subject,
                    :avs_result_code,
                    :avs_result_message,
                    :avs_result_street_match,
                    :avs_result_postal_match,
                    :cvv_result_code,
                    :cvv_result_message,
                    :success

    def self.from_response(api_call, kb_payment_id, response)
      PaypalExpressResponse.new({
                                    :api_call => api_call,
                                    :kb_payment_id => kb_payment_id,
                                    :message => response.message,
                                    :authorization => response.authorization,
                                    :fraud_review => response.fraud_review?,
                                    :test => response.test?,
                                    :token => response.token,
                                    :payer_id => response.payer_id,
                                    :billing_agreement_id => response.params['billing_agreement_id'],
                                    :payer_name => response.name,
                                    :payer_email => response.email,
                                    :payer_country => response.payer_country,
                                    :contact_phone => response.contact_phone,
                                    :ship_to_address_name => response.address['name'],
                                    :ship_to_address_company => response.address['company'],
                                    :ship_to_address_address1 => response.address['address1'],
                                    :ship_to_address_address2 => response.address['address2'],
                                    :ship_to_address_city => response.address['city'],
                                    :ship_to_address_state => response.address['state'],
                                    :ship_to_address_country => response.address['country'],
                                    :ship_to_address_zip => response.address['zip'],
                                    :ship_to_address_phone => response.address['phone'],
                                    :receiver_info_business => receiver_info(response)['Business'],
                                    :receiver_info_receiver => receiver_info(response)['Receiver'],
                                    :receiver_info_receiverid => receiver_info(response)['ReceiverID'],
                                    :payment_info_transactionid => payment_info(response)['TransactionID'],
                                    :payment_info_parenttransactionid => payment_info(response)['ParentTransactionID'],
                                    :payment_info_receiptid => payment_info(response)['ReceiptID'],
                                    :payment_info_transactiontype => payment_info(response)['TransactionType'],
                                    :payment_info_paymenttype => payment_info(response)['PaymentType'],
                                    :payment_info_paymentdate => payment_info(response)['PaymentDate'],
                                    :payment_info_grossamount => payment_info(response)['GrossAmount'],
                                    :payment_info_feeamount => payment_info(response)['FeeAmount'],
                                    :payment_info_taxamount => payment_info(response)['TaxAmount'],
                                    :payment_info_exchangerate => payment_info(response)['ExchangeRate'],
                                    :payment_info_paymentstatus => payment_info(response)['PaymentStatus'],
                                    :payment_info_pendingreason => payment_info(response)['PendingReason'],
                                    :payment_info_reasoncode => payment_info(response)['ReasonCode'],
                                    :payment_info_protectioneligibility => payment_info(response)['ProtectionEligibility'],
                                    :payment_info_protectioneligibilitytype => payment_info(response)['ProtectionEligibilityType'],
                                    :payment_info_shipamount => payment_info(response)['ShipAmount'],
                                    :payment_info_shiphandleamount => payment_info(response)['ShipHandleAmount'],
                                    :payment_info_shipdiscount => payment_info(response)['ShipDiscount'],
                                    :payment_info_insuranceamount => payment_info(response)['InsuranceAmount'],
                                    :payment_info_subject => payment_info(response)['Subject'],
                                    :avs_result_code => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.code : response.avs_result['code'],
                                    :avs_result_message => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.message : response.avs_result['message'],
                                    :avs_result_street_match => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.street_match : response.avs_result['street_match'],
                                    :avs_result_postal_match => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.postal_match : response.avs_result['postal_match'],
                                    :cvv_result_code => response.cvv_result.kind_of?(ActiveMerchant::Billing::CVVResult) ? response.cvv_result.code : response.cvv_result['code'],
                                    :cvv_result_message => response.cvv_result.kind_of?(ActiveMerchant::Billing::CVVResult) ? response.cvv_result.message : response.cvv_result['message'],
                                    :success => response.success?
                                })
    end

    def to_express_checkout_url
      url = Killbill::PaypalExpress.test ? Killbill::PaypalExpress.paypal_sandbox_url : Killbill::PaypalExpress.paypal_production_url
      "#{url}?cmd=_express-checkout&token=#{token}"
    end

    def to_payment_response
      to_killbill_response :payment
    end

    def to_refund_response
      to_killbill_response :refund
    end

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

      # Exact matches only
      where_clause =     t[:authorization].eq(search_key)
                     .or(t[:billing_agreement_id].eq(search_key))
                     .or(t[:payment_info_transactionid].eq(search_key))

      # Only search successful payments and refunds
      where_clause = where_clause.and(t[:api_call].eq(api_call))
                                 .and(t[:success].eq(true))

      query = t.where(where_clause)
               .order(t[:id])

      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, offset = 0, limit = 100, type = :payment)
      api_call = type == :payment ? 'charge' : 'refund'
      pagination = Killbill::Plugin::Model::Pagination.new
      pagination.current_offset = offset
      pagination.total_nb_records = self.count_by_sql(self.search_query(api_call, search_key))
      pagination.max_nb_records = self.where(:api_call => api_call, :success => true).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(api_call, search_key, offset, limit))
            .map { |x| type == :payment ? x.to_payment_response : x.to_refund_response }
      end
      pagination
    end

    private

    def to_killbill_response(type)
      if paypal_express_transaction.nil?
        # payment_info_grossamount may have a value but we cannot convert it to cents since we don't have the currency
        amount_in_cents = nil
        currency = nil
        created_date = created_at
        first_payment_reference_id = nil
        second_payment_reference_id = nil
      else
        amount_in_cents = paypal_express_transaction.amount_in_cents
        currency = paypal_express_transaction.currency
        created_date = paypal_express_transaction.created_at
        first_reference_id = paypal_express_transaction.paypal_express_txn_id
        second_reference_id = nil
      end

      effective_date = created_date
      gateway_error = message
      gateway_error_code = nil

      if type == :payment
        p_info_plugin = Killbill::Plugin::Model::PaymentInfoPlugin.new
        p_info_plugin.kb_payment_id = kb_payment_id
        p_info_plugin.amount = Money.new(amount_in_cents, currency).to_d if currency
        p_info_plugin.currency = currency
        p_info_plugin.created_date = created_date
        p_info_plugin.effective_date = effective_date
        p_info_plugin.status = (success ? :PROCESSED : :ERROR)
        p_info_plugin.gateway_error = gateway_error
        p_info_plugin.gateway_error_code = gateway_error_code
        p_info_plugin.first_payment_reference_id = first_reference_id
        p_info_plugin.second_payment_reference_id = second_reference_id
        p_info_plugin
      else
        r_info_plugin = Killbill::Plugin::Model::RefundInfoPlugin.new
        r_info_plugin.kb_payment_id = kb_payment_id
        r_info_plugin.amount = Money.new(amount_in_cents, currency).to_d if currency
        r_info_plugin.currency = currency
        r_info_plugin.created_date = created_date
        r_info_plugin.effective_date = effective_date
        r_info_plugin.status = (success ? :PROCESSED : :ERROR)
        r_info_plugin.gateway_error = gateway_error
        r_info_plugin.gateway_error_code = gateway_error_code
        r_info_plugin.first_refund_reference_id = first_reference_id
        r_info_plugin.second_refund_reference_id = second_reference_id
        r_info_plugin
      end
    end

    # Paypal has various response formats depending on the API call and the ActiveMerchant Paypal plugin doesn't try to
    # unify them, hence the gymnastic here

    def self.receiver_info(response)
      response.params['ReceiverInfo'] || (response.params['PaymentTransactionDetails'] || {})['ReceiverInfo'] || {}
    end

    def self.payment_info(response)
      response.params['PaymentInfo'] || (response.params['PaymentTransactionDetails'] || {})['PaymentInfo'] || {}
    end
  end
end