module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class VisanetPeruGateway < Gateway
      include Empty
      self.display_name = 'VisaNet Peru Gateway'
      self.homepage_url = 'http://www.visanet.com.pe'

      self.test_url = 'https://devapi.vnforapps.com/api.tokenization/api/v2/merchant'
      self.live_url = 'https://api.vnforapps.com/api.tokenization/api/v2/merchant'

      self.supported_countries = %w[US PE]
      self.default_currency = 'PEN'
      self.money_format = :dollars
      self.supported_cardtypes = %i[visa master american_express discover]

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

      def purchase(amount, payment_method, options = {})
        MultiResponse.run() do |r|
          r.process { authorize(amount, payment_method, options) }
          r.process { capture(amount, r.authorization, options) }
        end
      end

      def authorize(amount, payment_method, options = {})
        params = {}

        add_invoice(params, amount, options)
        add_payment_method(params, payment_method)
        add_antifraud_data(params, options)
        params[:email] = options[:email] || 'unknown@email.com'
        params[:createAlias] = false

        commit('authorize', params, options)
      end

      def capture(amount, authorization, options = {})
        params = {}
        options[:id_unico] = split_authorization(authorization)[1]
        add_auth_order_id(params, authorization, options)
        commit('deposit', params, options)
      end

      def void(authorization, options = {})
        params = {}
        add_auth_order_id(params, authorization, options)
        commit('void', params, options)
      end

      def refund(amount, authorization, options = {})
        params = {}
        params[:amount] = amount(amount) if amount
        add_auth_order_id(params, authorization, options)
        response = commit('cancelDeposit', params, options)
        return response if response.success? || split_authorization(authorization).length == 1 || !options[:force_full_refund_if_unsettled]

        # Attempt RefundSingleTransaction if unsettled (and stash the original
        # response message so it will be included it in the follow-up response
        # message)
        options[:error_message] = response.message
        prepare_refund_data(params, authorization, options)
        commit('refund', params, options)
      end

      def verify(credit_card, options = {})
        MultiResponse.run(:use_first_response) do |r|
          r.process { authorize(100, credit_card, options) }
          r.process(:ignore_result) { void(r.authorization, options) }
        end
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
          gsub(%r((\"cardNumber\\\":\\\")\d+), '\1[FILTERED]').
          gsub(%r((\"cvv2Code\\\":\\\")\d+), '\1[FILTERED]')
      end

      private

      CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") }
      CURRENCY_CODES['USD'] = 840
      CURRENCY_CODES['PEN'] = 604

      def add_invoice(params, money, options)
        # Visanet Peru expects a 12-digit alphanumeric purchaseNumber
        params[:purchaseNumber] = generate_purchase_number_stamp
        params[:externalTransactionId] = options[:order_id]
        params[:amount] = amount(money)
        params[:currencyId] = CURRENCY_CODES[options[:currency] || currency(money)]
      end

      def add_auth_order_id(params, authorization, options)
        purchase_number, = split_authorization(authorization)
        params[:purchaseNumber] = purchase_number
        params[:externalTransactionId] = options[:order_id]
      end

      def add_payment_method(params, payment_method)
        params[:firstName] = payment_method.first_name
        params[:lastName] = payment_method.last_name
        params[:cardNumber] = payment_method.number
        params[:cvv2Code] = payment_method.verification_value
        params[:expirationYear] = format(payment_method.year, :four_digits)
        params[:expirationMonth] = format(payment_method.month, :two_digits)
      end

      def add_antifraud_data(params, options)
        antifraud = {}

        if billing_address = options[:billing_address] || options[:address]
          antifraud[:billTo_street1] = billing_address[:address1]
          antifraud[:billTo_city] = billing_address[:city]
          antifraud[:billTo_state] = billing_address[:state]
          antifraud[:billTo_country] = billing_address[:country]
          antifraud[:billTo_postalCode] = billing_address[:zip]
        end

        antifraud[:deviceFingerprintId] = options[:device_fingerprint_id] || SecureRandom.hex(16)
        antifraud[:merchantDefineData] = options[:merchant_define_data] if options[:merchant_define_data]

        params[:antifraud] = antifraud
      end

      def prepare_refund_data(params, authorization, options)
        params.delete(:purchaseNumber)
        params[:externalReferenceId] = params.delete(:externalTransactionId)
        _, transaction_id = split_authorization(authorization)

        options.update(transaction_id: transaction_id)
        params[:ruc] = options[:ruc]
      end

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

      def generate_purchase_number_stamp
        rand(('9' * 12).to_i).to_s.center(12, rand(9).to_s)
      end

      def commit(action, params, options = {})
        raw_response = ssl_request(method(action), url(action, params, options), params.to_json, headers)
        response = parse(raw_response).merge('purchaseNumber' => params[:purchaseNumber])
      rescue ResponseError => e
        raw_response = e.response.body
        response_error(raw_response, options, action)
      rescue JSON::ParserError
        unparsable_response(raw_response)
      else
        Response.new(
          success_from(response),
          message_from(response, options, action),
          response,
          test: test?,
          authorization: authorization_from(params, response, options),
          error_code: response['errorCode']
        )
      end

      def headers
        {
          'Authorization' => 'Basic ' + Base64.strict_encode64("#{@options[:access_key_id]}:#{@options[:secret_access_key]}").strip,
          'Content-Type'  => 'application/json'
        }
      end

      def url(action, params, options = {})
        if action == 'authorize'
          "#{base_url}/#{@options[:merchant_id]}"
        elsif action == 'refund'
          "#{base_url}/#{@options[:merchant_id]}/#{action}/#{options[:transaction_id]}"
        else
          "#{base_url}/#{@options[:merchant_id]}/#{action}/#{params[:purchaseNumber]}"
        end
      end

      def method(action)
        %w(authorize refund).include?(action) ? :post : :put
      end

      def authorization_from(params, response, options)
        id_unico = response['data']['ID_UNICO'] || options[:id_unico]
        "#{params[:purchaseNumber]}|#{id_unico}"
      end

      def base_url
        test? ? test_url : live_url
      end

      def parse(body)
        JSON.parse(body)
      end

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

      def message_from(response, options, action)
        message_from_messages(
          response['errorMessage'],
          action_code_description(response),
          options[:error_message]
        )
      end

      def message_from_messages(*args)
        args.reject { |m| error_message_empty?(m) }.join(' | ')
      end

      def action_code_description(response)
        return nil unless response['data']

        response['data']['DSC_COD_ACCION']
      end

      def error_message_empty?(error_message)
        empty?(error_message) || error_message == '[ ]'
      end

      def response_error(raw_response, options, action)
        response = parse(raw_response)
      rescue JSON::ParserError
        unparsable_response(raw_response)
      else
        return Response.new(
          false,
          message_from(response, options, action),
          response,
          test: test?,
          authorization: response['transactionUUID'],
          error_code: response['errorCode']
        )
      end

      def unparsable_response(raw_response)
        message = 'Invalid JSON response received from VisanetPeruGateway. Please contact VisanetPeruGateway if you continue to receive this message.'
        message += " (The raw response returned by the API was #{raw_response.inspect})"
        return Response.new(false, message)
      end
    end
  end
end
