require 'rexml/document'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class EfsnetGateway < Gateway
      self.supported_countries = ['US']
      self.supported_cardtypes = %i[visa master american_express discover]
      self.homepage_url = 'http://www.concordefsnet.com/'
      self.display_name = 'Efsnet'

      self.test_url = 'https://testefsnet.concordebiz.com/efsnet.dll'
      self.live_url = 'https://efsnet.concordebiz.com/efsnet.dll'

      # login is your Store ID
      # password is your Store Key
      def initialize(options = {})
        requires!(options, :login, :password)
        super
      end

      def authorize(money, creditcard, options = {})
        request = build_credit_card_request(money, creditcard, options)
        commit(:credit_card_authorize, request)
      end

      def purchase(money, creditcard, options = {})
        request = build_credit_card_request(money, creditcard, options)
        commit(:credit_card_charge, request)
      end

      def capture(money, identification, options = {})
        request = build_refund_or_settle_request(money, identification, options)
        commit(:credit_card_settle, request)
      end

      def credit(money, identification_or_credit_card, options = {})
        if identification_or_credit_card.is_a?(String)
          ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
          # Perform authorization reversal
          refund(money, identification_or_credit_card, options)
        else
          # Perform credit
          request = build_credit_card_request(money, identification_or_credit_card, options)
          commit(:credit_card_credit, request)
        end
      end

      def refund(money, reference, options = {})
        # Perform authorization reversal
        request = build_refund_or_settle_request(money, reference, options)
        commit(:credit_card_refund, request)
      end

      def void(identification, options = {})
        requires!(options, :order_id)
        original_transaction_id, = identification.split(';')
        commit(:void_transaction, { reference_number: format_reference_number(options[:order_id]), transaction_id: original_transaction_id })
      end

      def voice_authorize(money, authorization_code, creditcard, options = {})
        options[:authorization_number] = authorization_code
        request = build_credit_card_request(money, creditcard, options)
        commit(:credit_card_voice_authorize, request)
      end

      def force(money, authorization_code, creditcard, options = {})
        options[:authorization_number] = authorization_code
        request = build_credit_card_request(money, creditcard, options)
        commit(:credit_card_capture, request)
      end

      def system_check
        commit(:system_check, {})
      end

      private

      def build_refund_or_settle_request(money, identification, options = {})
        original_transaction_id, original_transaction_amount = identification.split(';')

        requires!(options, :order_id)

        {
          reference_number: format_reference_number(options[:order_id]),
          transaction_amount: amount(money),
          original_transaction_amount: original_transaction_amount,
          original_transaction_id: original_transaction_id,
          client_ip_address: options[:ip]
        }
      end

      def build_credit_card_request(money, creditcard, options = {})
        requires!(options, :order_id)

        post = {
          reference_number: format_reference_number(options[:order_id]),
          authorization_number: options[:authorization_number],
          transaction_amount: amount(money),
          client_ip_address: options[:ip]

        }
        add_creditcard(post, creditcard)
        add_address(post, options)
        post
      end

      def format_reference_number(number)
        number.to_s.slice(0, 12)
      end

      def add_address(post, options)
        if address = options[:billing_address] || options[:address]
          if address[:address2]
            post[:billing_address]    = address[:address1].to_s << ' ' << address[:address2].to_s
          else
            post[:billing_address]    = address[:address1].to_s
          end
          post[:billing_city]         = address[:city].to_s
          post[:billing_state]        = address[:state].blank? ? 'n/a' : address[:state]
          post[:billing_postal_code]  = address[:zip].to_s
          post[:billing_country]      = address[:country].to_s
        end

        if address = options[:shipping_address]
          if address[:address2]
            post[:shipping_address]   = address[:address1].to_s << ' ' << address[:address2].to_s
          else
            post[:shipping_address]   = address[:address1].to_s
          end
          post[:shipping_city]        = address[:city].to_s
          post[:shipping_state]       = address[:state].blank? ? 'n/a' : address[:state]
          post[:shipping_postal_code] = address[:zip].to_s
          post[:shipping_country]     = address[:country].to_s
        end
      end

      def add_creditcard(post, creditcard)
        post[:billing_name] = creditcard.name if creditcard.name
        post[:account_number] = creditcard.number
        post[:card_verification_value] = creditcard.verification_value if creditcard.verification_value?
        post[:expiration_month] = sprintf('%.2i', creditcard.month)
        post[:expiration_year] = sprintf('%.4i', creditcard.year)[-2..-1]
      end

      def commit(action, parameters)
        response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(action, parameters), 'Content-Type' => 'text/xml'))

        Response.new(
          success?(response),
          message_from(response[:result_message]),
          response,
          test: test?,
          authorization: authorization_from(response, parameters),
          avs_result: { code: response[:avs_response_code] },
          cvv_result: response[:cvv_response_code]
        )
      end

      def success?(response)
        response[:response_code] == '0'
      end

      def authorization_from(response, params)
        [response[:transaction_id], params[:transaction_amount]].compact.join(';')
      end

      def parse(xml)
        response = {}

        xml = REXML::Document.new(xml)

        xml.elements.each('//Reply//TransactionReply/*') do |node|
          response[node.name.underscore.to_sym] = normalize(node.text)
        end unless xml.root.nil?

        response
      end

      def post_data(action, parameters = {})
        xml   = REXML::Document.new("<?xml version='1.0' encoding='UTF-8'?>")
        root  = xml.add_element('Request')
        root.attributes['StoreID'] = options[:login]
        root.attributes['StoreKey'] = options[:password]
        root.attributes['ApplicationID'] = 'ot 1.0'
        transaction = root.add_element(action.to_s.camelize)

        actions[action].each do |key|
          transaction.add_element(key).text = parameters[key.underscore.to_sym] unless parameters[key.underscore.to_sym].blank?
        end

        xml.to_s
      end

      def message_from(message)
        return 'Unspecified error' if message.blank?

        message.gsub(/[^\w]/, ' ').split.join(' ').capitalize
      end

      def actions
        ACTIONS
      end

      CREDIT_CARD_FIELDS = %w(AuthorizationNumber ClientIpAddress BillingAddress BillingCity BillingState BillingPostalCode BillingCountry BillingName CardVerificationValue ExpirationMonth ExpirationYear ReferenceNumber TransactionAmount AccountNumber)

      ACTIONS = {
        credit_card_authorize: CREDIT_CARD_FIELDS,
        credit_card_charge: CREDIT_CARD_FIELDS,
        credit_card_voice_authorize: CREDIT_CARD_FIELDS,
        credit_card_capture: CREDIT_CARD_FIELDS,
        credit_card_credit: CREDIT_CARD_FIELDS + ['OriginalTransactionAmount'],
        credit_card_refund: %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress),
        void_transaction: %w(ReferenceNumber TransactionID),
        credit_card_settle: %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress),
        system_check: %w(SystemCheck)
      }
    end
  end
end
