require 'nokogiri'

module ActiveMerchant # :nodoc:
  module Billing # :nodoc:
    class PayexGateway < Gateway
      class_attribute :live_external_url, :test_external_url, :live_confined_url, :test_confined_url

      self.live_external_url = 'https://external.payex.com/'
      self.test_external_url = 'https://test-external.payex.com/'

      self.live_confined_url = 'https://confined.payex.com/'
      self.test_confined_url = 'https://test-confined.payex.com/'

      self.money_format = :cents
      self.supported_countries = %w[DK FI NO SE]
      self.supported_cardtypes = %i[visa master american_express discover]
      self.homepage_url = 'http://payex.com/'
      self.display_name = 'Payex'
      self.default_currency = 'EUR'

      TRANSACTION_STATUS = {
        sale:       '0',
        initialize: '1',
        credit:     '2',
        authorize:  '3',
        cancel:     '4',
        failure:    '5',
        capture:    '6'
      }

      SOAP_ACTIONS = {
        initialize: { name: 'Initialize8', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
        purchasecc: { name: 'PurchaseCC', url: 'pxconfined/pxorder.asmx', xmlns: 'http://confined.payex.com/PxOrder/', confined: true },
        cancel: { name: 'Cancel2', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
        capture: { name: 'Capture5', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
        credit: { name: 'Credit5', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
        create_agreement: { name: 'CreateAgreement3', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' },
        delete_agreement: { name: 'DeleteAgreement', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' },
        autopay: { name: 'AutoPay3', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' }
      }

      def initialize(options = {})
        requires!(options, :account, :encryption_key)
        super
      end

      # Public: Send an authorize Payex request
      #
      # amount         - The monetary amount of the transaction in cents.
      # payment_method - The Active Merchant payment method or the +store+ authorization for stored transactions.
      # options        - A standard ActiveMerchant options hash:
      #                  :currency          - Three letter currency code for the transaction (default: "EUR")
      #                  :order_id          - The unique order ID for this transaction (required).
      #                  :product_number    - The merchant product number (default: '1').
      #                  :description       - The merchant description for this product (default: The :order_id).
      #                  :ip                - The client IP address (default: '127.0.0.1').
      #                  :vat               - The vat amount (optional).
      #
      # Returns an ActiveMerchant::Billing::Response object
      def authorize(amount, payment_method, options = {})
        requires!(options, :order_id)
        amount = amount(amount)
        if payment_method.respond_to?(:number)
          # credit card authorization
          MultiResponse.new.tap do |r|
            r.process { send_initialize(amount, true, options) }
            r.process { send_purchasecc(payment_method, r.params['orderref']) }
          end
        else
          # stored authorization
          send_autopay(amount, payment_method, true, options)
        end
      end

      # Public: Send a purchase Payex request
      #
      # amount         - The monetary amount of the transaction in cents.
      # payment_method - The Active Merchant payment method or the +store+ authorization for stored transactions.
      # options        - A standard ActiveMerchant options hash:
      #                  :currency          - Three letter currency code for the transaction (default: "EUR")
      #                  :order_id          - The unique order ID for this transaction (required).
      #                  :product_number    - The merchant product number (default: '1').
      #                  :description       - The merchant description for this product (default: The :order_id).
      #                  :ip                - The client IP address (default: '127.0.0.1').
      #                  :vat               - The vat amount (optional).
      #
      # Returns an ActiveMerchant::Billing::Response object
      def purchase(amount, payment_method, options = {})
        requires!(options, :order_id)
        amount = amount(amount)
        if payment_method.respond_to?(:number)
          # credit card purchase
          MultiResponse.new.tap do |r|
            r.process { send_initialize(amount, false, options) }
            r.process { send_purchasecc(payment_method, r.params['orderref']) }
          end
        else
          # stored purchase
          send_autopay(amount, payment_method, false, options)
        end
      end

      # Public: Capture money from a previously authorized transaction
      #
      # money - The amount to capture
      # authorization - The authorization token from the authorization request
      #
      # Returns an ActiveMerchant::Billing::Response object
      def capture(money, authorization, options = {})
        amount = amount(money)
        send_capture(amount, authorization)
      end

      # Public: Voids an authorize transaction
      #
      # authorization - The authorization returned from the successful authorize transaction.
      # options        - A standard ActiveMerchant options hash
      #
      # Returns an ActiveMerchant::Billing::Response object
      def void(authorization, options = {})
        send_cancel(authorization)
      end

      # Public: Refunds a purchase transaction
      #
      # money - The amount to refund
      # authorization - The authorization token from the purchase request.
      # options        - A standard ActiveMerchant options hash:
      #                  :order_id          - The unique order ID for this transaction (required).
      #                  :vat_amount        - The vat amount (optional).
      #
      # Returns an ActiveMerchant::Billing::Response object
      def refund(money, authorization, options = {})
        requires!(options, :order_id)
        amount = amount(money)
        send_credit(authorization, amount, options)
      end

      # Public: Stores a credit card and creates a Payex agreement with a customer
      #
      # creditcard - The credit card to store.
      # options    - A standard ActiveMerchant options hash:
      #               :order_id          - The unique order ID for this transaction (required).
      #               :merchant_ref      - A reference that links this agreement to something the merchant takes money for (default: '1')
      #               :currency          - Three letter currency code for the transaction (default: "EUR")
      #               :product_number    - The merchant product number (default: '1').
      #               :description       - The merchant description for this product (default: The :order_id).
      #               :ip                - The client IP address (default: '127.0.0.1').
      #               :max_amount        - The maximum amount to allow to be charged (default: 100000).
      #               :vat               - The vat amount (optional).
      #
      # Returns an ActiveMerchant::Billing::Response object where the authorization is set to the agreement_ref which is used for stored payments.
      def store(creditcard, options = {})
        requires!(options, :order_id)
        amount = amount(1) # 1 cent for authorization
        MultiResponse.run(:first) do |r|
          r.process { send_create_agreement(options) }
          r.process { send_initialize(amount, true, options.merge({ agreement_ref: r.authorization })) }
          order_ref = r.params['orderref']
          r.process { send_purchasecc(creditcard, order_ref) }
        end
      end

      # Public: Unstores a customer's credit card and deletes their Payex agreement.
      #
      # authorization - The authorization token from the store request.
      #
      # Returns an ActiveMerchant::Billing::Response object
      def unstore(authorization, options = {})
        send_delete_agreement(authorization)
      end

      private

      def send_initialize(amount, is_auth, options = {})
        properties = {
          accountNumber: @options[:account],
          purchaseOperation: is_auth ? 'AUTHORIZATION' : 'SALE',
          price: amount,
          priceArgList: nil,
          currency: (options[:currency] || default_currency),
          vat: options[:vat] || 0,
          orderID: options[:order_id],
          productNumber: options[:product_number] || '1',
          description: options[:description] || options[:order_id],
          clientIPAddress: options[:client_ip_address] || '127.0.0.1',
          clientIdentifier: nil,
          additionalValues: nil,
          externalID: nil,
          returnUrl: 'http://example.net', # set to dummy value since this is not used but is required
          view: 'CREDITCARD',
          agreementRef: options[:agreement_ref], # this is used to attach a stored agreement to a transaction as part of the store card
          cancelUrl: nil,
          clientLanguage: nil
        }
        hash_fields = %i[accountNumber purchaseOperation price priceArgList currency vat orderID
                         productNumber description clientIPAddress clientIdentifier additionalValues
                         externalID returnUrl view agreementRef cancelUrl clientLanguage]
        add_request_hash(properties, hash_fields)
        soap_action = SOAP_ACTIONS[:initialize]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def send_purchasecc(payment_method, order_ref)
        properties = {
          accountNumber: @options[:account],
          orderRef: order_ref,
          transactionType: 1, # online payment
          cardNumber: payment_method.number,
          cardNumberExpireMonth: format(payment_method.month, :two_digits),
          cardNumberExpireYear: format(payment_method.year, :two_digits),
          cardHolderName: payment_method.name,
          cardNumberCVC: payment_method.verification_value
        }
        hash_fields = %i[accountNumber orderRef transactionType cardNumber cardNumberExpireMonth
                         cardNumberExpireYear cardNumberCVC cardHolderName]
        add_request_hash(properties, hash_fields)

        soap_action = SOAP_ACTIONS[:purchasecc]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def send_autopay(amount, authorization, is_auth, options = {})
        properties = {
          accountNumber: @options[:account],
          agreementRef: authorization,
          price: amount,
          productNumber: options[:product_number] || '1',
          description: options[:description] || options[:order_id],
          orderId: options[:order_id],
          purchaseOperation: is_auth ? 'AUTHORIZATION' : 'SALE',
          currency: (options[:currency] || default_currency)
        }
        hash_fields = %i[accountNumber agreementRef price productNumber description orderId purchaseOperation currency]
        add_request_hash(properties, hash_fields)

        soap_action = SOAP_ACTIONS[:autopay]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def send_capture(amount, transaction_number, options = {})
        properties = {
          accountNumber: @options[:account],
          transactionNumber: transaction_number,
          amount:,
          orderId: options[:order_id] || '',
          vatAmount: options[:vat_amount] || 0,
          additionalValues: ''
        }
        hash_fields = %i[accountNumber transactionNumber amount orderId vatAmount additionalValues]
        add_request_hash(properties, hash_fields)

        soap_action = SOAP_ACTIONS[:capture]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def send_credit(transaction_number, amount, options = {})
        properties = {
          accountNumber: @options[:account],
          transactionNumber: transaction_number,
          amount:,
          orderId: options[:order_id],
          vatAmount: options[:vat_amount] || 0,
          additionalValues: ''
        }
        hash_fields = %i[accountNumber transactionNumber amount orderId vatAmount additionalValues]
        add_request_hash(properties, hash_fields)

        soap_action = SOAP_ACTIONS[:credit]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def send_cancel(transaction_number)
        properties = {
          accountNumber: @options[:account],
          transactionNumber: transaction_number
        }
        hash_fields = %i[accountNumber transactionNumber]
        add_request_hash(properties, hash_fields)

        soap_action = SOAP_ACTIONS[:cancel]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def send_create_agreement(options)
        properties = {
          accountNumber: @options[:account],
          merchantRef: options[:merchant_ref] || '1',
          description: options[:description] || options[:order_id],
          purchaseOperation: 'SALE',
          maxAmount: options[:max_amount] || 100000, # default to 1,000
          notifyUrl: '',
          startDate: options[:startDate] || '',
          stopDate: options[:stopDate] || ''
        }
        hash_fields = %i[accountNumber merchantRef description purchaseOperation maxAmount notifyUrl startDate stopDate]
        add_request_hash(properties, hash_fields)

        soap_action = SOAP_ACTIONS[:create_agreement]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def send_delete_agreement(authorization)
        properties = {
          accountNumber: @options[:account],
          agreementRef: authorization
        }
        hash_fields = %i[accountNumber agreementRef]
        add_request_hash(properties, hash_fields)

        soap_action = SOAP_ACTIONS[:delete_agreement]
        request = build_xml_request(soap_action, properties)
        commit(soap_action, request)
      end

      def url_for(soap_action)
        File.join(base_url(soap_action), soap_action[:url])
      end

      def base_url(soap_action)
        if soap_action[:confined]
          test? ? test_confined_url : live_confined_url
        else
          test? ? test_external_url : live_external_url
        end
      end

      # this will add a hash to the passed in properties as required by Payex requests
      def add_request_hash(properties, fields)
        data = fields.map { |e| properties[e] }
        data << @options[:encryption_key]
        properties['hash_'] = Digest::MD5.hexdigest(data.join(''))
      end

      def build_xml_request(soap_action, properties)
        builder = Nokogiri::XML::Builder.new
        builder.__send__('soap12:Envelope', { 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
                                             'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
                                             'xmlns:soap12' => 'http://www.w3.org/2003/05/soap-envelope' }) do |root|
          root.__send__('soap12:Body') do |body|
            body.__send__(soap_action[:name], xmlns: soap_action[:xmlns]) do |doc|
              properties.each do |key, val|
                doc.send(key, val)
              end
            end
          end
        end
        builder.to_xml
      end

      def parse(xml)
        response = {}

        xmldoc = Nokogiri::XML(xml)
        body = xmldoc.xpath('//soap:Body/*[1]')[0].inner_text

        doc = Nokogiri::XML(body)

        doc.root&.xpath('*')&.each do |node|
          if node.elements.size == 0
            response[node.name.downcase.to_sym] = node.text
          else
            node.elements.each do |childnode|
              name = "#{node.name.downcase}_#{childnode.name.downcase}"
              response[name.to_sym] = childnode.text
            end
          end
        end

        response
      end

      # Commits all requests to the Payex soap endpoint
      def commit(soap_action, request)
        url = url_for(soap_action)
        headers = {
          'Content-Type' => 'application/soap+xml; charset=utf-8',
          'Content-Length' => request.size.to_s
        }
        response = parse(ssl_post(url, request, headers))
        Response.new(
          success?(response),
          message_from(response),
          response,
          test: test?,
          authorization: build_authorization(response)
        )
      end

      def build_authorization(response)
        # agreementref is for the store transaction, everything else gets transactionnumber
        response[:transactionnumber] || response[:agreementref]
      end

      def success?(response)
        response[:status_errorcode] == 'OK' && response[:transactionstatus] != TRANSACTION_STATUS[:failure]
      end

      def message_from(response)
        response[:status_description]
      end
    end
  end
end
