require 'rexml/document'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:

    class EfsnetGateway < Gateway
      self.supported_countries = ['US']
      self.supported_cardtypes = [: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)
          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, original_transaction_amount = 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)

        post = {
          :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

      # Make a ruby type out of the response string
      def normalize(field)
        case field
        when "true"   then true
        when "false"  then false
        when ""       then nil
        when "null"   then nil
        else field
        end
      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
