require 'nokogiri'

module ActiveMerchant # :nodoc:
  module Billing # :nodoc:
    class TelrGateway < Gateway
      self.display_name = 'Telr'
      self.homepage_url = 'http://www.telr.com/'

      self.live_url = 'https://secure.telr.com/gateway/remote.xml'

      self.supported_countries = %w[AE IN SA]
      self.default_currency = 'AED'
      self.money_format = :dollars
      self.supported_cardtypes = %i[visa master american_express maestro jcb]

      CVC_CODE_TRANSLATOR = {
        'Y' => 'M',
        'N' => 'N',
        'X' => 'P',
        'E' => 'U'
      }

      AVS_CODE_TRANSLATOR = {
        'Y' => 'M',
        'P' => 'A',
        'N' => 'N',
        'X' => 'I',
        'E' => 'R'
      }

      def initialize(options = {})
        requires!(options, :merchant_id, :api_key)
        super
      end

      def purchase(amount, payment_method, options = {})
        commit(:purchase, amount, options[:currency]) do |doc|
          add_invoice(doc, 'sale', amount, payment_method, options)
          add_payment_method(doc, payment_method, options)
          add_customer_data(doc, payment_method, options)
        end
      end

      def authorize(amount, payment_method, options = {})
        commit(:authorize, amount, options[:currency]) do |doc|
          add_invoice(doc, 'auth', amount, payment_method, options)
          add_payment_method(doc, payment_method, options)
          add_customer_data(doc, payment_method, options)
        end
      end

      def capture(amount, authorization, options = {})
        commit(:capture) do |doc|
          add_invoice(doc, 'capture', amount, authorization, options)
        end
      end

      def void(authorization, options = {})
        _, amount, currency = split_authorization(authorization)
        commit(:void) do |doc|
          add_invoice(doc, 'void', amount.to_i, authorization, options.merge(currency:))
        end
      end

      def refund(amount, authorization, options = {})
        commit(:refund) do |doc|
          add_invoice(doc, 'refund', amount, authorization, options)
        end
      end

      def verify(credit_card, options = {})
        commit(:verify) do |doc|
          add_invoice(doc, 'verify', 100, credit_card, options)
          add_payment_method(doc, credit_card, options)
          add_customer_data(doc, credit_card, options)
        end
      end

      def verify_credentials
        response = void('0')
        !%w[01 04].include?(response.error_code)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((<Number>)[^<]+(<))i, '\1[FILTERED]\2').
          gsub(%r((<CVV>)[^<]+(<))i, '\1[FILTERED]\2').
          gsub(%r((<Key>)[^<]+(<))i, '\1[FILTERED]\2')
      end

      private

      def add_invoice(doc, action, money, payment_method, options)
        doc.tran do
          doc.type(action)
          doc.amount(amount(money))
          doc.currency(options[:currency] || currency(money))
          doc.cartid(options[:order_id])
          doc.class_(transaction_class(action, payment_method))
          doc.description(options[:description] || 'Description')
          doc.test_(test_mode)
          add_ref(doc, action, payment_method)
        end
      end

      def add_payment_method(doc, payment_method, options)
        return if payment_method.is_a?(String)

        doc.card do
          doc.number(payment_method.number)
          doc.cvv(payment_method.verification_value)
          doc.expiry do
            doc.month(format(payment_method.month, :two_digits))
            doc.year(format(payment_method.year, :four_digits))
          end
        end
      end

      def add_customer_data(doc, payment_method, options)
        return if payment_method.is_a?(String)

        doc.billing do
          doc.name do
            doc.first(payment_method.first_name)
            doc.last(payment_method.last_name)
          end
          doc.email(options[:email] || 'unspecified@email.com')
          doc.ip(options[:ip]) if options[:ip]
          doc.address do
            add_address(doc, options)
          end
        end
      end

      def add_address(doc, options)
        address = options[:billing_address] || {}
        doc.country(address[:country] ? lookup_country_code(address[:country]) : 'NA')
        doc.city(address[:city] || 'City')
        doc.line1(address[:address1] || 'Address')
        return unless address

        doc.line2(address[:address2]) if address[:address2]
        doc.zip(address[:zip]) if address[:zip]
        doc.region(address[:state]) if address[:state]
      end

      def add_ref(doc, action, payment_method)
        doc.ref(split_authorization(payment_method)[0]) if %w[capture refund void].include?(action) || payment_method.is_a?(String)
      end

      def add_authentication(doc)
        doc.store(@options[:merchant_id])
        doc.key(@options[:api_key])
      end

      def lookup_country_code(code)
        country = Country.find(code) rescue nil
        country.code(:alpha2)
      end

      def commit(action, amount = nil, currency = nil, &block)
        currency = default_currency if currency == nil
        request = build_xml_request(&block)
        response = ssl_post(live_url, request, headers)
        parsed = parse(response)

        succeeded = success_from(parsed)
        Response.new(
          succeeded,
          message_from(succeeded, parsed),
          parsed,
          authorization: authorization_from(action, parsed, amount, currency),
          avs_result: avs_result(parsed),
          cvv_result: cvv_result(parsed),
          error_code: error_code_from(succeeded, parsed),
          test: test?
        )
      end

      def root_attributes
        {
          store: @options[:merchant_id],
          key: @options[:api_key]
        }
      end

      def build_xml_request
        builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
          xml.remote do |doc|
            add_authentication(doc)
            yield(doc)
          end
        end

        builder.doc.to_xml
      end

      def test_mode
        test? ? '1' : '0'
      end

      def transaction_class(action, payment_method)
        if payment_method.is_a?(String) && action == 'sale'
          return 'cont'
        else
          return 'moto'
        end
      end

      def parse(xml)
        response = {}

        doc = Nokogiri::XML(xml)
        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 = childnode.name.downcase
              response[name.to_sym] = childnode.text
            end
          end
        end

        response
      end

      def authorization_from(action, response, amount, currency)
        auth = response[:tranref]
        [auth, amount, currency].join('|')
      end

      def split_authorization(authorization)
        authorization.split('|')
      end

      def success_from(response)
        response[:status] == 'A'
      end

      def message_from(succeeded, response)
        if succeeded
          'Succeeded'
        else
          response[:message]
        end
      end

      def error_code_from(succeeded, response)
        response[:code] unless succeeded
      end

      def cvv_result(parsed)
        CVVResult.new(CVC_CODE_TRANSLATOR[parsed[:cvv]])
      end

      def avs_result(parsed)
        AVSResult.new(code: AVS_CODE_TRANSLATOR[parsed[:avs]])
      end

      def headers
        {
          'Content-Type' => 'text/xml'
        }
      end
    end
  end
end
