module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class Latitude19Gateway < Gateway
      self.display_name = 'Latitude19 Gateway'
      self.homepage_url = 'http://www.l19tech.com'

      self.live_url = 'https://gateway.l19tech.com/payments/'
      self.test_url = 'https://gateway-sb.l19tech.com/payments/'

      self.supported_countries = %w[US CA]
      self.default_currency = 'USD'
      self.money_format = :dollars
      self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]

      RESPONSE_CODE_MAPPING = {
        '100' => 'Approved',
        '101' => 'Local duplicate detected',
        '102' => 'Accepted local capture with no match',
        '103' => 'Auth succeeded but capture failed',
        '104' => 'Auth succeeded but failed to save info',
        '200' => STANDARD_ERROR_CODE[:card_declined],
        '300' => 'Processor reject',
        '301' => 'Local reject on user/password',
        '302' => 'Local reject',
        '303' => 'Processor unknown response',
        '304' => 'Error parsing processor response',
        '305' => 'Processor auth succeeded but settle failed',
        '306' => 'Processor auth succeeded settle status unknown',
        '307' => 'Processor settle status unknown',
        '308' => 'Processor duplicate',
        '400' => 'Not submitted',
        '401' => 'Terminated before request submitted',
        '402' => 'Local server busy',
        '500' => 'Submitted not returned',
        '501' => 'Terminated before response returned',
        '502' => 'Processor returned timeout status',
        '600' => 'Failed local capture with no match',
        '601' => 'Failed local capture',
        '700' => 'Failed local void (not in capture file)',
        '701' => 'Failed local void',
        '800' => 'Failed local refund (not authorized)',
        '801' => 'Failed local refund'
      }

      BRAND_MAP = {
        'master' => 'MC',
        'visa' => 'VI',
        'american_express' => 'AX',
        'discover' => 'DS',
        'diners_club' => 'DC',
        'jcb' => 'JC'
      }

      def initialize(options = {})
        requires!(options, :account_number, :configuration_id, :secret)
        super
      end

      def purchase(amount, payment_method, options = {})
        if payment_method.is_a?(String)
          auth_or_sale('sale', payment_method, amount, nil, options)
        else
          MultiResponse.run() do |r|
            r.process { get_session(options) }
            r.process { get_token(r.authorization, payment_method, options) }
            r.process { auth_or_sale('sale', r.authorization, amount, payment_method, options) }
          end
        end
      end

      def authorize(amount, payment_method, options = {})
        if payment_method.is_a?(String)
          auth_or_sale('auth', payment_method, amount, nil, options)
        else
          MultiResponse.run() do |r|
            r.process { get_session(options) }
            r.process { get_token(r.authorization, payment_method, options) }
            r.process { auth_or_sale('auth', r.authorization, amount, payment_method, options) }
          end
        end
      end

      def capture(amount, authorization, options = {})
        post = {}
        post[:method] = 'deposit'
        add_request_id(post)

        params = {}

        _, params[:pgwTID] = split_authorization(authorization)

        add_invoice(params, amount, options)
        add_credentials(params, post[:method])

        post[:params] = [params]
        commit('v1/', post)
      end

      def void(authorization, options = {})
        method, pgwTID = split_authorization(authorization)
        case method
        when 'auth'
          reverse_or_void('reversal', pgwTID, options)
        when 'deposit', 'sale'
          reverse_or_void('void', pgwTID, options)
        else
          message = 'Unsupported operation: successful Purchase, Authorize and unsettled Capture transactions can only be voided.'
          return Response.new(false, message)
        end
      end

      def credit(amount, payment_method, options = {})
        if payment_method.is_a?(String)
          refundWithCard(payment_method, amount, nil, options)
        else
          MultiResponse.run() do |r|
            r.process { get_session(options) }
            r.process { get_token(r.authorization, payment_method, options) }
            r.process { refundWithCard(r.authorization, amount, payment_method, options) }
          end
        end
      end

      def verify(payment_method, options = {}, action = nil)
        if payment_method.is_a?(String)
          verifyOnly(action, payment_method, nil, options)
        else
          MultiResponse.run() do |r|
            r.process { get_session(options) }
            r.process { get_token(r.authorization, payment_method, options) }
            r.process { verifyOnly(action, r.authorization, payment_method, options) }
          end
        end
      end

      def store(payment_method, options = {})
        verify(payment_method, options, 'store')
      end

      def supports_scrubbing?
        true
      end

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

      private

      def add_request_id(post)
        post[:id] = SecureRandom.hex(16)
      end

      def add_timestamp
        Time.now.getutc.strftime('%Y%m%d%H%M%S')
      end

      def add_hmac(params, method)
        if method == 'getSession'
          hmac_message = params[:pgwAccountNumber] + '|' + params[:pgwConfigurationId] + '|' + params[:requestTimeStamp] + '|' + method
        else
          hmac_message = params[:pgwAccountNumber] + '|' + params[:pgwConfigurationId] + '|' + (params[:orderNumber] || '') + '|' + method + '|' + (params[:amount] || '') + '|' + (params[:sessionToken] || '') + '|' + (params[:accountToken] || '')
        end

        OpenSSL::HMAC.hexdigest('sha512', @options[:secret], hmac_message)
      end

      def add_credentials(params, method)
        params[:pgwAccountNumber] = @options[:account_number]
        params[:pgwConfigurationId] = @options[:configuration_id]

        params[:requestTimeStamp] = add_timestamp() if method == 'getSession'

        params[:pgwHMAC] = add_hmac(params, method)
      end

      def add_invoice(params, money, options)
        params[:amount] = amount(money)
        params[:orderNumber] = options[:order_id]
        params[:transactionClass] = options[:transaction_class] || 'eCommerce'
      end

      def add_payment_method(params, credit_card)
        params[:cardExp] = format(credit_card.month, :two_digits).to_s + '/' + format(credit_card.year, :two_digits).to_s
        params[:cardType] = BRAND_MAP[credit_card.brand.to_s]
        params[:cvv] = credit_card.verification_value
        params[:firstName] = credit_card.first_name
        params[:lastName] = credit_card.last_name
      end

      def add_customer_data(params, options)
        if (billing_address = options[:billing_address] || options[:address])
          params[:address1] = billing_address[:address1]
          params[:address2] = billing_address[:address2]
          params[:city] = billing_address[:city]
          params[:stateProvince] = billing_address[:state]
          params[:zipPostalCode] = billing_address[:zip]
          params[:countryCode] = billing_address[:country]
        end
      end

      def get_session(options = {})
        post = {}
        post[:method] = 'getSession'
        add_request_id(post)

        params = {}
        add_credentials(params, post[:method])

        post[:params] = [params]
        commit('session', post)
      end

      def get_token(authorization, payment_method, options = {})
        post = {}
        post[:method] = 'tokenize'
        add_request_id(post)

        params = {}
        _, params[:sessionId] = split_authorization(authorization)
        params[:cardNumber] = payment_method.number

        post[:params] = [params]
        commit('token', post)
      end

      def auth_or_sale(method, authorization, amount, credit_card, options = {})
        post = {}
        post[:method] = method
        add_request_id(post)

        params = {}
        if credit_card
          _, params[:sessionToken] = split_authorization(authorization)
          add_payment_method(params, credit_card)
          add_customer_data(params, options)
        else
          _, params[:accountToken] = split_authorization(authorization)
        end
        add_invoice(params, amount, options)
        add_credentials(params, post[:method])

        post[:params] = [params]
        commit('v1/', post)
      end

      def verifyOnly(action, authorization, credit_card, options = {})
        post = {}
        post[:method] = 'verifyOnly'
        add_request_id(post)

        params = {}
        if credit_card
          _, params[:sessionToken] = split_authorization(authorization)
          add_payment_method(params, credit_card)
          add_customer_data(params, options)
        else
          _, params[:accountToken] = split_authorization(authorization)
        end
        params[:requestAccountToken] = '1' if action == 'store'
        add_invoice(params, 0, options)
        add_credentials(params, post[:method])

        post[:params] = [params]
        commit('v1/', post)
      end

      def refundWithCard(authorization, amount, credit_card, options = {})
        post = {}
        post[:method] = 'refundWithCard'
        add_request_id(post)

        params = {}
        if credit_card
          _, params[:sessionToken] = split_authorization(authorization)
          add_payment_method(params, credit_card)
        else
          _, params[:accountToken] = split_authorization(authorization)
        end
        add_invoice(params, amount, options)
        add_credentials(params, post[:method])

        post[:params] = [params]
        commit('v1/', post)
      end

      def reverse_or_void(method, pgwTID, options = {})
        post = {}
        post[:method] = method
        add_request_id(post)

        params = {}
        params[:orderNumber] = options[:order_id]
        params[:pgwTID] = pgwTID
        add_credentials(params, post[:method])

        post[:params] = [params]
        commit('v1/', post)
      end

      def commit(endpoint, post)
        raw_response = ssl_post(url() + endpoint, post_data(post), headers)
        response = parse(raw_response)
      rescue ResponseError => e
        raw_response = e.response.body
        response_error(raw_response)
      rescue JSON::ParserError
        unparsable_response(raw_response)
      else
        success = success_from(response)
        Response.new(
          success,
          message_from(response),
          response,
          authorization: success ? authorization_from(response, post[:method]) : nil,
          avs_result: success ? avs_from(response) : nil,
          cvv_result: success ? cvv_from(response) : nil,
          error_code: success ? nil : error_from(response),
          test: test?
        )
      end

      def headers
        {
          'Content-Type' => 'application/json'
        }
      end

      def post_data(params)
        params.to_json
      end

      def url
        test? ? test_url : live_url
      end

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

      def success_from(response)
        return false if response['result'].nil? || response['error']

        if response['result'].key?('pgwResponseCode')
          response['error'].nil? && response['result']['lastActionSucceeded'] == 1 && response['result']['pgwResponseCode'] == '100'
        else
          response['error'].nil? && response['result']['lastActionSucceeded'] == 1
        end
      end

      def message_from(response)
        return response['error'] if response['error']
        return 'Failed' unless response.key?('result')

        if response['result'].key?('pgwResponseCode')
          RESPONSE_CODE_MAPPING[response['result']['pgwResponseCode']] || response['result']['responseText']
        else
          response['result']['lastActionSucceeded'] == 1 ? 'Succeeded' : 'Failed'
        end
      end

      def error_from(response)
        return response['error'] if response['error']
        return 'Failed' unless response.key?('result')

        return response['result']['pgwResponseCode'] || response['result']['processor']['responseCode'] || 'Failed'
      end

      def authorization_from(response, method)
        method + '|' + (
          response['result']['sessionId'] ||
          response['result']['sessionToken'] ||
          response['result']['pgwTID'] ||
          response['result']['accountToken']
        )
      end

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

      def avs_from(response)
        response['result'].key?('avsResponse') ? AVSResult.new(code: response['result']['avsResponse']) : nil
      end

      def cvv_from(response)
        response['result'].key?('cvvResponse') ? CVVResult.new(response['result']['cvvResponse']) : nil
      end

      def response_error(raw_response)
        response = parse(raw_response)
      rescue JSON::ParserError
        unparsable_response(raw_response)
      else
        return Response.new(
          false,
          message_from(response),
          response,
          test: test?
        )
      end

      def unparsable_response(raw_response)
        message = 'Invalid JSON response received from Latitude19Gateway. Please contact Latitude19Gateway 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
