require 'rexml/document'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    # The National Australia Bank provide a payment gateway that seems to
    # be a rebadged Securepay Australia service, though some differences exist.
    class NabTransactGateway < Gateway
      API_VERSION = 'xml-4.2'
      PERIODIC_API_VERSION = "spxml-4.2"

      TEST_URL = 'https://transact.nab.com.au/test/xmlapi/payment'
      LIVE_URL = 'https://transact.nab.com.au/live/xmlapi/payment'
      TEST_PERIODIC_URL = "https://transact.nab.com.au/xmlapidemo/periodic"
      LIVE_PERIODIC_URL = "https://transact.nab.com.au/xmlapi/periodic"

      self.supported_countries = ['AU']

      # The card types supported by the payment gateway
      # Note that support for Diners, Amex, and JCB require extra
      # steps in setting up your account, as detailed in the NAB Transact API
      self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb]

      self.homepage_url = 'http://transact.nab.com.au'
      self.display_name = 'NAB Transact'

      cattr_accessor :request_timeout
      self.request_timeout = 60

      self.money_format = :cents
      self.default_currency = 'AUD'

      #Transactions currently accepted by NAB Transact XML API
      TRANSACTIONS = {
        :purchase => 0,         #Standard Payment
        :credit => 4,           #Refund
        :void => 6,             #Client Reversal (Void)
        :authorization => 10,   #Preauthorise
        :capture => 11          #Preauthorise Complete (Advice)
      }

      PERIODIC_TYPES = {
        :addcrn    => 5,
        :editcrn   => 5,
        :deletecrn => 5,
        :trigger   => 8
      }

      SUCCESS_CODES = [ '00', '08', '11', '16', '77' ]


      def initialize(options = {})
        requires!(options, :login, :password)
        @options = options
        super
      end

      def test?
        @options[:test] || super
      end

      def purchase(money, credit_card_or_stored_id, options = {})
        if credit_card_or_stored_id.respond_to?(:number)
          #Credit card for instant payment
          commit :purchase, build_purchase_request(money, credit_card_or_stored_id, options)
        else
          #Triggered payment for an existing stored credit card
          options[:billing_id] = credit_card_or_stored_id.to_s
          commit_periodic build_periodic_item(:trigger, money, nil, options)
        end
      end

      def store(creditcard, options = {})
        requires!(options, :billing_id, :amount)
        commit_periodic(build_periodic_item(:addcrn, options[:amount], creditcard, options))
      end

      def unstore(identification, options = {})
        options[:billing_id] = identification
        commit_periodic(build_periodic_item(:deletecrn, options[:amount], nil, options))
      end

      private

      def add_metadata(xml, options)
        if options[:merchant_name] || options[:merchant_location]
          xml.tag! 'metadata' do
            xml.tag! 'meta', :name => 'ca_name', :value => options[:merchant_name] if options[:merchant_name]
            xml.tag! 'meta', :name => 'ca_location', :value => options[:merchant_location] if options[:merchant_location]
          end
        end
      end

      def build_purchase_request(money, credit_card, options)
        xml = Builder::XmlMarkup.new
        xml.tag! 'amount', amount(money)
        xml.tag! 'currency', options[:currency] || currency(money)
        xml.tag! 'purchaseOrderNo', options[:order_id].to_s.gsub(/[ ']/, '')

        xml.tag! 'CreditCardInfo' do
          xml.tag! 'cardNumber', credit_card.number
          xml.tag! 'expiryDate', expdate(credit_card)
          xml.tag! 'cvv', credit_card.verification_value if credit_card.verification_value?
        end

        add_metadata(xml, options)

        xml.target!
      end

      #Generate payment request XML
      # - API is set to allow multiple Txn's but currentlu only allows one
      # - txnSource = 23 - (XML)

      def build_request(action, body)
        xml = Builder::XmlMarkup.new
        xml.instruct!
        xml.tag! 'NABTransactMessage' do
          xml.tag! 'MessageInfo' do
            xml.tag! 'messageID', Utils.generate_unique_id.slice(0, 30)
            xml.tag! 'messageTimestamp', generate_timestamp
            xml.tag! 'timeoutValue', request_timeout
            xml.tag! 'apiVersion', API_VERSION
          end

          xml.tag! 'MerchantInfo' do
            xml.tag! 'merchantID', @options[:login]
            xml.tag! 'password', @options[:password]
          end

          xml.tag! 'RequestType', 'Payment'
          xml.tag! 'Payment' do
            xml.tag! 'TxnList', "count" => 1 do
              xml.tag! 'Txn', "ID" => 1 do
                xml.tag! 'txnType', TRANSACTIONS[action]
                xml.tag! 'txnSource', 23
                xml << body
              end
            end
          end
        end

        xml.target!
      end

      def build_periodic_item(action, money, credit_card, options)
        xml = Builder::XmlMarkup.new

        xml.tag! 'actionType', action.to_s
        xml.tag! 'periodicType', PERIODIC_TYPES[action] if PERIODIC_TYPES[action]
        xml.tag! 'currency', options[:currency] || currency(money)
        xml.tag! 'crn', options[:billing_id]

        if credit_card
          xml.tag! 'CreditCardInfo' do
            xml.tag! 'cardNumber', credit_card.number
            xml.tag! 'expiryDate', expdate(credit_card)
            xml.tag! 'cvv', credit_card.verification_value if credit_card.verification_value?
          end
        end
        xml.tag! 'amount', amount(money)

        xml.target!
      end

      def build_periodic_request(body)
        xml = Builder::XmlMarkup.new
        xml.instruct!
        xml.tag! 'NABTransactMessage' do
          xml.tag! 'MessageInfo' do
            xml.tag! 'messageID', ActiveMerchant::Utils.generate_unique_id.slice(0, 30)
            xml.tag! 'messageTimestamp', generate_timestamp
            xml.tag! 'timeoutValue', request_timeout
            xml.tag! 'apiVersion', PERIODIC_API_VERSION
          end

          xml.tag! 'MerchantInfo' do
            xml.tag! 'merchantID', @options[:login]
            xml.tag! 'password', @options[:password]
          end

          xml.tag! 'RequestType', 'Periodic'
          xml.tag! 'Periodic' do
            xml.tag! 'PeriodicList', "count" => 1 do
              xml.tag! 'PeriodicItem', "ID" => 1 do
                xml << body
              end
            end
          end
        end

        xml.target!
      end

      def commit(action, request)
        response = parse(ssl_post(test? ? TEST_URL : LIVE_URL, build_request(action, request)))

        Response.new(success?(response), message_from(response), response,
          :test => test?,
          :authorization => authorization_from(response)
        )
      end

      def commit_periodic(request)
        response = parse(ssl_post(test? ? TEST_PERIODIC_URL : LIVE_PERIODIC_URL, build_periodic_request(request)))
        Response.new(success?(response), message_from(response), response,
          :test => test?,
          :authorization => authorization_from(response)
        )
      end

      def success?(response)
        SUCCESS_CODES.include?(response[:response_code])
      end

      def authorization_from(response)
        response[:txn_id]
      end

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

      def expdate(credit_card)
        "#{format(credit_card.month, :two_digits)}/#{format(credit_card.year, :two_digits)}"
      end

      def parse(body)
        xml = REXML::Document.new(body)

        response = {}

        xml.root.elements.to_a.each do |node|
          parse_element(response, node)
        end

        response
      end

      def parse_element(response, node)
        if node.has_elements?
          node.elements.each{|element| parse_element(response, element) }
        else
          response[node.name.underscore.to_sym] = node.text
        end
      end

      # YYYYDDMMHHNNSSKKK000sOOO
      def generate_timestamp
        time = Time.now.utc
        time.strftime("%Y%d%m%H%M%S#{time.usec}+000")
      end

    end
  end
end