# frozen_string_literal: true

require 'net/https'
require 'timeout'
require 'uri'

class PayPalRecurring
  API_VERSION = '72.0'
  USER_AGENT = 'Neocities PayPalRecurring'
  OPEN_TIMEOUT = 5
  READ_TIMEOUT = 10
  WRITE_TIMEOUT = 10

  ENDPOINTS = {
    sandbox: {
      api:  'https://api-3t.sandbox.paypal.com/nvp',
      site: 'https://www.sandbox.paypal.com/cgi-bin/webscr'
    },
    production: {
      api:  'https://api-3t.paypal.com/nvp',
      site: 'https://www.paypal.com/cgi-bin/webscr'
    }
  }.freeze

  ACTIONS = {
    cancel:     'Cancel',
    suspend:    'Suspend',
    reactivate: 'Reactivate'
  }.freeze

  INITIAL_AMOUNT_ACTIONS = {
    cancel:   'CancelOnFailure',
    continue: 'ContinueOnFailure'
  }.freeze

  OUTSTANDING = {
    next_billing: 'AddToNextBilling',
    no_auto:      'NoAutoBill'
  }.freeze

  PERIOD = {
    daily:   'Day',
    weekly:  'Weekly',
    monthly: 'Month',
    yearly:  'Year'
  }.freeze

  NETWORK_ERRORS = [
    Errno::ECONNREFUSED,
    Errno::ECONNRESET,
    Errno::EHOSTUNREACH,
    Errno::ENETUNREACH,
    Net::OpenTimeout,
    Net::ReadTimeout,
    OpenSSL::SSL::SSLError,
    SocketError,
    Timeout::Error
  ].freeze

  class << self
    attr_accessor :sandbox
    attr_accessor :username
    attr_accessor :password
    attr_accessor :signature

    def configure
      yield self
    end

    def sandbox?
      sandbox == true
    end

    def environment
      sandbox? ? :sandbox : :production
    end

    def api_endpoint
      ENDPOINTS[environment][:api]
    end

    def site_endpoint
      ENDPOINTS[environment][:site]
    end
  end

  attr_accessor :amount
  attr_accessor :cancel_url
  attr_accessor :currency
  attr_accessor :description
  attr_accessor :email
  attr_accessor :failed
  attr_accessor :frequency
  attr_accessor :initial_amount
  attr_accessor :initial_amount_action
  attr_accessor :ipn_url
  attr_accessor :locale
  attr_accessor :outstanding
  attr_accessor :payer_id
  attr_accessor :period
  attr_accessor :profile_id
  attr_accessor :reference
  attr_accessor :return_url
  attr_accessor :start_at
  attr_accessor :token
  attr_accessor :trial_amount
  attr_accessor :trial_frequency
  attr_accessor :trial_length
  attr_accessor :trial_period

  def initialize(options={})
    options.each { |name, value| public_send("#{name}=", value) }
  end

  def checkout
    run('SetExpressCheckout', billing_agreement_params.merge(
      'RETURNURL' => return_url,
      'CANCELURL' => cancel_url,
      'LOCALECODE' => build_locale(locale),
      'PAYMENTREQUEST_0_PAYMENTACTION' => 'Authorization',
      'NOSHIPPING' => 1,
      'L_BILLINGTYPE0' => 'RecurringPayments'
    ))
  end

  def request_payment
    run('DoExpressCheckoutPayment', billing_agreement_params.merge(
      'RETURNURL' => return_url,
      'CANCELURL' => cancel_url,
      'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
      'PAYERID' => payer_id,
      'TOKEN' => token,
      'PROFILEREFERENCE' => reference,
      'PAYMENTREQUEST_0_CUSTOM' => reference,
      'PAYMENTREQUEST_0_INVNUM' => reference
    ))
  end

  def create_recurring_profile
    run('CreateRecurringPaymentsProfile', billing_agreement_params.merge(
      'INITAMT' => initial_amount,
      'FAILEDINITAMTACTION' => map(INITIAL_AMOUNT_ACTIONS, initial_amount_action),
      'PAYERID' => payer_id,
      'TOKEN' => token,
      'PROFILEREFERENCE' => reference,
      'PROFILESTARTDATE' => build_timestamp(start_at),
      'MAXFAILEDPAYMENTS' => failed,
      'AUTOBILLOUTAMT' => map(OUTSTANDING, outstanding),
      'BILLINGFREQUENCY' => frequency,
      'BILLINGPERIOD' => map(PERIOD, period),
      'EMAIL' => email,
      'TRIALTOTALBILLINGCYCLES' => trial_length,
      'TRIALBILLINGPERIOD' => map(PERIOD, trial_period),
      'TRIALBILLINGFREQUENCY' => trial_frequency,
      'TRIALAMT' => trial_amount
    ))
  end

  def cancel
    run('ManageRecurringPaymentsProfileStatus', {
      'ACTION' => map(ACTIONS, :cancel),
      'PROFILEID' => profile_id
    })
  end

  private

  def billing_agreement_params
    {
      'PAYMENTREQUEST_0_AMT' => amount,
      'AMT' => amount,
      'PAYMENTREQUEST_0_CURRENCYCODE' => currency,
      'CURRENCYCODE' => currency,
      'DESC' => description,
      'PAYMENTREQUEST_0_DESC' => description,
      'L_BILLINGAGREEMENTDESCRIPTION0' => description,
      'PAYMENTREQUEST_0_NOTIFYURL' => ipn_url,
      'NOTIFYURL' => ipn_url
    }
  end

  def run(method, params)
    Response.new(post(default_params.merge(params).merge('METHOD' => method)))
  rescue *NETWORK_ERRORS => e
    Response.new(nil, error: e)
  end

  def post(params)
    uri = URI(self.class.api_endpoint)
    request = Net::HTTP::Post.new(uri.request_uri)
    request['User-Agent'] = USER_AGENT
    request.set_form_data compact_params(params)

    http_client(uri).request(request)
  end

  def http_client(uri)
    Net::HTTP.new(uri.host, uri.port).tap do |http|
      http.use_ssl = uri.scheme == 'https'
      http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
      http.cert_store = OpenSSL::X509::Store.new if http.use_ssl?
      http.cert_store&.set_default_paths
      http.open_timeout = OPEN_TIMEOUT
      http.read_timeout = READ_TIMEOUT
      http.write_timeout = WRITE_TIMEOUT if http.respond_to?(:write_timeout=)
    end
  end

  def default_params
    {
      'USER' => self.class.username,
      'PWD' => self.class.password,
      'SIGNATURE' => self.class.signature,
      'VERSION' => API_VERSION
    }
  end

  def compact_params(params)
    params.reject { |_name, value| value.nil? }
  end

  def build_locale(value)
    value.to_s.upcase if value
  end

  def build_timestamp(value)
    value.respond_to?(:to_time) ? value.to_time.utc.strftime('%Y-%m-%dT%H:%M:%SZ') : value
  end

  def map(mapping, value)
    return nil if value.nil?

    mapping.fetch(value.to_sym, value)
  end

  class Response
    attr_reader :http_response
    attr_reader :error

    def initialize(http_response, error: nil)
      @http_response = http_response
      @error = error
    end

    def params
      return {} if http_response.nil?

      @params ||= URI.decode_www_form(http_response.body.to_s).each_with_object({}) do |(name, value), parsed|
        parsed[name] = value
      end
    end

    def token
      params['TOKEN']
    end

    def ack
      params['ACK']
    end

    def profile_id
      params['PROFILEID']
    end

    def checkout_url
      "#{PayPalRecurring.site_endpoint}?#{URI.encode_www_form(
        cmd: '_express-checkout',
        token: token,
        useraction: 'commit'
      )}"
    end

    def completed?
      params['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Completed'
    end

    def approved?
      params['PAYMENTINFO_0_ACK'] == 'Success'
    end

    def success?
      ack == 'Success'
    end

    def valid?
      !network_error? && errors.empty? && success?
    end

    def network_error?
      !error.nil?
    end

    def errors
      return [{code: 'network_error', messages: [error.message]}] if network_error?

      @errors ||= begin
        index = 0
        [].tap do |results|
          while params["L_ERRORCODE#{index}"]
            results << {
              code: params["L_ERRORCODE#{index}"],
              messages: [
                params["L_SHORTMESSAGE#{index}"],
                params["L_LONGMESSAGE#{index}"]
              ]
            }
            index += 1
          end
        end
      end
    end
  end
end
