module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class OpenpayGateway < Gateway
      class_attribute :mx_live_url, :mx_test_url
      class_attribute :co_live_url, :co_test_url

      self.co_live_url = 'https://api.openpay.co/v1/'
      self.co_test_url = 'https://sandbox-api.openpay.co/v1/'
      self.mx_live_url = 'https://api.openpay.mx/v1/'
      self.mx_test_url = 'https://sandbox-api.openpay.mx/v1/'
      self.live_url = self.co_live_url
      self.test_url = self.co_test_url

      self.supported_countries = %w(CO MX)
      self.supported_cardtypes = %i[visa master american_express carnet]
      self.homepage_url = 'http://www.openpay.mx/'
      self.display_name = 'Openpay'
      self.default_currency = 'MXN'

      # Instantiate a instance of OpenpayGateway by passing through your
      # merchant id and private api key.
      #
      # === To obtain your own credentials
      # 1. Visit http://openpay.mx
      # 2. Sign up
      # 3. Activate your account clicking on the email confirmation
      def initialize(options = {})
        requires!(options, :key, :merchant_id)
        @api_key = options[:key]
        @merchant_id = options[:merchant_id]
        super
      end

      def gateway_url(options = {})
        country = options[:merchant_country] || @options[:merchant_country]

        if country == 'MX'
          test? ? mx_test_url : mx_live_url
        else
          test? ? co_test_url : co_live_url
        end
      end

      def purchase(money, creditcard, options = {})
        post = create_post_for_auth_or_purchase(money, creditcard, options)
        commit(:post, 'charges', post, options)
      end

      def authorize(money, creditcard, options = {})
        post = create_post_for_auth_or_purchase(money, creditcard, options)
        post[:capture] = false
        commit(:post, 'charges', post, options)
      end

      def capture(money, authorization, options = {})
        post = {}
        post[:amount] = amount(money) if money
        post[:payments] = options[:payments] if options[:payments]
        commit(:post, "charges/#{CGI.escape(authorization)}/capture", post, options)
      end

      def void(identification, options = {})
        commit(:post, "charges/#{CGI.escape(identification)}/refund", nil, options)
      end

      def refund(money, identification, options = {})
        post = {}
        post[:description] = options[:description]
        post[:amount] = amount(money)
        commit(:post, "charges/#{CGI.escape(identification)}/refund", post, options)
      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 store(creditcard, options = {})
        card_params = {}
        add_creditcard(card_params, creditcard, options)
        card = card_params[:card]

        if options[:customer].present?
          commit(:post, "customers/#{CGI.escape(options[:customer])}/cards", card, options)
        else
          requires!(options, :email, :name)
          post = {}
          post[:name] = options[:name]
          post[:email] = options[:email]
          MultiResponse.run(:first) do |r|
            r.process { commit(:post, 'customers', post, options) }

            if r.success? && !r.params['id'].blank?
              customer_id = r.params['id']
              r.process { commit(:post, "customers/#{customer_id}/cards", card, options) }
            end
          end
        end
      end

      def unstore(customer_id, card_id = nil, options = {})
        if card_id.nil?
          commit(:delete, "customers/#{CGI.escape(customer_id)}", nil, options)
        else
          commit(:delete, "customers/#{CGI.escape(customer_id)}/cards/#{CGI.escape(card_id)}", nil, options)
        end
      end

      def supports_scrubbing
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
          gsub(%r((card_number\\?":\\?")[^"\\]*)i, '\1[FILTERED]').
          gsub(%r((cvv2\\?":\\?")\d+[^"\\]*)i, '\1[FILTERED]').
          gsub(%r((cvv2\\?":)null), '\1[BLANK]').
          gsub(%r((cvv2\\?":\\?")\\?"), '\1[BLANK]"').
          gsub(%r((cvv2\\?":\\?")\s+), '\1[BLANK]')
      end

      private

      def create_post_for_auth_or_purchase(money, creditcard, options)
        post = {}
        post[:amount] = amount(money)
        post[:method] = 'card'
        post[:description] = options[:description]
        post[:order_id] = options[:order_id]
        post[:device_session_id] = options[:device_session_id]
        post[:currency] = (options[:currency] || currency(money)).upcase
        post[:use_card_points] = options[:use_card_points] if options[:use_card_points]
        post[:payment_plan] = { payments: options[:payments] } if options[:payments]
        add_creditcard(post, creditcard, options)
        post
      end

      def add_creditcard(post, creditcard, options)
        if creditcard.kind_of?(String)
          post[:source_id] = creditcard
        elsif creditcard.respond_to?(:number)
          card = {
            card_number: creditcard.number,
            expiration_month: sprintf('%02d', creditcard.month),
            expiration_year: creditcard.year.to_s[-2, 2],
            cvv2: creditcard.verification_value,
            holder_name: creditcard.name
          }
          add_address(card, options)
          add_customer_data(post, creditcard, options)
          post[:card] = card
        end
      end

      def add_customer_data(post, creditcard, options)
        if options[:email]
          customer = {
            name: creditcard.name || options[:name],
            email: options[:email]
          }
          post[:customer] = customer
        end
        post
      end

      def add_address(card, options)
        return unless card.kind_of?(Hash)

        if address = (options[:billing_address] || options[:address])
          card[:address] = {
            line1: address[:address1],
            line2: address[:address2],
            line3: address[:company],
            city: address[:city],
            postal_code: address[:zip],
            state: address[:state],
            country_code: address[:country]
          }
        end
      end

      def headers(options = {})
        {
          'Content-Type' => 'application/json',
          'Authorization' => 'Basic ' + Base64.strict_encode64(@api_key.to_s + ':').strip,
          'User-Agent' => "Openpay/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}",
          'X-Openpay-Client-User-Agent' => user_agent
        }
      end

      def parse(body)
        return {} unless body

        JSON.parse(body)
      end

      def commit(method, resource, parameters, options = {})
        response = http_request(method, resource, parameters, options)
        success = !error?(response)

        Response.new(
          success,
          (success ? response['error_code'] : response['description']),
          response,
          test: test?,
          authorization: response['id']
        )
      end

      def http_request(method, resource, parameters = {}, options = {})
        url = gateway_url(options) + @merchant_id + '/' + resource
        raw_response = nil
        begin
          raw_response = ssl_request(method, url, (parameters ? parameters.to_json : nil), headers(options))
          parse(raw_response)
        rescue ResponseError => e
          raw_response = e.response.body
          response_error(raw_response)
        rescue JSON::ParserError
          json_error(raw_response)
        end
      end

      def error?(response)
        response['error_code'] && !response['error_code'].blank?
      end

      def response_error(raw_response)
        parse(raw_response)
      rescue JSON::ParserError
        json_error(raw_response)
      end

      def json_error(raw_response)
        msg = 'Invalid response received from the Openpay API.  Please contact soporte@openpay.mx if you continue to receive this message.'
        msg += "  (The raw response returned by the API was #{raw_response.inspect})"
        {
          'category' => 'request',
          'error_code' => '9999',
          'description' => msg
        }
      end
    end
  end
end
