require 'active_merchant/billing/gateways/cecabank/cecabank_common'

module ActiveMerchant
  module Billing
    class CecabankJsonGateway < Gateway
      include CecabankCommon

      CECA_ACTIONS_DICTIONARY = {
        purchase: :REST_AUTORIZACION,
        authorize: :REST_PREAUTORIZACION,
        capture: :REST_COBRO_PREAUTORIZACION,
        refund: :REST_DEVOLUCION,
        void: :REST_ANULACION
      }.freeze

      CECA_REASON_TYPES = {
        installment: :I,
        recurring: :R,
        unscheduled: :C
      }.freeze

      CECA_INITIATOR = {
        merchant: :N,
        cardholder: :S
      }.freeze

      CECA_SCA_TYPES = {
        low_value_exemption: :LOW,
        transaction_risk_analysis_exemption: :TRA
      }.freeze

      WALLET_PAYMENT_METHODS = {
        apple_pay: 'A',
        google_pay: 'G'
      }

      self.test_url = 'https://tpv.ceca.es/tpvweb/rest/procesos/'
      self.live_url = 'https://pgw.ceca.es/tpvweb/rest/procesos/'

      def authorize(money, creditcard, options = {})
        handle_purchase(:authorize, money, creditcard, options)
      end

      def capture(money, identification, options = {})
        authorization, operation_number, _money = identification.split('#')

        post = {}
        options[:operation_number] = operation_number
        add_auth_invoice_data(:capture, post, money, authorization, options)

        commit('compra', post)
      end

      def purchase(money, creditcard, options = {})
        handle_purchase(:purchase, money, creditcard, options)
      end

      def void(identification, options = {})
        authorization, operation_number, money = identification.split('#')
        options[:operation_number] = operation_number
        handle_cancellation(:void, money.to_i, authorization, options)
      end

      def refund(money, identification, options = {})
        authorization, operation_number, _money = identification.split('#')
        options[:operation_number] = operation_number
        handle_cancellation(:refund, money, authorization, options)
      end

      def scrub(transcript)
        return '' if transcript.blank?

        before_message = transcript.gsub(%r(\\\")i, "'").scan(/{[^>]*}/).first.gsub("'", '"')
        request_data = JSON.parse(before_message)

        if @options[:encryption_key]
          params = parse(request_data['parametros'])
          sensitive_fields = decrypt_sensitive_fields(params['encryptedData'])
          filtered_params = filter_params(sensitive_fields)
          params['encryptedData'] = encrypt_sensitive_fields(filtered_params)
        else
          params = filter_params(decode_params(request_data['parametros']))
        end

        request_data['parametros'] = encode_params(params)
        before_message = before_message.gsub(%r(\")i, '\\\"')
        after_message = request_data.to_json.gsub(%r(\")i, '\\\"')
        transcript.sub(before_message, after_message)
      end

      private

      def filter_params(params)
        params.
          gsub(%r(("pan\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("caducidad\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("cvv2\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("csc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("authentication_value\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]')
      end

      def decrypt_sensitive_fields(data)
        cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
        cipher.key = [@options[:encryption_key]].pack('H*')
        cipher.iv = @options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*')
        cipher.update([data].pack('H*')) + cipher.final
      end

      def encrypt_sensitive_fields(data)
        cipher = OpenSSL::Cipher.new('AES-256-CBC').encrypt
        cipher.key = [@options[:encryption_key]].pack('H*')
        cipher.iv = @options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*')
        encrypted = cipher.update(data.to_json) + cipher.final
        encrypted.unpack1('H*')
      end

      def handle_purchase(action, money, creditcard, options)
        post = { parametros: { accion: CECA_ACTIONS_DICTIONARY[action] } }

        add_invoice(post, money, options)
        add_payment_method(post, creditcard, options)
        add_stored_credentials(post, creditcard, options)
        add_three_d_secure(post, options)

        commit('compra', post)
      end

      def handle_cancellation(action, money, authorization, options = {})
        post = {}
        add_auth_invoice_data(action, post, money, authorization, options)

        commit('anulacion', post)
      end

      def add_auth_invoice_data(action, post, money, authorization, options)
        params = post[:parametros] ||= {}
        params[:accion] = CECA_ACTIONS_DICTIONARY[action]
        params[:referencia] = authorization

        add_invoice(post, money, options)
      end

      def add_encryption(post)
        post[:cifrado] = CECA_ENCRIPTION
        post[:parametros][:encryptedData] = encrypt_sensitive_fields(post[:parametros][:encryptedData]) if @options[:encryption_key]
      end

      def add_signature(post, params_encoded, options)
        post[:firma] = Digest::SHA2.hexdigest(@options[:cypher_key].to_s + params_encoded)
      end

      def add_merchant_data(post)
        params = post[:parametros] ||= {}

        params[:merchantID] = @options[:merchant_id]
        params[:acquirerBIN] = @options[:acquirer_bin]
        params[:terminalID] = @options[:terminal_id]
      end

      def add_invoice(post, money, options)
        post[:parametros][:numOperacion] = options[:operation_number] || options[:order_id]
        post[:parametros][:importe] = amount(money)
        post[:parametros][:tipoMoneda] = CECA_CURRENCIES_DICTIONARY[options[:currency] || currency(money)].to_s
        post[:parametros][:exponente] = 2.to_s
      end

      def add_payment_method(post, payment_method, options)
        params = post[:parametros] ||= {}
        three_d_secure = options.fetch(:three_d_secure, {})

        pm = {
          pan: payment_method.number,
          caducidad: strftime_yyyymm(payment_method)
        }

        if payment_method.is_a?(NetworkTokenizationCreditCard) && WALLET_PAYMENT_METHODS[payment_method.source.to_sym]
          eci_to_format = payment_method.eci || three_d_secure[:eci]
          eci_value = eci_to_format.to_s.rjust(2, '0') unless eci_to_format.nil?

          pm[:wallet] = {

            # the authentication value should come nil (for recurring cases) or should I remove it?
            authentication_value: (payment_method.payment_cryptogram unless options.dig(:stored_credential, :network_transaction_id)),
            xid:  three_d_secure[:xid] || three_d_secure[:ds_transaction_id] || options[:xid],
            walletType: WALLET_PAYMENT_METHODS[payment_method.source.to_sym],
            eci: eci_value
          }.compact.to_json
        else
          pm[CreditCard.brand?(payment_method.number) == 'american_express' ? :csc : :cvv2] = payment_method.verification_value
        end
        @options[:encryption_key] ? params[:encryptedData] = pm : params.merge!(pm)
      end

      def add_stored_credentials(post, creditcard, options)
        return unless stored_credential = options[:stored_credential]

        return if options[:exemption_type].blank? && !(stored_credential[:reason_type] && stored_credential[:initiator])

        params = post[:parametros] ||= {}
        params[:exencionSCA] = 'MIT'

        requires!(stored_credential, :reason_type, :initiator)
        reason_type = CECA_REASON_TYPES[stored_credential[:reason_type].to_sym]
        initiator = CECA_INITIATOR[stored_credential[:initiator].to_sym]
        params[:tipoCOF] = reason_type
        params[:inicioRec] = initiator
        if initiator == :S
          requires!(options, :recurring_frequency)
          params[:finRec] = options[:recurring_end_date] || strftime_yyyymmdd_last_day(creditcard)
          params[:frecRec] = options[:recurring_frequency]
        end

        network_transaction_id = options[:network_transaction_id].present? ? options[:network_transaction_id] : stored_credential[:network_transaction_id]
        params[:mmppTxId] = network_transaction_id unless network_transaction_id.blank?
      end

      def add_three_d_secure(post, options)
        params = post[:parametros] ||= {}
        return unless three_d_secure = options[:three_d_secure]

        params[:exencionSCA] ||= CECA_SCA_TYPES.fetch(options[:exemption_type]&.to_sym, :NONE)

        three_d_response = {
          exemption_type: options[:exemption_type],
          three_ds_version: three_d_secure[:version],
          directory_server_transaction_id: three_d_secure[:ds_transaction_id],
          acs_transaction_id: three_d_secure[:acs_transaction_id],
          authentication_response_status: three_d_secure[:authentication_response_status],
          three_ds_server_trans_id: three_d_secure[:three_ds_server_trans_id],
          ecommerce_indicator: three_d_secure[:eci],
          enrolled: three_d_secure[:enrolled]
        }

        if @options[:encryption_key]
          params[:encryptedData].merge!({ authentication_value: three_d_secure[:cavv] })
        else
          three_d_response[:authentication_value] = three_d_secure[:cavv]
        end

        three_d_response[:amount] = post[:parametros][:importe]
        params[:ThreeDsResponse] = three_d_response.to_json
      end

      def commit(action, post)
        auth_options = {
          operation_number: post.dig(:parametros, :numOperacion),
          amount: post.dig(:parametros, :importe)
        }

        add_encryption(post)
        add_merchant_data(post)
        params_encoded = encode_post_parameters(post)
        add_signature(post, params_encoded, options)

        response = parse(ssl_post(url(action), post.to_json, headers))
        response[:parametros] = parse(response[:parametros]) if response[:parametros]

        Response.new(
          success_from(response),
          message_from(response),
          response,
          authorization: authorization_from(response, auth_options),
          network_transaction_id: network_transaction_id_from(response),
          test: test?,
          error_code: error_code_from(response)
        )
      end

      def url(action)
        (test? ? self.test_url : self.live_url) + action
      end

      def host
        URI.parse(url('')).host
      end

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

      def parse(string)
        JSON.parse(string).with_indifferent_access
      rescue JSON::ParserError
        parse(decode_params(string))
      end

      def encode_post_parameters(post)
        post[:parametros] = encode_params(post[:parametros])
      end

      def encode_params(params)
        Base64.strict_encode64(params.is_a?(Hash) ? params.to_json : params)
      end

      def decode_params(params)
        Base64.decode64(params)
      end

      def success_from(response)
        response[:codResult].blank?
      end

      def message_from(response)
        return response[:parametros].to_json if success_from(response)

        response[:paramsEntradaError] || response[:idProceso]
      end

      def authorization_from(response, auth_options = {})
        return unless response[:parametros]

        [
          response[:parametros][:referencia],
          auth_options[:operation_number],
          auth_options[:amount]
        ].join('#')
      end

      def network_transaction_id_from(response)
        response.dig(:parametros, :mmppTxId)
      end

      def error_code_from(response)
        (response[:codResult] || :paramsEntradaError) unless success_from(response)
      end
    end
  end
end
