require 'nokogiri'
require 'securerandom'

module ActiveMerchant # :nodoc:
  module Billing # :nodoc:
    class VantivExpressGateway < Gateway
      self.test_url = 'https://certtransaction.elementexpress.com'
      self.live_url = 'https://transaction.elementexpress.com'

      self.supported_countries = ['US']
      self.default_currency = 'USD'
      self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]

      self.homepage_url = 'http://www.elementps.com'
      self.display_name = 'Element'

      SERVICE_TEST_URL = 'https://certservices.elementexpress.com'
      SERVICE_LIVE_URL = 'https://services.elementexpress.com'

      NETWORK_TOKEN_TYPE = {
        apple_pay: 2,
        google_pay: 1
      }

      CARD_PRESENT_CODE = {
        'Unknown' => 1,
        'Present' => 2,
        'NotPresent' => 3
      }

      MARKET_CODE = {
        'AutoRental' => 1,
        'DirectMarketing' => 2,
        'ECommerce' => 3,
        'FoodRestaurant' => 4,
        'HotelLodging' => 5,
        'Petroleum' => 6,
        'Retail' => 7,
        'QSR' => 8,
        'Grocery' => 9
      }

      PAYMENT_TYPE = {
        'NotUsed' => 0,
        'Recurring' => 1,
        'Installment' => 2,
        'CardHolderInitiated' => 3,
        'CredentialOnFile' => 4
      }

      REVERSAL_TYPE = {
        'System' => 0,
        'Full' => 1,
        'Partial' => 2
      }

      SUBMISSION_TYPE = {
        'NotUsed' => 0,
        'Initial' => 1,
        'Subsequent' => 2,
        'Resubmission' => 3,
        'ReAuthorization' => 4,
        'DelayedCharges' => 5,
        'NoShow' => 6
      }

      LODGING_PPC = {
        'NonParticipant' => 0,
        'DollarLimit500' => 1,
        'DollarLimit1000' => 2,
        'DollarLimit1500' => 3
      }

      LODGING_SPC = {
        'Default' => 0,
        'Sale' => 1,
        'NoShow' => 2,
        'AdvanceDeposit' => 3
      }

      LODGING_CHARGE_TYPE = {
        'Default' => 0,
        'Restaurant' => 1,
        'GiftShop' => 2
      }

      TERMINAL_TYPE = {
        'Unknown' => 0,
        'PointOfSale' => 1,
        'ECommerce' => 2,
        'MOTO' => 3,
        'FuelPump' => 4,
        'ATM' => 5,
        'Voice' => 6,
        'Mobile' => 7,
        'WebSiteGiftCard' => 8
      }

      CARD_HOLDER_PRESENT_CODE = {
        'Default' => 0,
        'Unknown' => 1,
        'Present' => 2,
        'NotPresent' => 3,
        'MailOrder' => 4,
        'PhoneOrder' => 5,
        'StandingAuth' => 6,
        'ECommerce' => 7
      }

      CARD_INPUT_CODE = {
        'Default' => 0,
        'Unknown' => 1,
        'MagstripeRead' => 2,
        'ContactlessMagstripeRead' => 3,
        'ManualKeyed' => 4,
        'ManualKeyedMagstripeFailure' => 5,
        'ChipRead' => 6,
        'ContactlessChipRead' => 7,
        'ManualKeyedChipReadFailure' => 8,
        'MagstripeReadChipReadFailure' => 9,
        'MagstripeReadNonTechnicalFallback' => 10
      }

      CVV_PRESENCE_CODE = {
        'UseDefault' => 0,
        'NotProvided' => 1,
        'Provided' => 2,
        'Illegible' => 3,
        'CustomerIllegible' => 4
      }

      TERMINAL_CAPABILITY_CODE = {
        'Default' => 0,
        'Unknown' => 1,
        'NoTerminal' => 2,
        'MagstripeReader' => 3,
        'ContactlessMagstripeReader' => 4,
        'KeyEntered' => 5,
        'ChipReader' => 6,
        'ContactlessChipReader' => 7
      }

      TERMINAL_ENVIRONMENT_CODE = {
        'Default' => 0,
        'NoTerminal' => 1,
        'LocalAttended' => 2,
        'LocalUnattended' => 3,
        'RemoteAttended' => 4,
        'RemoteUnattended' => 5,
        'ECommerce' => 6
      }

      DISCOUNT_CODE = {
        'NotSupported' => 0,
        'AmountIsDiscounted' => 1,
        'AmountIsNotDiscounted' => 2
      }

      NET_GROSS_CODE = {
        'NotSupported' => 0,
        'ItemAmountIncludesTaxAmount' => 1,
        'ItemAmountDoesNotIncludeTaxAmount' => 2
      }

      DEBIT_CREDIT_CODE = {
        'NotSupported' => 0,
        'ExtendedItemAmountIsCredit' => 1,
        'ExtendedItemAmountIsDebit' => 2
      }

      def initialize(options = {})
        requires!(options, :account_id, :account_token, :application_id, :acceptor_id, :application_name, :application_version)
        super
      end

      def purchase(money, payment, options = {})
        action = payment.is_a?(Check) ? 'CheckSale' : 'CreditCardSale'
        eci = parse_eci(payment)

        request = build_xml_request do |xml|
          xml.send(action, xmlns: live_url) do
            add_credentials(xml)
            add_payment_method(xml, payment)
            add_transaction(xml, money, options, eci)
            add_terminal(xml, options, eci)
            add_address(xml, options)
            add_lodging(xml, options)
          end
        end

        commit(request, money, payment)
      end

      def authorize(money, payment, options = {})
        eci = parse_eci(payment)

        request = build_xml_request do |xml|
          xml.CreditCardAuthorization(xmlns: live_url) do
            add_credentials(xml)
            add_payment_method(xml, payment)
            add_transaction(xml, money, options, eci)
            add_terminal(xml, options, eci)
            add_address(xml, options)
            add_lodging(xml, options)
          end
        end

        commit(request, money, payment)
      end

      def capture(money, authorization, options = {})
        trans_id, _, eci = authorization.split('|')
        options[:trans_id] = trans_id

        request = build_xml_request do |xml|
          xml.CreditCardAuthorizationCompletion(xmlns: live_url) do
            add_credentials(xml)
            add_transaction(xml, money, options, eci)
            add_terminal(xml, options, eci)
            add_level_3_data(xml, options)
          end
        end

        commit(request, money)
      end

      def refund(money, authorization, options = {})
        trans_id, _, eci = authorization.split('|')
        options[:trans_id] = trans_id

        request = build_xml_request do |xml|
          xml.CreditCardReturn(xmlns: live_url) do
            add_credentials(xml)
            add_transaction(xml, money, options, eci)
            add_terminal(xml, options, eci)
          end
        end

        commit(request, money)
      end

      def credit(money, payment, options = {})
        eci = parse_eci(payment)

        request = build_xml_request do |xml|
          xml.CreditCardCredit(xmlns: live_url) do
            add_credentials(xml)
            add_payment_method(xml, payment)
            add_transaction(xml, money, options, eci)
            add_terminal(xml, options, eci)
          end
        end

        commit(request, money, payment)
      end

      def void(authorization, options = {})
        trans_id, trans_amount, eci = authorization.split('|')
        options.merge!({ trans_id:, trans_amount:, reversal_type: 1 })

        request = build_xml_request do |xml|
          xml.CreditCardReversal(xmlns: live_url) do
            add_credentials(xml)
            add_transaction(xml, trans_amount, options, eci)
            add_terminal(xml, options, eci)
          end
        end

        commit(request, trans_amount)
      end

      def store(payment, options = {})
        request = build_xml_request do |xml|
          xml.PaymentAccountCreate(xmlns: SERVICE_LIVE_URL) do
            add_credentials(xml)
            add_payment_method(xml, payment)
            add_payment_account(xml, payment, options[:payment_account_reference_number] || SecureRandom.hex(20))
            add_address(xml, options)
          end
        end

        commit(request, payment, nil, :store)
      end

      def verify(payment, options = {})
        eci = parse_eci(payment)

        request = build_xml_request do |xml|
          xml.CreditCardAVSOnly(xmlns: live_url) do
            add_credentials(xml)
            add_payment_method(xml, payment)
            add_transaction(xml, 0, options, eci)
            add_terminal(xml, options, eci)
            add_address(xml, options)
          end
        end

        commit(request)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((<AccountToken>).+?(</AccountToken>))i, '\1[FILTERED]\2').
          gsub(%r((<CardNumber>).+?(</CardNumber>))i, '\1[FILTERED]\2').
          gsub(%r((<CVV>).+?(</CVV>))i, '\1[FILTERED]\2').
          gsub(%r((<AccountNumber>).+?(</AccountNumber>))i, '\1[FILTERED]\2').
          gsub(%r((<RoutingNumber>).+?(</RoutingNumber>))i, '\1[FILTERED]\2')
      end

      private

      def add_credentials(xml)
        xml.Credentials do
          xml.AccountID @options[:account_id]
          xml.AccountToken @options[:account_token]
          xml.AcceptorID @options[:acceptor_id]
        end
        xml.Application do
          xml.ApplicationID @options[:application_id]
          xml.ApplicationName @options[:application_name]
          xml.ApplicationVersion @options[:application_version]
        end
      end

      def add_payment_method(xml, payment)
        if payment.is_a?(String)
          add_payment_account_id(xml, payment)
        elsif payment.is_a?(Check)
          add_echeck(xml, payment)
        elsif payment.is_a?(NetworkTokenizationCreditCard)
          add_network_tokenization_card(xml, payment)
        else
          add_credit_card(xml, payment)
        end
      end

      def add_payment_account(xml, payment, payment_account_reference_number)
        xml.PaymentAccount do
          xml.PaymentAccountType payment_account_type(payment)
          xml.PaymentAccountReferenceNumber payment_account_reference_number
        end
      end

      def add_payment_account_id(xml, payment)
        xml.PaymentAccount do
          xml.PaymentAccountID payment
        end
      end

      def add_transaction(xml, money, options = {}, network_token_eci = nil)
        xml.Transaction do
          xml.ReversalType REVERSAL_TYPE[options[:reversal_type]] || options[:reversal_type] if options[:reversal_type]
          xml.TransactionID options[:trans_id] if options[:trans_id]
          xml.TransactionAmount amount(money.to_i) if money
          xml.MarketCode market_code(money, options, network_token_eci) if options[:market_code] || money
          xml.ReferenceNumber options[:order_id].present? ? options[:order_id][0, 50] : SecureRandom.hex(20)
          xml.TicketNumber options[:ticket_number] || rand(1..999999)
          xml.MerchantSuppliedTransactionID options[:merchant_supplied_transaction_id] if options[:merchant_supplied_transaction_id]
          xml.PaymentType PAYMENT_TYPE[options[:payment_type]] || options[:payment_type] if options[:payment_type]
          xml.SubmissionType SUBMISSION_TYPE[options[:submission_type]] || options[:submission_type] if options[:submission_type]
          xml.DuplicateCheckDisableFlag 1 if options[:duplicate_check_disable_flag].to_s == 'true' || options[:duplicate_override_flag].to_s == 'true'
          xml.SalesTaxAmount options[:sales_tax_amount] if options[:sales_tax_amount]
          xml.CommercialCardCustomerCode options[:commercial_card_customer_code] if options[:commercial_card_customer_code]
        end
      end

      def parse_eci(payment)
        return nil unless payment.is_a?(NetworkTokenizationCreditCard)

        if (eci = payment.eci)
          eci = eci[0] == '0' ? eci.sub!(/^0/, '') : eci
          return eci
        else
          payment.brand == 'american_express' ? '9' : '6'
        end
      end

      def market_code(money, options, network_token_eci)
        return 3 if network_token_eci

        MARKET_CODE[options[:market_code]] || options[:market_code] || 0
      end

      def add_lodging(xml, options)
        if options[:lodging]
          lodging = parse_lodging(options[:lodging])
          xml.ExtendedParameters do
            xml.Lodging do
              xml.LodgingAgreementNumber lodging[:agreement_number] if lodging[:agreement_number]
              xml.LodgingCheckInDate lodging[:check_in_date] if lodging[:check_in_date]
              xml.LodgingCheckOutDate lodging[:check_out_date] if lodging[:check_out_date]
              xml.LodgingRoomAmount lodging[:room_amount] if lodging[:room_amount]
              xml.LodgingRoomTax lodging[:room_tax] if lodging[:room_tax]
              xml.LodgingNoShowIndicator lodging[:no_show_indicator] if lodging[:no_show_indicator]
              xml.LodgingDuration lodging[:duration] if lodging[:duration]
              xml.LodgingCustomerName lodging[:customer_name] if lodging[:customer_name]
              xml.LodgingClientCode lodging[:client_code] if lodging[:client_code]
              xml.LodgingExtraChargesDetail lodging[:extra_charges_detail] if lodging[:extra_charges_detail]
              xml.LodgingExtraChargesAmounts lodging[:extra_charges_amounts] if lodging[:extra_charges_amounts]
              xml.LodgingPrestigiousPropertyCode lodging[:prestigious_property_code] if lodging[:prestigious_property_code]
              xml.LodgingSpecialProgramCode lodging[:special_program_code] if lodging[:special_program_code]
              xml.LodgingChargeType lodging[:charge_type] if lodging[:charge_type]
            end
          end
        end
      end

      def add_terminal(xml, options, network_token_eci = nil)
        options = parse_terminal(options)

        xml.Terminal do
          xml.TerminalID options[:terminal_id] || '01'
          xml.TerminalType options[:terminal_type] if options[:terminal_type]
          xml.CardPresentCode options[:card_present_code] || 0
          xml.CardholderPresentCode options[:card_holder_present_code] || 0
          xml.CardInputCode options[:card_input_code] || 0
          xml.CVVPresenceCode options[:cvv_presence_code] || 0
          xml.TerminalCapabilityCode options[:terminal_capability_code] || 0
          xml.TerminalEnvironmentCode options[:terminal_environment_code] || 0
          xml.MotoECICode network_token_eci || 7
          xml.PartialApprovedFlag options[:partial_approved_flag] if options[:partial_approved_flag]
        end
      end

      def add_credit_card(xml, payment)
        xml.Card do
          xml.CardNumber payment.number
          xml.ExpirationMonth format(payment.month, :two_digits)
          xml.ExpirationYear format(payment.year, :two_digits)
          xml.CardholderName "#{payment.first_name} #{payment.last_name}"
          xml.CVV payment.verification_value
        end
      end

      def add_echeck(xml, payment)
        xml.DemandDepositAccount do
          xml.AccountNumber payment.account_number
          xml.RoutingNumber payment.routing_number
          xml.DDAAccountType payment.account_type == 'checking' ? 0 : 1
        end
      end

      def add_network_tokenization_card(xml, payment)
        xml.Card do
          xml.CardNumber payment.number
          xml.ExpirationMonth format(payment.month, :two_digits)
          xml.ExpirationYear format(payment.year, :two_digits)
          xml.CardholderName "#{payment.first_name} #{payment.last_name}"
          xml.Cryptogram payment.payment_cryptogram
          xml.WalletType NETWORK_TOKEN_TYPE[payment.source]
        end
      end

      def add_address(xml, options)
        address = address = options[:billing_address] || options[:address]
        shipping_address = options[:shipping_address]

        if address || shipping_address
          xml.Address do
            if address
              address[:email] ||= options[:email]

              xml.BillingAddress1 address[:address1] if address[:address1]
              xml.BillingAddress2 address[:address2] if address[:address2]
              xml.BillingCity address[:city] if address[:city]
              xml.BillingState address[:state] if address[:state]
              xml.BillingZipcode address[:zip] if address[:zip]
              xml.BillingEmail address[:email] if address[:email]
              xml.BillingPhone address[:phone_number] if address[:phone_number]
              xml.BillingName address[:name] if address[:name]
            end

            if shipping_address
              xml.ShippingAddress1 shipping_address[:address1] if shipping_address[:address1]
              xml.ShippingAddress2 shipping_address[:address2] if shipping_address[:address2]
              xml.ShippingCity shipping_address[:city] if shipping_address[:city]
              xml.ShippingState shipping_address[:state] if shipping_address[:state]
              xml.ShippingZipcode shipping_address[:zip] if shipping_address[:zip]
              xml.ShippingEmail shipping_address[:email] if shipping_address[:email]
              xml.ShippingPhone shipping_address[:phone_number] if shipping_address[:phone_number]
            end
          end
        end
      end

      # Add Enhanced Level III data to transaction requests
      def add_level_3_data(xml, options)
        return unless options[:level_3_data]

        level_3 = options[:level_3_data]
        xml.ExtendedParameters do
          xml.EnhancedData do
            # Header-level fields
            xml.MerchantVATRegistrationNumber level_3[:merchant_vat_registration_number] if level_3[:merchant_vat_registration_number]
            xml.CustomerVATRegistrationNumber level_3[:customer_vat_registration_number] if level_3[:customer_vat_registration_number]
            xml.SummaryCommodityCode level_3[:summary_commodity_code] if level_3[:summary_commodity_code]
            xml.DiscountAmount level_3[:discount_amount] if level_3[:discount_amount]
            xml.FreightAmount level_3[:freight_amount] if level_3[:freight_amount]
            xml.DutyAmount level_3[:duty_amount] if level_3[:duty_amount]
            xml.DestinationZIPCode level_3[:destination_postal_code] if level_3[:destination_postal_code]
            xml.ShipFromZIPCode level_3[:ship_from_postal_code] if level_3[:ship_from_postal_code]
            xml.DestinationCountryCode level_3[:destination_country_code] if level_3[:destination_country_code]
            xml.UniqueVATInvoiceReferenceNumber level_3[:unique_vat_invoice_reference_number] if level_3[:unique_vat_invoice_reference_number]
            xml.OrderDate level_3[:order_date] if level_3[:order_date]
            xml.VATAmount level_3[:vat_amount] if level_3[:vat_amount]
            xml.VATRate level_3[:vat_rate] if level_3[:vat_rate]
            xml.LineItemCount level_3[:line_items].size if level_3[:line_items]&.any?
            xml.AlternateTaxAmount level_3[:alternate_tax_amount] if level_3[:alternate_tax_amount]
            xml.NationalTaxAmount level_3[:national_tax_amount] if level_3[:national_tax_amount]

            # Line item details
            add_line_items(xml, level_3[:line_items]) if level_3[:line_items]&.any?
          end
        end
      end

      def add_line_items(xml, line_items)
        return unless line_items

        xml.LineItemDetail do
          line_items.each do |item|
            xml.LineItem do
              xml.ItemCommodityCode item[:commodity_code] if item[:commodity_code]
              xml.ItemDescription item[:description] if item[:description]
              xml.ProductCode item[:product_code] if item[:product_code]
              xml.Quantity item[:quantity] if item[:quantity]
              xml.UnitOfMeasure item[:unit_measure] if item[:unit_measure]
              xml.UnitCost item[:unit_price] if item[:unit_price]
              xml.LineItemVATAmount item[:tax_amount] if item[:tax_amount]
              xml.LineItemVATRate item[:tax_rate] if item[:tax_rate]
              xml.LineItemDiscountAmount item[:discount_amount] if item[:discount_amount]
              xml.LineItemTotalAmount item[:total_amount] if item[:total_amount]
              xml.AlternateTaxIdentifier item[:alternate_tax_id] if item[:alternate_tax_id]
              xml.VATType item[:vat_type] if item[:vat_type]
              xml.DiscountCode DISCOUNT_CODE[item[:discount_code]] || item[:discount_code] if item[:discount_code]
              xml.NetGrossCode NET_GROSS_CODE[item[:net_gross_code]] || item[:net_gross_code] if item[:net_gross_code]
              xml.ExtendedItemAmount item[:extended_item_amount] if item[:extended_item_amount]
              xml.DebitCreditCode DEBIT_CREDIT_CODE[item[:debit_credit_code]] || item[:debit_credit_code] if item[:debit_credit_code]
              xml.ItemDiscountRate item[:item_discount_rate] if item[:item_discount_rate]
            end
          end
        end
      end

      def parse(xml)
        response = {}

        doc = Nokogiri::XML(xml)
        doc.remove_namespaces!
        root = doc.root.xpath('//response/*')

        root = doc.root.xpath('//Response/*') if root.empty?

        root.each do |node|
          if node.elements.empty?
            response[node.name.downcase] = node.text
          else
            node_name = node.name.downcase
            response[node_name] = {}

            node.elements.each do |childnode|
              response[node_name][childnode.name.downcase] = childnode.text
            end
          end
        end

        response
      end

      def parse_lodging(lodging)
        lodging[:prestigious_property_code] = LODGING_PPC[lodging[:prestigious_property_code]] || lodging[:prestigious_property_code] if lodging[:prestigious_property_code]
        lodging[:special_program_code] = LODGING_SPC[lodging[:special_program_code]] || lodging[:special_program_code] if lodging[:special_program_code]
        lodging[:charge_type] = LODGING_CHARGE_TYPE[lodging[:charge_type]] || lodging[:charge_type] if lodging[:charge_type]

        lodging
      end

      def parse_terminal(options)
        options[:terminal_type] = TERMINAL_TYPE[options[:terminal_type]] || options[:terminal_type]
        options[:card_present_code] = CARD_PRESENT_CODE[options[:card_present_code]] || options[:card_present_code]
        options[:card_holder_present_code] = CARD_HOLDER_PRESENT_CODE[options[:card_holder_present_code]] || options[:card_holder_present_code]
        options[:card_input_code] = CARD_INPUT_CODE[options[:card_input_code]] || options[:card_input_code]
        options[:cvv_presence_code] = CVV_PRESENCE_CODE[options[:cvv_presence_code]] || options[:cvv_presence_code]
        options[:terminal_capability_code] = TERMINAL_CAPABILITY_CODE[options[:terminal_capability_code]] || options[:terminal_capability_code]
        options[:terminal_environment_code] = TERMINAL_ENVIRONMENT_CODE[options[:terminal_environment_code]] || options[:terminal_environment_code]

        options
      end

      def commit(xml, amount = nil, payment = nil, action = nil)
        response = parse(ssl_post(url(action), xml, headers))
        success = success_from(response)

        Response.new(
          success,
          message_from(response),
          response,
          authorization: authorization_from(action, response, amount, payment),
          avs_result: success ? avs_from(response) : nil,
          cvv_result: success ? cvv_from(response) : nil,
          test: test?
        )
      end

      def authorization_from(action, response, amount, payment)
        return response.dig('paymentaccount', 'paymentaccountid') if action == :store

        if response['transaction']
          authorization = "#{response.dig('transaction', 'transactionid')}|#{amount}"
          authorization << "|#{parse_eci(payment)}" if parse_eci(payment)
          authorization
        end
      end

      def success_from(response)
        response['expressresponsecode'] == '0'
      end

      def message_from(response)
        response['expressresponsemessage']
      end

      def avs_from(response)
        AVSResult.new(code: response['card']['avsresponsecode']) if response['card']
      end

      def cvv_from(response)
        CVVResult.new(response['card']['cvvresponsecode']) if response['card']
      end

      def build_xml_request(&block)
        builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8', &block)

        builder.to_xml
      end

      def payment_account_type(payment)
        return 0 unless payment.is_a?(Check)

        if payment.account_type == 'checking'
          1
        elsif payment.account_type == 'savings'
          2
        else
          3
        end
      end

      def url(action)
        if action == :store
          test? ? SERVICE_TEST_URL : SERVICE_LIVE_URL
        else
          test? ? test_url : live_url
        end
      end

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