module ActiveMerchant
  module Billing
    class PlugnpayGateway < Gateway
      class PlugnpayPostData < PostData
        # Fields that will be sent even if they are blank
        self.required_fields = %i[publisher_name publisher_password
                                  card_amount card_name card_number card_exp orderID]
      end
      self.live_url = self.test_url = 'https://pay1.plugnpay.com/payment/pnpremote.cgi'

      CARD_CODE_MESSAGES = {
        'M' => 'Card verification number matched',
        'N' => "Card verification number didn't match",
        'P' => 'Card verification number was not processed',
        'S' => 'Card verification number should be on card but was not indicated',
        'U' => 'Issuer was not certified for card verification'
      }

      CARD_CODE_ERRORS = %w(N S)

      AVS_MESSAGES = {
        'A' => 'Street address matches billing information, zip/postal code does not',
        'B' => 'Address information not provided for address verification check',
        'E' => 'Address verification service error',
        'G' => 'Non-U.S. card-issuing bank',
        'N' => 'Neither street address nor zip/postal match billing information',
        'P' => 'Address verification not applicable for this transaction',
        'R' => 'Payment gateway was unavailable or timed out',
        'S' => 'Address verification service not supported by issuer',
        'U' => 'Address information is unavailable',
        'W' => '9-digit zip/postal code matches billing information, street address does not',
        'X' => 'Street address and 9-digit zip/postal code matches billing information',
        'Y' => 'Street address and 5-digit zip/postal code matches billing information',
        'Z' => '5-digit zip/postal code matches billing information, street address does not'
      }

      AVS_ERRORS = %w(A E N R W Z)

      PAYMENT_GATEWAY_RESPONSES = {
        'P01' => 'AVS Mismatch Failure',
        'P02' => 'CVV2 Mismatch Failure',
        'P21' => 'Transaction may not be marked',
        'P30' => 'Test Tran. Bad Card',
        'P35' => 'Test Tran. Problem',
        'P40' => 'Username already exists',
        'P41' => 'Username is blank',
        'P50' => 'Fraud Screen Failure',
        'P51' => 'Missing PIN Code',
        'P52' => 'Invalid Bank Acct. No.',
        'P53' => 'Invalid Bank Routing No.',
        'P54' => 'Invalid/Missing Check No.',
        'P55' => 'Invalid Credit Card No.',
        'P56' => 'Invalid CVV2/CVC2 No.',
        'P57' => 'Expired. CC Exp. Date',
        'P58' => 'Missing Data',
        'P59' => 'Missing Email Address',
        'P60' => 'Zip Code does not match Billing State.',
        'P61' => 'Invalid Billing Zip Code',
        'P62' => 'Zip Code does not match Shipping State.',
        'P63' => 'Invalid Shipping Zip Code',
        'P64' => 'Invalid Credit Card CVV2/CVC2 Format.',
        'P65' => 'Maximum number of attempts has been exceeded.',
        'P66' => 'Credit Card number has been flagged and can not be used to access this service.',
        'P67' => 'IP Address is on Blocked List.',
        'P68' => 'Billing country does not match ipaddress country.',
        'P69' => 'US based ipaddresses are currently blocked.',
        'P70' => 'Credit Cards issued from this bank are currently not being accepted.',
        'P71' => 'Credit Cards issued from this bank are currently not being accepted.',
        'P72' => 'Daily volume exceeded.',
        'P73' => 'Too many transactions within allotted time.',
        'P91' => 'Missing/incorrect password',
        'P92' => 'Account not configured for mobil administration',
        'P93' => 'IP Not registered to username.',
        'P94' => 'Mode not permitted for this account.',
        'P95' => 'Currently Blank',
        'P96' => 'Currently Blank',
        'P97' => 'Processor not responding',
        'P98' => 'Missing merchant/publisher name',
        'P99' => 'Currently Blank'
      }

      TRANSACTIONS = {
        authorization: 'auth',
        purchase: 'auth',
        capture: 'mark',
        void: 'void',
        refund: 'return',
        credit: 'newreturn'
      }

      SUCCESS_CODES = %w[pending success]
      FAILURE_CODES = %w[badcard fraud]

      self.default_currency = 'USD'
      self.supported_countries = ['US']
      self.supported_cardtypes = %i[visa master american_express discover]
      self.homepage_url = 'http://www.plugnpay.com/'
      self.display_name = "Plug'n Pay"

      def initialize(options = {})
        requires!(options, :login, :password)
        super
      end

      def purchase(money, creditcard, options = {})
        post = PlugnpayPostData.new

        add_amount(post, money, options)
        add_creditcard(post, creditcard)
        add_addresses(post, options)
        add_invoice_data(post, options)
        add_customer_data(post, options)

        post[:authtype] = 'authpostauth'
        commit(:authorization, post)
      end

      def authorize(money, creditcard, options = {})
        post = PlugnpayPostData.new

        add_amount(post, money, options)
        add_creditcard(post, creditcard)
        add_addresses(post, options)
        add_invoice_data(post, options)
        add_customer_data(post, options)

        post[:authtype] = 'authonly'
        commit(:authorization, post)
      end

      def capture(money, authorization, options = {})
        post = PlugnpayPostData.new

        post[:orderID] = authorization

        add_amount(post, money, options)
        add_customer_data(post, options)

        commit(:capture, post)
      end

      def void(authorization, options = {})
        post = PlugnpayPostData.new

        post[:orderID] = authorization
        post[:txn_type] = 'auth'

        commit(:void, post)
      end

      def credit(money, identification_or_creditcard, options = {})
        post = PlugnpayPostData.new
        add_amount(post, money, options)

        if identification_or_creditcard.is_a?(String)
          ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
          refund(money, identification_or_creditcard, options)
        else
          add_creditcard(post, identification_or_creditcard)
          add_addresses(post, options)
          add_customer_data(post, options)

          commit(:credit, post)
        end
      end

      def refund(money, reference, options = {})
        post = PlugnpayPostData.new
        add_amount(post, money, options)
        post[:orderID] = reference
        commit(:refund, post)
      end

      private

      def commit(action, post)
        response = parse(ssl_post(self.live_url, post_data(action, post)))
        success = SUCCESS_CODES.include?(response[:finalstatus])
        message = success ? 'Success' : message_from(response)

        Response.new(
          success,
          message,
          response,
          test: test?,
          authorization: response[:orderid],
          avs_result: { code: response[:avs_code] },
          cvv_result: response[:cvvresp]
        )
      end

      def parse(body)
        body = CGI.unescape(body)
        results = {}
        body.split('&').collect { |e| e.split('=') }.each do |key, value|
          results[key.downcase.to_sym] = normalize(value.to_s.strip)
        end

        results.delete(:publisher_password)
        results[:avs_message] = AVS_MESSAGES[results[:avs_code]] if results[:avs_code]
        results[:card_code_message] = CARD_CODE_MESSAGES[results[:cvvresp]] if results[:cvvresp]

        results
      end

      def post_data(action, post)
        post[:mode]               = TRANSACTIONS[action]
        post[:convert]            = 'underscores'
        post[:app_level]          = 0
        post[:publisher_name]     = @options[:login]
        post[:publisher_password] = @options[:password]

        post.to_s
      end

      def add_creditcard(post, creditcard)
        post[:card_number]  = creditcard.number
        post[:card_cvv]     = creditcard.verification_value
        post[:card_exp]     = expdate(creditcard)
        post[:card_name]    = creditcard.name.slice(0..38)
      end

      def add_customer_data(post, options)
        post[:email] = options[:email]
        post[:dontsndmail] = 'yes' unless options[:send_email_confirmation]
        post[:ipaddress] = options[:ip]
      end

      def add_invoice_data(post, options)
        post[:shipping] = amount(options[:shipping]) unless options[:shipping].blank?
        post[:tax] = amount(options[:tax]) unless options[:tax].blank?
      end

      def add_addresses(post, options)
        if address = options[:billing_address] || options[:address]
          post[:card_address1] = address[:address1]
          post[:card_zip]      = address[:zip]
          post[:card_city]     = address[:city]
          post[:card_country]  = address[:country]
          post[:phone]         = address[:phone]

          case address[:country]
          when 'US', 'CA'
            post[:card_state] = address[:state]
          else
            post[:card_state] = 'ZZ'
            post[:card_prov]  = address[:state]
          end
        end

        if shipping_address = options[:shipping_address] || address
          post[:shipname] = shipping_address[:name]
          post[:address1] = shipping_address[:address1]
          post[:address2] = shipping_address[:address2]
          post[:city] = shipping_address[:city]

          case shipping_address[:country]
          when 'US', 'CA'
            post[:state] = shipping_address[:state]
          else
            post[:state] = 'ZZ'
            post[:province] = shipping_address[:state]
          end

          post[:country] = shipping_address[:country]
          post[:zip] = shipping_address[:zip]
        end
      end

      def add_amount(post, money, options)
        post[:card_amount] = amount(money)
        post[:currency] = options[:currency] || currency(money)
      end

      def message_from(results)
        PAYMENT_GATEWAY_RESPONSES[results[:resp_code]]
      end

      def expdate(creditcard)
        year  = sprintf('%.4i', creditcard.year)
        month = sprintf('%.2i', creditcard.month)

        "#{month}/#{year[-2..-1]}"
      end
    end
  end
end
