module ActiveMerchant # :nodoc:
  module Billing # :nodoc:
    class PlexoGateway < Gateway
      version 'v1'

      self.test_url = "https://api.testing.plexo.com.uy/#{fetch_version}/payments"
      self.live_url = "https://api.plexo.com.uy/#{fetch_version}/payments"

      self.supported_countries = ['UY']
      self.default_currency = 'UYU'
      self.supported_cardtypes = %i[visa master american_express discover passcard edenred anda tarjeta-d]

      self.homepage_url = 'https://www.plexo.com.uy'
      self.display_name = 'Plexo'

      APPENDED_URLS = %w(captures refunds cancellations verify)
      AMOUNT_IN_RESPONSE = %w(authonly purchase /verify)
      APPROVED_STATUS = %w(approved authorized)

      def initialize(options = {})
        requires!(options, :client_id, :api_key)
        @credentials = options
        super
      end

      def purchase(money, payment, options = {})
        post = {}
        build_auth_purchase_request(money, post, payment, options)

        commit('purchase', post, options)
      end

      def authorize(money, payment, options = {})
        post = {}
        build_auth_purchase_request(money, post, payment, options)
        add_capture_type(post, options)

        commit('authonly', post, options)
      end

      def capture(money, authorization, options = {})
        post = {}
        post[:ReferenceId] = options[:reference_id] || generate_unique_id
        post[:Amount] = amount(money)

        commit("/#{authorization}/captures", post, options)
      end

      def inquire(authorization, options = {})
        post = { payment_id: authorization }
        commit('status', post, options)
      end

      def refund(money, authorization, options = {})
        post = {}
        post[:ReferenceId] = options[:reference_id] || generate_unique_id
        post[:Type] = options[:refund_type] || 'refund'
        post[:Description] = options[:description]
        post[:Reason] = options[:reason]
        post[:Amount] = amount(money)

        commit("/#{authorization}/refunds", post, options)
      end

      def void(authorization, options = {})
        post = {}
        post[:ReferenceId] = options[:reference_id] || generate_unique_id
        post[:Description] = options[:description]
        post[:Reason] = options[:reason]

        commit("/#{authorization}/cancellations", post, options)
      end

      def verify(credit_card, options = {})
        post = {}
        post[:ReferenceId] = options[:reference_id] || generate_unique_id
        post[:Flow] = 'direct'
        post[:MerchantId] = options[:merchant_id] || @credentials[:merchant_id]
        post[:StatementDescriptor] = options[:statement_descriptor] if options[:statement_descriptor]
        post[:CustomerId] = options[:customer_id] if options[:customer_id]
        money = options[:verify_amount].to_i || 100

        add_payment_method(post, credit_card, options)
        add_metadata(post, options[:metadata])
        add_amount(money, post, options)
        add_browser_details(post, options)
        add_invoice_number(post, options)

        commit('/verify', post, options)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
          gsub(%r(("Number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("Cvc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("InvoiceNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("MerchantId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("Cryptogram\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]')
      end

      private

      def encoded_credentials
        Base64.encode64("#{@credentials[:client_id]}:#{@credentials[:api_key]}").delete("\n")
      end

      def build_auth_purchase_request(money, post, payment, options)
        post[:ReferenceId] = options[:reference_id] || generate_unique_id
        post[:MerchantId] = options[:merchant_id] || @credentials[:merchant_id]
        post[:Installments] = options[:installments] if options[:installments]
        post[:StatementDescriptor] = options[:statement_descriptor] if options[:statement_descriptor]
        post[:CustomerId] = options[:customer_id] if options[:customer_id]
        post[:Flow] = 'direct'

        add_payment_method(post, payment, options)
        add_items(post, options[:items])
        add_metadata(post, options[:metadata])
        add_amount(money, post, options)
        add_browser_details(post, options)
        add_invoice_number(post, options)
      end

      def header(parameters = {})
        {
          'Content-Type' => 'application/json',
          'Authorization' => "Basic #{encoded_credentials}"
        }
      end

      def add_capture_type(post, options)
        post[:Capture] = {}
        post[:Capture][:Method] = options.dig(:capture_type, :method) || 'manual'
      end

      def add_items(post, items)
        return unless items&.kind_of?(Array)

        post[:Items] = []

        items.each do |option_item|
          item = {}
          item[:ReferenceId] = option_item[:reference_id] || generate_unique_id
          item[:Name] = option_item[:name] if option_item[:name]
          item[:Description] = option_item[:description] if option_item[:description]
          item[:Quantity] = option_item[:quantity] if option_item[:quantity]
          item[:Price] = option_item[:price] if option_item[:price]
          item[:Discount] = option_item[:discount] if option_item[:discount]

          post[:Items].append(item)
        end
      end

      def add_metadata(post, metadata)
        return unless metadata&.kind_of?(Hash)

        metadata.transform_keys! { |key| key.to_s.camelize.to_sym }
        post[:Metadata] = metadata
      end

      def add_amount(money, post, amount_options)
        post[:Amount] = {}

        post[:Amount][:Currency] = amount_options[:currency] || self.default_currency
        post[:Amount][:Total] = amount(money)
        post[:Amount][:Details] = {}
        add_amount_details(post[:Amount][:Details], amount_options[:amount_details]) if amount_options[:amount_details]
      end

      def add_amount_details(amount_details, options)
        return unless options

        amount_details[:TaxedAmount] = options[:taxed_amount] if options[:taxed_amount]
        amount_details[:TipAmount] = options[:tip_amount] if options[:tip_amount]
        amount_details[:DiscountAmount] = options[:discount_amount] if options[:discount_amount]
        amount_details[:TaxableAmount] = options[:taxable_amount] if options[:taxable_amount]
        add_tax(amount_details, options[:tax])
      end

      def add_tax(post, tax)
        return unless tax

        post[:Tax] = {}
        post[:Tax][:Type] = tax[:type] if tax[:type]
        post[:Tax][:Amount] = tax[:amount] if tax[:amount]
        post[:Tax][:Rate] = tax[:rate] if tax[:rate]
      end

      def add_browser_details(post, browser_details)
        return unless browser_details

        post[:BrowserDetails] = {}
        post[:BrowserDetails][:DeviceFingerprint] = browser_details[:finger_print] if browser_details[:finger_print]
        post[:BrowserDetails][:IpAddress] = browser_details[:ip] if browser_details[:ip]
      end

      def add_invoice_number(post, options)
        post[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number]
      end

      def add_payment_method(post, payment, options)
        payment_method = build_payment_method(payment)

        if payment_method.present?
          add_card_holder(payment_method[:NetworkToken] || payment_method[:Card], payment, options)
          post[:paymentMethod] = payment_method
        end
      end

      def build_payment_method(payment)
        case payment
        when NetworkTokenizationCreditCard
          {
            source: 'network-token',
            id: payment.brand,
            NetworkToken: {
              Number: payment.number,
              ExpMonth: (format(payment.month, :two_digits) if payment.month),
              ExpYear: (format(payment.year, :two_digits) if payment.year),
              Cryptogram: payment.payment_cryptogram
            }
          }
        when CreditCard
          {
            type: 'card',
            Card: {
              Number: payment.number,
              ExpMonth: (format(payment.month, :two_digits) if payment.month),
              ExpYear: (format(payment.year, :two_digits) if payment.year),
              Cvc: payment.verification_value
            }
          }
        end
      end

      def add_card_holder(card, payment, options)
        requires!(options, :email)

        cardholder = {}
        cardholder[:FirstName] = payment.first_name if payment.first_name
        cardholder[:LastName] = payment.last_name if payment.last_name
        cardholder[:Email] = options[:email]
        cardholder[:Birthdate] = options[:cardholder_birthdate] if options[:cardholder_birthdate]
        cardholder[:Identification] = {}
        cardholder[:Identification][:Type] = options[:identification_type] if options[:identification_type]
        cardholder[:Identification][:Value] = options[:identification_value] if options[:identification_value]
        add_billing_address(cardholder, options)

        card[:Cardholder] = cardholder
      end

      def add_billing_address(cardholder, options)
        return unless address = options[:billing_address]

        cardholder[:BillingAddress] = {}
        cardholder[:BillingAddress][:City] = address[:city]
        cardholder[:BillingAddress][:Country] = address[:country]
        cardholder[:BillingAddress][:Line1] = address[:address1]
        cardholder[:BillingAddress][:Line2] = address[:address2]
        cardholder[:BillingAddress][:PostalCode] = address[:zip]
        cardholder[:BillingAddress][:State] = address[:state]
      end

      def parse(body)
        return {} if body == ''

        JSON.parse(body)
      end

      def build_url(action, options, parameters)
        base_url = test? ? test_url : live_url
        return "#{base_url}/#{parameters[:payment_id]}" if action == 'status' && parameters[:payment_id]

        url = base_url
        url += action if APPENDED_URLS.any? { |key| action.include?(key) }
        url
      end

      def get_authorization_from_url(url)
        url.split('/')[1]
      end

      def reorder_amount_fields(response)
        return response unless response['amount']

        amount_obj = response['amount']
        response['amount'] = amount_obj['total'].to_i if amount_obj['total']
        response['currency'] = amount_obj['currency'] if amount_obj['currency']
        response['amount_details'] = amount_obj['details'] if amount_obj['details']
        response
      end

      def commit(action, parameters, options = {})
        url = build_url(action, options, parameters)

        raw = make_request(action, url, parameters, options)
        response = parse(raw)

        reorder_amount_fields(response) if AMOUNT_IN_RESPONSE.include?(action)

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

      def make_request(action, url, parameters, options)
        if action == 'status'
          ssl_get(url, header(options))
        else
          ssl_post(url, parameters.to_json, header(options))
        end
      rescue ResponseError => e
        e.response.body
      end

      def handle_response(response)
        case response.code.to_i
        when 200...300, 400, 401
          response.body
        else
          raise ResponseError.new(response)
        end
      end

      def success_from(response)
        APPROVED_STATUS.include?(response['status'])
      end

      def message_from(response)
        response = response['transactions']&.first if response['transactions']&.is_a?(Array)
        response['resultMessage'] || response['message']
      end

      def authorization_from(response, action = nil)
        if action.include?('captures')
          get_authorization_from_url(action)
        else
          response['id']
        end
      end

      def error_code_from(response)
        return if success_from(response)

        response = response['transactions']&.first if response['transactions']&.is_a?(Array)
        response['resultCode'] || response['status']
      end
    end
  end
end
