module ActiveMerchant
  module Billing
    #
    # ActiveMerchant PSL Card Gateway
    #
    # Notes:
    #   -To be able to use the capture function, the IP address of the machine must be
    #    registered with PSL
    #   -ESALE_KEYED should only be used in situations where the cardholder perceives the
    #    transaction to be Internet-based, such as purchasing from a web site/on-line store.
    #    If the Internet is used purely for the transport of information from the merchant
    #    directly to the gateway then the appropriate cardholder present or not present message
    #    type should be used rather than the ‘E’ equivalent.
    #   -The CV2 / AVS policies are set up with the account settings when signing up for an account
    class PslCardGateway < Gateway
      self.money_format = :cents
      self.default_currency = 'GBP'

      self.supported_countries = ['GB']
      # Visa Credit, Visa Debit, Mastercard, Maestro, Electron,
      # American Express, Diners Club, JCB, International Maestro,
      # Style, Clydesdale Financial Services, Other

      self.supported_cardtypes = %i[visa master american_express diners_club jcb maestro]
      self.homepage_url = 'http://www.paymentsolutionsltd.com/'
      self.display_name = 'PSL Payment Solutions'

      # Default ISO 3166 country code (GB)
      cattr_accessor :location
      self.location = 826

      # PslCard server self.live_url - The url is the same whether testing or live - use
      # the test account when testing...
      self.live_url = self.test_url = 'https://pslcard3.paymentsolutionsltd.com/secure/transact.asp?'

      # eCommerce sale transaction, details keyed by merchant or cardholder
      MESSAGE_TYPE = 'ESALE_KEYED'

      # The type of response that we want to get from PSL, options are HTML, XML or REDIRECT
      RESPONSE_ACTION = 'HTML'

      # Currency Codes
      CURRENCY_CODES = {
        'AUD' => 036,
        'GBP' => 826,
        'USD' => 840
      }

      # The terminal used - only for swipe transactions, so hard coded to 32 for online
      EMV_TERMINAL_TYPE = 32

      # Different Dispatch types
      DISPATCH_LATER  = 'LATER'
      DISPATCH_NOW    = 'NOW'

      # Return codes
      APPROVED = '00'

      # Nominal amount to authorize for a 'dispatch later' type
      # The nominal amount is held straight away, when the goods are ready
      # to be dispatched, PSL is informed and the full amount is the
      # taken.
      NOMINAL_AMOUNT = 101

      AVS_CODE = {
        'ALL MATCH'	=> 'Y',
        'SECURITY CODE MATCH ONLY' => 'N',
        'ADDRESS MATCH ONLY' => 'Y',
        'NO DATA MATCHES'	=> 'N',
        'DATA NOT CHECKED'	=> 'R',
        'SECURITY CHECKS NOT SUPPORTED'	=> 'X'
      }

      CVV_CODE = {
        'ALL MATCH'	=> 'M',
        'SECURITY CODE MATCH ONLY' => 'M',
        'ADDRESS MATCH ONLY' => 'N',
        'NO DATA MATCHES'	=> 'N',
        'DATA NOT CHECKED'	=> 'P',
        'SECURITY CHECKS NOT SUPPORTED'	=> 'X'
      }

      # Create a new PslCardGateway
      #
      # The gateway requires that a valid :login be passed in the options hash
      #
      # Paramaters:
      #   -options:
      #     :login -    the PslCard account login (required)
      def initialize(options = {})
        requires!(options, :login)

        super
      end

      # Purchase the item straight away
      #
      # Parameters:
      #   -money: Amount to be charged as an Integer value in cents
      #   -authorization: the PSL cross reference from the previous authorization
      #   -options:
      #
      # Returns:
      #   -ActiveMerchant::Billing::Response object
      #
      def purchase(money, credit_card, options = {})
        post = {}

        add_amount(post, money, DISPATCH_NOW, options)
        add_credit_card(post, credit_card)
        add_address(post, options)
        add_invoice(post, options)
        add_purchase_details(post)

        commit(post)
      end

      # Authorize the transaction
      #
      # Reserves the funds on the customer's credit card, but does not
      # charge the card.
      #
      # This implementation does not authorize the full amount, rather it checks that the full amount
      # is available and only 'reserves' the nominal amount (currently a pound and a penny)
      #
      # Parameters:
      #   -money: Amount to be charged as an Integer value in cents
      #   -authorization: the PSL cross reference from the previous authorization
      #   -options:
      #
      # Returns:
      #   -ActiveMerchant::Billing::Response object
      #
      def authorize(money, credit_card, options = {})
        post = {}

        add_amount(post, money, DISPATCH_LATER, options)
        add_credit_card(post, credit_card)
        add_address(post, options)
        add_invoice(post, options)
        add_purchase_details(post)

        commit(post)
      end

      # Post an authorization.
      #
      # Captures the funds from an authorized transaction.
      #
      # Parameters:
      #   -money: Amount to be charged as an Integer value in cents
      #   -authorization: The PSL Cross Reference
      #   -options:
      #
      # Returns:
      #   -ActiveMerchant::Billing::Response object
      #
      def capture(money, authorization, options = {})
        post = {}

        add_amount(post, money, DISPATCH_NOW, options)
        add_reference(post, authorization)
        add_purchase_details(post)

        commit(post)
      end

      private

      def add_credit_card(post, credit_card)
        post[:QAName] = credit_card.name
        post[:CardNumber] = credit_card.number
        post[:EMVTerminalType] = EMV_TERMINAL_TYPE
        post[:ExpMonth] = credit_card.month
        post[:ExpYear] = credit_card.year

        # CV2 check
        post[:AVSCV2Check] = credit_card.verification_value? ? 'YES' : 'NO'
        post[:CV2] = credit_card.verification_value if credit_card.verification_value?
      end

      def add_address(post, options)
        address = options[:billing_address] || options[:address]
        return if address.nil?

        post[:QAAddress] = %i[address1 address2 city state].collect { |a| address[a] }.reject(&:blank?).join(' ')
        post[:QAPostcode] = address[:zip]
      end

      def add_invoice(post, options)
        post[:MerchantName] = options[:merchant] || 'Merchant Name' # May use this as the order_id field
        post[:OrderID] = options[:order_id] unless options[:order_id].blank?
      end

      def add_reference(post, authorization)
        post[:CrossReference] = authorization
      end

      def add_amount(post, money, dispatch_type, options)
        post[:CurrencyCode] = currency_code(options[:currency] || currency(money))

        if dispatch_type == DISPATCH_LATER
          post[:amount] = amount(NOMINAL_AMOUNT)
          post[:DispatchLaterAmount] = amount(money)
        else
          post[:amount] = amount(money)
        end

        post[:Dispatch] = dispatch_type
      end

      def add_purchase_details(post)
        post[:EchoAmount] = 'YES'
        post[:SCBI] = 'YES' # Return information about the transaction
        post[:MessageType] = MESSAGE_TYPE
      end

      # Get the currency code for the passed money object
      #
      # The money class stores the currency as an ISO 4217:2001 Alphanumeric,
      # however PSL requires the ISO 4217:2001 Numeric code.
      #
      # Parameters:
      #   -money: Integer value in cents
      #
      # Returns:
      #   -the ISO 4217:2001 Numberic currency code
      #
      def currency_code(currency)
        CURRENCY_CODES[currency]
      end

      # Parse the PSL response and create a Response object
      #
      # Parameters:
      #   -body:  The response string returned from PSL, Formatted:
      #           Key=value&key=value...
      #
      # Returns:
      #   -a hash with all of the values returned in the PSL response
      #
      def parse(body)
        fields = {}
        for line in body.split('&')
          key, value = *line.scan(%r{^(\w+)\=(.*)$}).flatten
          fields[key] = CGI.unescape(value)
        end
        fields.symbolize_keys
      end

      # Send the passed data to PSL for processing
      #
      # Parameters:
      #   -request: The data that is to be sent to PSL
      #
      # Returns:
      #   - ActiveMerchant::Billing::Response object
      #
      def commit(request)
        response = parse(ssl_post(self.live_url, post_data(request)))

        Response.new(
          response[:ResponseCode] == APPROVED,
          response[:Message],
          response,
          test: test?,
          authorization: response[:CrossReference],
          cvv_result: CVV_CODE[response[:AVSCV2Check]],
          avs_result: { code: AVS_CODE[response[:AVSCV2Check]] }
        )
      end

      # Put the passed data into a format that can be submitted to PSL
      # Key=Value&Key=Value...
      #
      # Any ampersands and equal signs are removed from the data being posted
      # as PSL puts them back into the response string which then cannot be parsed.
      # This is after escaping before sending the request to PSL - this is a work
      # around for the time being
      #
      # Parameters:
      #   -post: Hash of all the data to be sent
      #
      # Returns:
      #   -String: the data to be sent
      #
      def post_data(post)
        post[:CountryCode] = self.location
        post[:MerchantID] = @options[:login]
        post[:ValidityID] = @options[:password]
        post[:ResponseAction] = RESPONSE_ACTION

        post.collect { |key, value|
          "#{key}=#{CGI.escape(value.to_s.tr('&=', ' '))}"
        }.join('&')
      end
    end
  end
end
