require 'json'

module ActiveMerchant # :nodoc:
  module Billing # :nodoc:
    class ForteGateway < Gateway
      include Empty

      version 'v2'

      self.test_url = "https://sandbox.forte.net/api/#{fetch_version}"
      self.live_url = "https://api.forte.net/#{fetch_version}"

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

      self.homepage_url = 'https://www.forte.net'
      self.display_name = 'Forte'

      def initialize(options = {})
        requires!(options, :api_key, :secret, :location_id, :account_id)
        super
      end

      def purchase(money, payment_method, options = {})
        post = {}
        add_amount(post, money, options)
        add_service_fee(post, options)
        add_invoice(post, options)
        add_payment_method(post, payment_method, options)
        add_billing_address(post, payment_method, options)
        add_shipping_address(post, options)
        add_xdata(post, options)
        post[:action] = 'sale'

        commit(:post, post)
      end

      def authorize(money, payment_method, options = {})
        post = {}
        add_amount(post, money, options)
        add_service_fee(post, options)
        add_invoice(post, options)
        add_payment_method(post, payment_method, options)
        add_billing_address(post, payment_method, options)
        add_shipping_address(post, options)
        add_xdata(post, options)
        post[:action] = 'authorize'

        commit(:post, post)
      end

      def capture(money, authorization, options = {})
        post = {}
        post[:transaction_id] = transaction_id_from(authorization)
        post[:authorization_code] = authorization_code_from(authorization) || ''
        post[:action] = 'capture'

        commit(:put, post)
      end

      def credit(money, payment_method, options = {})
        post = {}
        add_amount(post, money, options)
        add_invoice(post, options)
        add_payment_method(post, payment_method, options)
        add_billing_address(post, payment_method, options)
        post[:action] = 'disburse'

        commit(:post, post)
      end

      def refund(money, authorization, options = {})
        post = {}
        add_amount(post, money, options)
        post[:original_transaction_id] = transaction_id_from(authorization)
        post[:authorization_code] = authorization_code_from(authorization)
        post[:action] = 'reverse'

        commit(:post, post)
      end

      def void(authorization, options = {})
        post = {}
        post[:transaction_id] = transaction_id_from(authorization)
        post[:authorization_code] = authorization_code_from(authorization)
        post[:action] = 'void'

        commit(:put, post)
      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((account_number)\W+\d+), '\1[FILTERED]').
          gsub(%r((card_verification_value)\W+\d+), '\1[FILTERED]')
      end

      private

      def add_auth(post)
        post[:account_id] = "act_#{@options[:account_id]}"
        post[:location_id] = "loc_#{@options[:location_id]}"
      end

      def add_invoice(post, options)
        post[:order_number] = options[:order_id]
      end

      def add_amount(post, money, options)
        post[:authorization_amount] = amount(money)
      end

      def add_service_fee(post, options)
        post[:service_fee_amount] = options[:service_fee_amount] if options[:service_fee_amount]
      end

      def add_xdata(post, options)
        post[:xdata] = {}
        if xdata = options[:xdata]
          (1..9).each do |n|
            field = "xdata_#{n}".to_sym
            post[:xdata][field] = xdata[field] if xdata[field]
          end
        end
      end

      def add_billing_address(post, payment, options)
        post[:billing_address] = {}
        if address = options[:billing_address] || options[:address]
          first_name, last_name = split_names(address[:name])
          post[:billing_address][:first_name] = first_name if first_name
          post[:billing_address][:last_name] = last_name if last_name
          post[:billing_address][:physical_address] = {}
          post[:billing_address][:physical_address][:street_line1] = address[:address1] if address[:address1]
          post[:billing_address][:physical_address][:street_line2] = address[:address2] if address[:address2]
          post[:billing_address][:physical_address][:postal_code] = address[:zip] if address[:zip]
          post[:billing_address][:physical_address][:region] = address[:state] if address[:state]
          post[:billing_address][:physical_address][:locality] = address[:city] if address[:city]
        end

        post[:billing_address][:first_name] = payment.first_name if empty?(post[:billing_address][:first_name]) && payment.first_name

        post[:billing_address][:last_name] = payment.last_name if empty?(post[:billing_address][:last_name]) && payment.last_name
      end

      def add_shipping_address(post, options)
        return unless options[:shipping_address]

        address = options[:shipping_address]

        post[:shipping_address] = {}
        first_name, last_name = split_names(address[:name])
        post[:shipping_address][:first_name] = first_name if first_name
        post[:shipping_address][:last_name] = last_name if last_name
        post[:shipping_address][:physical_address][:street_line1] = address[:address1] if address[:address1]
        post[:shipping_address][:physical_address][:street_line2] = address[:address2] if address[:address2]
        post[:shipping_address][:physical_address][:postal_code] = address[:zip] if address[:zip]
        post[:shipping_address][:physical_address][:region] = address[:state] if address[:state]
        post[:shipping_address][:physical_address][:locality] = address[:city] if address[:city]
      end

      def add_payment_method(post, payment_method, options)
        if payment_method.respond_to?(:brand)
          add_credit_card(post, payment_method)
        else
          add_echeck(post, payment_method, options)
        end
      end

      def add_echeck(post, payment, options)
        post[:echeck] = {}
        post[:echeck][:account_holder] = payment.name
        post[:echeck][:account_number] = payment.account_number
        post[:echeck][:routing_number] = payment.routing_number
        post[:echeck][:account_type] = payment.account_type
        post[:echeck][:check_number] = payment.number
        post[:echeck][:sec_code] = options[:sec_code] || 'PPD'
      end

      def add_credit_card(post, payment)
        post[:card] = {}
        post[:card][:card_type] = format_card_brand(payment.brand)
        post[:card][:name_on_card] = payment.name
        post[:card][:account_number] = payment.number
        post[:card][:expire_month] = payment.month
        post[:card][:expire_year] = payment.year
        post[:card][:card_verification_value] = payment.verification_value
      end

      def commit(type, parameters)
        add_auth(parameters)

        url = (test? ? test_url : live_url)
        response = parse(handle_resp(raw_ssl_request(type, url + endpoint, parameters.to_json, headers)))

        Response.new(
          success_from(response),
          message_from(response),
          response,
          authorization: authorization_from(response, parameters),
          avs_result: AVSResult.new(code: response['response']['avs_result']),
          cvv_result: CVVResult.new(response['response']['cvv_code']),
          test: test?
        )
      end

      def handle_resp(response)
        case response.code.to_i
        when 200..499
          response.body
        else
          raise ResponseError.new(response)
        end
      end

      def parse(response_body)
        JSON.parse(response_body)
      end

      def success_from(response)
        response['response']['response_code'] == 'A01'
      end

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

      def authorization_from(response, parameters)
        if parameters[:action] == 'capture'
          [response['transaction_id'], response.dig('response', 'authorization_code'), parameters[:transaction_id], parameters[:authorization_code]].join('#')
        else
          [response['transaction_id'], response.dig('response', 'authorization_code')].join('#')
        end
      end

      def endpoint
        "/accounts/act_#{@options[:account_id].strip}/locations/loc_#{@options[:location_id].strip}/transactions/"
      end

      def headers
        {
          'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:api_key]}:#{@options[:secret]}")),
          'X-Forte-Auth-Account-Id' => "act_#{@options[:account_id]}",
          'Content-Type' => 'application/json'
        }
      end

      def format_card_brand(card_brand)
        case card_brand
        when 'visa'
          return 'visa'
        when 'master'
          return 'mast'
        when 'american_express'
          return 'amex'
        when 'discover'
          return 'disc'
        end
      end

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

      def authorization_code_from(authorization)
        _, authorization_code, _, original_auth_authorization_code = split_authorization(authorization)
        original_auth_authorization_code.present? ? original_auth_authorization_code : authorization_code
      end

      def transaction_id_from(authorization)
        transaction_id, _, original_auth_transaction_id, = split_authorization(authorization)
        original_auth_transaction_id.present? ? original_auth_transaction_id : transaction_id
      end
    end
  end
end
