module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class SimetrikGateway < Gateway
      self.test_url = 'https://payments.sta.simetrik.com/v1'
      self.live_url = 'https://payments.simetrik.com/v1'

      class_attribute :test_auth_url, :live_auth_url, :test_audience, :live_audience
      self.test_auth_url = 'https://tenant-payments-dev.us.auth0.com/oauth/token'
      self.live_auth_url = 'https://tenant-payments-prod.us.auth0.com/oauth/token'

      self.test_audience = 'https://tenant-payments-dev.us.auth0.com/api/v2/'
      self.live_audience = 'https://tenant-payments-prod.us.auth0.com/api/v2/'

      self.supported_countries = %w(PE AR)
      self.default_currency = 'USD'
      self.supported_cardtypes = %i[visa master american_express discover]

      self.homepage_url = 'https://www.simetrik.com'
      self.display_name = 'Simetrik'

      STANDARD_ERROR_CODE_MAPPING = {
        'R101' => STANDARD_ERROR_CODE[:incorrect_number],
        'R102' => STANDARD_ERROR_CODE[:invalid_number],
        'R103' => STANDARD_ERROR_CODE[:invalid_expiry_date],
        'R104' => STANDARD_ERROR_CODE[:invalid_cvc],
        'R105' => STANDARD_ERROR_CODE[:expired_card],
        'R106' => STANDARD_ERROR_CODE[:incorrect_cvc],
        'R107' => STANDARD_ERROR_CODE[:incorrect_pin],
        'R201' => STANDARD_ERROR_CODE[:incorrect_zip],
        'R202' => STANDARD_ERROR_CODE[:incorrect_address],
        'R301' => STANDARD_ERROR_CODE[:card_declined],
        'R302' => STANDARD_ERROR_CODE[:processing_error],
        'R303' => STANDARD_ERROR_CODE[:call_issuer],
        'R304' => STANDARD_ERROR_CODE[:pick_up_card],
        'R305' => STANDARD_ERROR_CODE[:processing_error],
        'R306' => STANDARD_ERROR_CODE[:processing_error],
        'R307' => STANDARD_ERROR_CODE[:processing_error],
        'R401' => STANDARD_ERROR_CODE[:config_error],
        'R402' => STANDARD_ERROR_CODE[:test_mode_live_card],
        'R403' => STANDARD_ERROR_CODE[:unsupported_feature]

      }

      def initialize(options = {})
        requires!(options, :client_id, :client_secret)
        super
        @access_token = options[:access_token] || {}
        sign_access_token()
      end

      def authorize(money, payment, options = {})
        requires!(options, :token_acquirer)

        post = {}
        add_forward_route(post, options)
        add_forward_payload(post, money, payment, options)
        add_stored_credential(post, options)
        commit('authorize', post, { token_acquirer: options[:token_acquirer] })
      end

      def capture(money, authorization, options = {})
        requires!(options, :token_acquirer)
        post = {
          forward_payload: {
            amount: {
              total_amount: amount(money).to_f,
              currency: (options[:currency] || currency(money))
            },
            transaction: {
              id: authorization
            },
            acquire_extra_options: options[:acquire_extra_options] || {}
          }
        }
        post[:forward_payload][:amount][:vat] = options[:vat].to_f / 100 if options[:vat]

        add_forward_route(post, options)
        commit('capture', post, { token_acquirer: options[:token_acquirer] })
      end

      def refund(money, authorization, options = {})
        requires!(options, :token_acquirer)
        post = {
          forward_payload: {
            amount: {
              total_amount: amount(money).to_f,
              currency: (options[:currency] || currency(money))
            },
            transaction: {
              id: authorization
            },
            acquire_extra_options: options[:acquire_extra_options] || {}
          }
        }
        post[:forward_payload][:transaction][:comment] = options[:comment] if options[:comment]

        add_forward_route(post, options)
        commit('refund', post, { token_acquirer: options[:token_acquirer] })
      end

      def void(authorization, options = {})
        requires!(options, :token_acquirer)
        post = {
          forward_payload: {
            transaction: {
              id: authorization
            },
            acquire_extra_options: options[:acquire_extra_options] || {}
          }
        }
        add_forward_route(post, options)
        commit('void', post, { token_acquirer: options[:token_acquirer] })
      end

      def purchase(money, payment, options = {})
        requires!(options, :token_acquirer)

        post = {}
        add_forward_route(post, options)
        add_forward_payload(post, money, payment, options)

        add_stored_credential(post, options)
        commit('charge', post, { token_acquirer: options[:token_acquirer] })
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Bearer ).+), '\1[FILTERED]').
          gsub(%r(("client_secret\\?":\\?")\w+), '\1[FILTERED]').
          gsub(%r(("number\\?":\\?")\d+), '\1[FILTERED]').
          gsub(%r(("security_code\\?":\\?")\d+), '\1[FILTERED]')
      end

      private

      def add_forward_route(post, options)
        forward_route = {}
        forward_route[:trace_id] = options[:trace_id] if options[:trace_id]

        forward_route[:psp_extra_fields] = options[:psp_extra_fields] || {}
        post[:forward_route] = forward_route
      end

      def add_forward_payload(post, money, payment, options)
        forward_payload = {}
        add_user(forward_payload, options[:user]) if options[:user]
        add_order(forward_payload, money, options)
        add_payment_method(forward_payload, payment, options[:payment_method]) if options[:payment_method] || payment

        forward_payload[:payment_method] = {} unless forward_payload[:payment_method]
        forward_payload[:payment_method][:card] = {} unless forward_payload[:payment_method][:card]
        add_address('billing_address', forward_payload[:payment_method][:card], options[:billing_address]) if options[:billing_address]

        add_three_ds_fields(forward_payload[:authentication] = {}, options[:three_ds_fields]) if options[:three_ds_fields]
        add_sub_merchant(forward_payload, options[:sub_merchant]) if options[:sub_merchant]
        forward_payload[:acquire_extra_options] = options[:acquire_extra_options] || {}
        post[:forward_payload] = forward_payload
      end

      def add_sub_merchant(post, sub_merchant_options)
        sub_merchant = {}
        sub_merchant[:merchant_id] = sub_merchant_options[:merchant_id] if sub_merchant_options[:merchant_id]
        sub_merchant[:extra_params] = sub_merchant_options[:extra_params] if sub_merchant_options[:extra_params]
        sub_merchant[:mcc] =  sub_merchant_options[:mcc] if sub_merchant_options[:mcc]
        sub_merchant[:name] = sub_merchant_options[:name] if sub_merchant_options[:name]
        sub_merchant[:address] = sub_merchant_options[:address] if sub_merchant_options[:address]
        sub_merchant[:postal_code] = sub_merchant_options[:postal_code] if sub_merchant_options[:postal_code]
        sub_merchant[:url] = sub_merchant_options[:url] if sub_merchant_options[:url]
        sub_merchant[:phone_number] = sub_merchant_options[:phone_number] if sub_merchant_options[:phone_number]

        post[:sub_merchant] = sub_merchant
      end

      def add_payment_method(post, payment, payment_method_options)
        payment_method = {}
        opts = nil
        opts = payment_method_options[:card] if payment_method_options
        add_card(payment_method, payment, opts) if opts || payment

        post[:payment_method] = payment_method
      end

      def add_three_ds_fields(post, three_ds_options)
        three_ds = {}
        three_ds[:version] = three_ds_options[:version] if three_ds_options[:version]
        three_ds[:eci] = three_ds_options[:eci] if three_ds_options[:eci]
        three_ds[:cavv] = three_ds_options[:cavv] if three_ds_options[:cavv]
        three_ds[:ds_transaction_id] = three_ds_options[:ds_transaction_id] if three_ds_options[:ds_transaction_id]
        three_ds[:acs_transaction_id] = three_ds_options[:acs_transaction_id] if three_ds_options[:acs_transaction_id]
        three_ds[:xid] = three_ds_options[:xid] if three_ds_options[:xid]
        three_ds[:enrolled] = three_ds_options[:enrolled] if three_ds_options[:enrolled]
        three_ds[:cavv_algorithm] = three_ds_options[:cavv_algorithm] if three_ds_options[:cavv_algorithm]
        three_ds[:directory_response_status] = three_ds_options[:directory_response_status] if three_ds_options[:directory_response_status]
        three_ds[:authentication_response_status] = three_ds_options[:authentication_response_status] if three_ds_options[:authentication_response_status]
        three_ds[:three_ds_server_trans_id] = three_ds_options[:three_ds_server_trans_id] if three_ds_options[:three_ds_server_trans_id]

        post[:three_ds_fields] = three_ds
      end

      def add_card(post, card, card_options = {})
        card_hash = {}
        card_hash[:number] = card.number
        card_hash[:exp_month] = card.month
        card_hash[:exp_year] = card.year
        card_hash[:security_code] = card.verification_value
        card_hash[:type] = card.brand
        card_hash[:holder_first_name] = card.first_name
        card_hash[:holder_last_name] = card.last_name
        post[:card] = card_hash
      end

      def add_user(post, user_options)
        user = {}
        user[:id] = user_options[:id] if user_options[:id]
        user[:email] = user_options[:email] if user_options[:email]

        post[:user] = user
      end

      def add_stored_credential(post, options)
        return unless options[:stored_credential]

        check_initiator = %w[merchant cardholder].any? { |item| item == options[:stored_credential][:initiator] }
        check_reason_type = %w[recurring installment unscheduled].any? { |item| item == options[:stored_credential][:reason_type] }
        post[:forward_payload][:authentication] = {} unless post[:forward_payload].key?(:authentication)
        post[:forward_payload][:authentication][:stored_credential] = options[:stored_credential] if check_initiator && check_reason_type
      end

      def add_order(post, money, options)
        return unless options[:order] || money

        order = {}
        order_options = options[:order] || {}
        order[:id] = options[:order_id] if options[:order_id]
        order[:description] = options[:description] if options[:description]
        order[:installments] = order_options[:installments].to_i if order_options[:installments]
        order[:datetime_local_transaction] = order_options[:datetime_local_transaction] if order_options[:datetime_local_transaction]

        add_amount(order, money, options)
        add_address('shipping_address', order, options)

        post[:order] = order
      end

      def add_amount(post, money, options)
        amount_obj = {}
        amount_obj[:total_amount] = amount(money).to_f
        amount_obj[:currency] = (options[:currency] || currency(money))
        amount_obj[:vat] = options[:vat].to_f / 100 if options[:vat]

        post[:amount] = amount_obj
      end

      def add_address(tag, post, options)
        return unless address_options = options[:shipping_address]

        address = {}
        address[:name] = address_options[:name] if address_options[:name]
        address[:address1] = address_options[:address1] if address_options[:address1]
        address[:address2] = address_options[:address2] if address_options[:address2]
        address[:company] = address_options[:company] if address_options[:company]
        address[:city] = address_options[:city] if address_options[:city]
        address[:state] = address_options[:state] if address_options[:state]
        address[:zip] = address_options[:zip] if address_options[:zip]
        address[:country] = address_options[:country] if address_options[:country]
        address[:phone] = address_options[:phone] if address_options[:phone]
        address[:fax] = address_options[:fax] if address_options[:fax]

        post[tag] = address
      end

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

      def commit(action, parameters, url_params = {})
        begin
          response = JSON.parse ssl_post(url(action, url_params), post_data(parameters), authorized_headers())
        rescue ResponseError => e
          case e.response.code.to_i
          when 400...499
            response = JSON.parse e.response.body
          else
            raise e
          end
        end

        Response.new(
          success_from(response['code']),
          message_from(response),
          response,
          authorization: authorization_from(response),
          avs_result: AVSResult.new(code: avs_code_from(response)),
          cvv_result: CVVResult.new(cvv_code_from(response)),
          test: test?,
          error_code: error_code_from(response)
        )
      end

      def avs_code_from(response)
        response['avs_result']
      end

      def cvv_code_from(response)
        response['cvv_result']
      end

      def success_from(code)
        code == 'S001'
      end

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

      def url(action, url_params)
        "#{test? ? test_url : live_url}/#{url_params[:token_acquirer]}/#{action}"
      end

      def post_data(data = {})
        data.to_json
      end

      def authorization_from(response)
        response['simetrik_authorization_id']
      end

      def error_code_from(response)
        STANDARD_ERROR_CODE_MAPPING[response['code']] unless success_from(response['code'])
      end

      def authorized_headers
        {
          'content-Type' => 'application/json',
          'Authorization' => "Bearer #{sign_access_token()}"
        }
      end

      # if this method is refactored, ensure that the client_secret is properly scrubbed
      def sign_access_token
        fetch_access_token() if Time.new.to_i > (@access_token[:expires_at] || 0) + 10
        @access_token[:access_token]
      end

      def auth_url
        (test? ? test_auth_url : live_auth_url)
      end

      def fetch_access_token
        login_info = {}
        login_info[:client_id] = @options[:client_id]
        login_info[:client_secret] = @options[:client_secret]
        login_info[:audience] = test? ? test_audience : live_audience
        login_info[:grant_type] = 'client_credentials'

        begin
          raw_response = ssl_post(auth_url(), login_info.to_json, {
            'content-Type' => 'application/json'
          })
        rescue ResponseError => e
          raise OAuthResponseError.new(e)
        else
          response = parse(raw_response)
          @access_token[:access_token] = response['access_token']
          @access_token[:expires_at] = Time.new.to_i + response['expires_in']
        end
      end
    end
  end
end
