app/models/zuora_connect/app_instance_base.rb



module ZuoraConnect
  require "uri"
  class AppInstanceBase < ActiveRecord::Base
    default_scope {select(ZuoraConnect::AppInstance.column_names.delete_if {|x| ["catalog_mapping", "catalog"].include?(x) }) }
    after_initialize :init
    self.table_name = "zuora_connect_app_instances"
    attr_accessor :options, :mode, :logins, :task_data, :last_refresh, :username, :password, :s3_client, :api_version

    REFRESH_TIMEOUT = 2.minute                #Used to determine how long to wait on current refresh call before executing another
    INSTANCE_REFRESH_WINDOW = 30.minutes      #Used to set how how long till app starts attempting to refresh cached task connect data
    INSTANCE_REDIS_CACHE_PERIOD = 60.minutes  #Used to determine how long to cached task data will live for
    API_LIMIT_TIMEOUT = 2.minutes             #Used to set the default for expiring timeout when api rate limiting is in effect
    BLANK_OBJECT_ID_LOOKUP = 'BlankValueSupplied'

    def init
      @options = Hash.new
      @logins = Hash.new
      @api_version = "v2"
      self.attr_builder("timezone", ZuoraConnect.configuration.default_time_zone)
      self.attr_builder("locale", ZuoraConnect.configuration.default_locale)
      PaperTrail.whodunnit = "Backend" if defined?(PaperTrail)
      if INSTANCE_REFRESH_WINDOW > INSTANCE_REDIS_CACHE_PERIOD
        raise "The instance refresh window cannot be greater than the instance cache period"
      end
      self.apartment_switch(nil, true)
    end


    # Methods for writing Telegraf metrics

    # Returns the process type if any
    def self.get_process_type
      p_type = nil
      if ENV['HOSTNAME'] && ENV['DEIS_APP']
        temp = ENV['HOSTNAME'].split(ENV['DEIS_APP'])[1]
        temp = temp.split(/(-[0-9a-zA-Z]{5})$/)[0] # remove the 5 char hash
        p_type = temp[1, temp.rindex("-")-1]
      end
      return p_type
    end

    # Write to telegraf
    def self.write_to_telegraf(endpoint_name: nil, method_name: nil, status_code: nil, response_time: nil, db_runtime: nil, view_runtime: nil, content_type: nil, direction: nil, error_type: nil, app_instance: nil, function_name: nil)

      # To avoid writing metrics from rspec tests
      if ENV['DEIS_APP']
        # Getting the process type
        p_type = ZuoraConnect::AppInstanceBase.get_process_type

        if direction == "inbound"
          Thread.current[:appinstance].present? ? app_instance = Thread.current[:appinstance].id : app_instance = 0

          # Separately handling 200 and non 200 as influx does not accept nil as a value
          if db_runtime && view_runtime
            # 200 requests
            begin
              ZuoraConnect.configuration.telegraf_client.write(ZuoraConnect.configuration.influxdb_series_name_inbound,
                  tags: {"app_name": "#{ZuoraConnect.configuration.app_name}", "controller_action": endpoint_name, "content-type": content_type, method: method_name, status: status_code, process_type: p_type, "app_instance": app_instance},
                  values: {response_time: response_time, db_time: db_runtime, view_time: view_runtime})
            rescue => e
              raise e
            end
          else
            # non 200 requests
            begin
              ZuoraConnect.configuration.telegraf_client.write(ZuoraConnect.configuration.influxdb_series_name_inbound,
                  tags: {"app_name": "#{ZuoraConnect.configuration.app_name}", "controller_action": endpoint_name, "content-type": content_type, method: method_name, status: status_code, process_type: p_type, "app_instance": app_instance},
                  values: {response_time: response_time})
            rescue=> e
              raise e
            end
          end

        elsif direction == "outbound"
          # if there is an error
          if error_type
            begin
              ZuoraConnect.configuration.telegraf_client.write(ZuoraConnect.configuration.influxdb_series_name_outbound,
                  tags: {"app_name": "#{ZuoraConnect.configuration.app_name}", endpoint: endpoint_name, status: status_code, process_type: p_type, "app_instance": app_instance, "function_name": function_name, method: method_name, "error_type": error_type},
                  values: {response_time: response_time})
            rescue => e
              raise e
            end
          else
            begin
              ZuoraConnect.configuration.telegraf_client.write(ZuoraConnect.configuration.influxdb_series_name_outbound,
                  tags: {"app_name": "#{ZuoraConnect.configuration.app_name}", endpoint: endpoint_name, status: status_code, process_type: p_type, "app_instance": app_instance, "function_name": function_name, method: method_name},
                  values: {response_time: response_time})
            rescue => e
              raise e
            end
          end
        end
      end
    end

    def self.get_metrics(type)
      namespace = ENV['DEIS_APP'].present? ? "#{ENV['DEIS_APP']}" : "#{Rails.application.class.parent_name}"

      data = {}

      if type == "versions"
        data = {
          app_name: namespace,
          Version_Gem: ZuoraConnect::VERSION,
          Version_Zuora: ZuoraAPI::VERSION ,
          Version_Ruby: RUBY_VERSION,
          Version_Rails: Rails.version,
          hold: 1
        }
      elsif type == "stats"
        begin
          Resque.redis.ping
          data = {
            app_name: namespace,
            Resque:{
              Jobs_Finished: Resque.info[:processed] ,
              Jobs_Failed: Resque.info[:failed],
              Jobs_Pending: Resque.info[:pending],
              Workers_Active: Resque.info[:working],
              Workers_Total: Resque.info[:workers]
            }
          }
        rescue
        end
      end
      return data.to_json
    end


    def apartment_switch(method = nil, migrate = false)
      begin
        Apartment::Tenant.switch!(self.id) if self.persisted?
      rescue Apartment::TenantNotFound => ex
        Apartment::Tenant.create(self.id.to_s)
        retry
      end
      if migrate && ActiveRecord::Migrator.needs_migration?
        Apartment::Migrator.migrate(self.id)
      end
      Thread.current[:appinstance] = self
    end

    def new_session(session: self.data_lookup, username: self.access_token, password: self.refresh_token, holding_pattern: false)
      @api_version = "v2"
      @username = username
      @password = password
      @last_refresh = session["#{self.id}::last_refresh"]

      ## DEV MODE TASK DATA MOCKUP
      if ZuoraConnect.configuration.mode != "Production"
        mock_task_data = {
          "mode" => ZuoraConnect.configuration.dev_mode_mode
        }

        case ZuoraConnect.configuration.dev_mode_options.class
        when Hash
          @options = ZuoraConnect.configuration.dev_mode_options
        when Array
          mock_task_data["options"] = ZuoraConnect.configuration.dev_mode_options
        end

        ZuoraConnect.configuration.dev_mode_logins.each do |k,v|
          v = v.merge({"entities": [] }) if !v.keys.include?("entities")
          mock_task_data[k] = v
        end

        build_task(mock_task_data, session)
      else
        time_expire = (session["#{self.id}::last_refresh"] || Time.now).to_i - INSTANCE_REFRESH_WINDOW.ago.to_i

        if session.empty?
          Rails.logger.info("[#{self.id}] REFRESHING - Session Empty")
          raise ZuoraConnect::Exceptions::HoldingPattern if holding_pattern && !self.mark_for_refresh
          self.refresh(session)

        elsif (self.id != session["appInstance"].to_i)
          Rails.logger.info("[#{self.id}] REFRESHING - AppInstance ID(#{self.id}) does not match session id(#{session["appInstance"].to_i})")
          raise ZuoraConnect::Exceptions::HoldingPattern if holding_pattern && !self.mark_for_refresh
          self.refresh(session)

        elsif session["#{self.id}::task_data"].blank?
          Rails.logger.info("[#{self.id}] REFRESHING - Task Data Blank")
          raise ZuoraConnect::Exceptions::HoldingPattern if holding_pattern && !self.mark_for_refresh
          self.refresh(session)

        elsif session["#{self.id}::last_refresh"].blank?
          Rails.logger.info("[#{self.id}] REFRESHING - No Time on Cookie")
          raise ZuoraConnect::Exceptions::HoldingPattern if holding_pattern && !self.mark_for_refresh
          self.refresh(session)

        # If the cache is expired and we can aquire a refresh lock
        elsif (session["#{self.id}::last_refresh"].to_i < INSTANCE_REFRESH_WINDOW.ago.to_i) && self.mark_for_refresh
          Rails.logger.info("[#{self.id}] REFRESHING - Session Old by #{time_expire.abs} second")
          self.refresh(session)
        else
          if time_expire < 0
            Rails.logger.info(["[#{self.id}] REBUILDING - Expired by #{time_expire} seconds", self.marked_for_refresh? ? " cache updating as of #{self.reset_mark_refreshed_at} seconds ago" : nil].compact.join(','))
          else
            Rails.logger.info("[#{self.id}] REBUILDING - Expires in #{time_expire} seconds")
          end
          build_task(session["#{self.id}::task_data"], session)
        end
      end
      begin
        I18n.locale = self.locale
      rescue I18n::InvalidLocale => ex
        Rails.logger.debug("Invalid Locale: #{ex.message}")
      end
      Time.zone = self.timezone
      return self
    rescue ZuoraConnect::Exceptions::HoldingPattern => ex
      while self.marked_for_refresh?
        Rails.logger.info("[#{self.id}] Holding - Expires in #{self.reset_mark_expires_at}")
        sleep(5)
      end
      self.reload_attributes([:refresh_token, :oauth_expires_at, :access_token])
      session = self.data_lookup(session: session)
      retry
    end

    def refresh(session = nil)
      refresh_count ||= 0
      error_type = ""
      start = Time.now
      response = HTTParty.get(ZuoraConnect.configuration.url + "/api/#{self.api_version}/tools/tasks/#{self.id}.json",:body => {:access_token => self.access_token})
      response_time = Time.now - start

      Rails.logger.debug("[#{self.id}] REFRESH TASK - Connect Task Info Request Time #{response_time.round(2).to_s}")
      if response.code == 200
        build_task(JSON.parse(response.body), session)
        @last_refresh = Time.now.to_i
        self.cache_app_instance
        self.reset_mark_for_refresh
      else
        Rails.logger.fatal("[#{self.id}] REFRESH TASK - Failed Code #{response.code}")
        raise ZuoraConnect::Exceptions::ConnectCommunicationError.new("Error Communicating with Connect", response.body, response.code)
      end
    rescue Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError => ex
      if (refresh_count += 1) < 3
        Rails.logger.info("[#{self.id}] REFRESH TASK - #{ex.class} Retrying(#{refresh_count})")
        retry
      else
        error_type = "#{ex.class}"
        Rails.logger.fatal("[#{self.id}] REFRESH TASK - #{ex.class} Failed #{refresh_count}x")
        raise
      end
    rescue ZuoraConnect::Exceptions::ConnectCommunicationError => ex
      if (refresh_count += 1) < 3
        Rails.logger.info("[#{self.id}] REFRESH TASK - Failed Retrying(#{refresh_count})")
        if ex.code == 401
          self.refresh_oauth
        end
        retry
      else
        error_type = "#{ex.class}"
        Rails.logger.fatal("[#{self.id}] REFRESH TASK - Failed #{refresh_count}x")
        raise
      end
    ensure
      # Writing to telegraf
      status_code = response.code if response
      endpoint_name = URI(ZuoraConnect.configuration.url).host
      Thread.current[:appinstance].present? ? app_instance = Thread.current[:appinstance].id : app_instance = 0
      ZuoraConnect::AppInstanceBase.write_to_telegraf("response_time": response_time, "status_code": status_code, "endpoint_name": endpoint_name, "direction": "outbound", "error_type": error_type, "function_name": "#{self.class}##{__method__}", "method_name": "GET", "app_instance": app_instance)
    end

    #### START Task Mathods ####
      def build_task(task_data, session)
        begin
          @task_data = task_data
          @mode = @task_data["mode"]
          @task_data.each do |k,v|
            if k.match(/^(.*)_login$/)
              tmp = ZuoraConnect::Login.new(v)
              if !session.nil? && v["tenant_type"] == "Zuora"
                if tmp.entities.size > 0
                  tmp.entities.each do |value|
                    entity_id = value["id"]
                    tmp.client(entity_id).current_session = session["#{self.id}::#{k}::#{entity_id}:session"] if !session.nil? && v["tenant_type"] == "Zuora" && session["#{self.id}::#{k}::#{entity_id}:session"]
                  end
                else
                  tmp.client.current_session = session["#{self.id}::#{k}:session"] if !session.nil? && v["tenant_type"] == "Zuora" && session["#{self.id}::#{k}:session"]
                end
              end
              @logins[k] = tmp
              self.attr_builder(k, @logins[k])
            elsif k == "options"
              v.each do |opt|
                @options[opt["config_name"]] = opt
              end
            elsif k == "user_settings"
              self.timezone =  v["timezone"]
              self.locale = v["local"]
            end
          end
        rescue => ex
          Rails.logger.error("Task Data: #{task_data}")
          Rails.logger.error("Task Session: #{session.to_hash}")
          raise
        end
      end

      def updateOption(optionId, value)
        begin
          start_time = Time.now
          response = HTTParty.get(ZuoraConnect.configuration.url + "/api/#{self.api_version}/tools/application_options/#{optionId}/edit?value=#{value}",:body => {:access_token => self.username})
        rescue => e
          error_type = "#{e.class}"
        ensure
          end_time = Time.now
          response_time = end_time - start_time
          status_code = response.code if response
          endpoint_name = URI(ZuoraConnect.configuration.url).host
          Thread.current[:appinstance].present? ? app_instance = Thread.current[:appinstance].id : app_instance = 0
          ZuoraConnect::AppInstanceBase.write_to_telegraf("response_time": response_time, "status_code": status_code, "endpoint_name": endpoint_name, "direction": "outbound", "error_type": error_type, "function_name": "#{self.class}##{__method__}", "method_name": "GET", "app_instance": app_instance)
          return response
        end
      end

      #This can update an existing login, add a new login, change to another existing login
      #EXAMPLE: {"name": "ftp_login_14","username": "ftplogin7","tenant_type": "Custom","password": "test2","url": "www.ftp.com","custom_data": {  "path": "/var/usr/test"}}
      def update_logins(options)
        update_login_count ||= 0
        start_time = Time.now
        error_type = ""
        response = HTTParty.post(ZuoraConnect.configuration.url + "/api/#{self.api_version}/tools/tasks/#{self.id}/logins",:body => {:access_token => self.username}.merge(options))
        parsed_json =  JSON.parse(response.body)
        if response.code == 200
          if defined?(Redis.current)
            self.build_task(parsed_json, self.data_lookup)
            self.last_refresh = Time.now.to_i
            self.cache_app_instance
          end
          return parsed_json
        elsif response.code == 400
          raise ZuoraConnect::Exceptions::APIError.new(message: parsed_json['errors'].join(' '), response: response.body, code: response.code)
        else
          raise ZuoraConnect::Exceptions::ConnectCommunicationError.new("Error Communicating with Connect", response.body, response.code)
        end
      rescue Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError => ex
        if (update_login_count += 1) < 3
          retry
        else
          error_type = "#{ex.class}"
          raise
        end
      rescue ZuoraConnect::Exceptions::ConnectCommunicationError => ex
        if (update_login_count += 1) < 3
          if ex.code == 401
            self.refresh_oauth
          end
          retry
        else
          error_type = "#{ex.class}"
          raise
        end
      ensure
        end_time = Time.now
        response_time = end_time - start_time
        status_code = response.code if response
        endpoint_name = URI(ZuoraConnect.configuration.url).host
        Thread.current[:appinstance].present? ? app_instance = Thread.current[:appinstance].id : app_instance = 0
        ZuoraConnect::AppInstanceBase.write_to_telegraf("response_time": response_time, "status_code": status_code, "endpoint_name": endpoint_name, "direction": "outbound", "error_type": error_type, "function_name": "#{self.class}##{__method__}", "method_name": "POST", "app_instance": app_instance)
      end
    #### END Task Mathods ####

    #### START Connect OAUTH methods ####
      def check_oauth_state(method)
        #Refresh token if already expired
        if self.oauth_expired?
          Rails.logger.debug("[#{self.id}] Before '#{method}' method, Oauth expired")
          self.refresh_oauth
        end
      end

      def oauth_expired?
        return self.oauth_expires_at.present? ? (self.oauth_expires_at < Time.now) : true
      end

      def refresh_oauth
        refresh_oauth_count ||= 0
        error_type = ""
        start = Time.now
        params = {
                  :grant_type => "refresh_token",
                  :redirect_uri => ZuoraConnect.configuration.oauth_client_redirect_uri,
                  :refresh_token => self.refresh_token
                }
        response = HTTParty.post("#{ZuoraConnect.configuration.url}/oauth/token",:body => params)
        response_time = Time.now - start
        Rails.logger.info("[#{self.id}] REFRESH OAUTH - In #{response_time.round(2).to_s}")

        if response.code == 200
          response_body = JSON.parse(response.body)

          self.refresh_token = response_body["refresh_token"]
          self.access_token = response_body["access_token"]
          self.oauth_expires_at = Time.at(response_body["created_at"].to_i) + response_body["expires_in"].seconds
          self.save(:validate => false)
        else
          Rails.logger.fatal("[#{self.id}] REFRESH OAUTH - Failed Code #{response.code}")
          raise ZuoraConnect::Exceptions::ConnectCommunicationError.new("Error Refreshing Access Token", response.body, response.code)
        end
      rescue Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError => ex
        if (refresh_oauth_count += 1) < 3
          Rails.logger.info("[#{self.id}] REFRESH OAUTH - #{ex.class} Retrying(#{refresh_oauth_count})")
          retry
        else
          error_type = "#{ex.class}"
          Rails.logger.fatal("[#{self.id}] REFRESH OAUTH - #{ex.class} Failed #{refresh_oauth_count}x")
          raise
        end
      rescue ZuoraConnect::Exceptions::ConnectCommunicationError => ex
        sleep(5)
        self.reload_attributes([:refresh_token, :oauth_expires_at, :access_token]) #Reload only the refresh token for retry

        #After reload, if nolonger expired return
        return if !self.oauth_expired?

        if (refresh_oauth_count += 1) < 3
          Rails.logger.info("[#{self.id}] REFRESH OAUTH - Failed Retrying(#{refresh_oauth_count})")
          retry
        else
          error_type = "#{ex.class}"
          Rails.logger.fatal("[#{self.id}] REFRESH OAUTH - Failed #{refresh_oauth_count}x")
          raise
        end
      ensure
        status_code = response.code if response
        endpoint_name = URI(ZuoraConnect.configuration.url).host
        Thread.current[:appinstance].present? ? app_instance = Thread.current[:appinstance].id : app_instance = 0
        ZuoraConnect::AppInstanceBase.write_to_telegraf("response_time": response_time, "status_code": status_code, "endpoint_name": endpoint_name, "direction": "outbound", "error_type": error_type, "function_name": "#{self.class}##{__method__}", "method_name": "POST", "app_instance": app_instance)
      end
    #### END Connect OAUTH methods ####

    #### START AppInstance Temporary Persistance Methods ####
      def marked_for_refresh?
        return defined?(Redis.current) ? Redis.current.get("AppInstance:#{self.id}:Refreshing").to_bool : false
      end

      def reset_mark_for_refresh
        Redis.current.del("AppInstance:#{self.id}:Refreshing") if defined?(Redis.current)
      end

      def reset_mark_refreshed_at
        return defined?(Redis.current) ? REFRESH_TIMEOUT.to_i - Redis.current.ttl("AppInstance:#{self.id}:Refreshing") : 0
      end

      def reset_mark_expires_at
        return defined?(Redis.current) ? Redis.current.ttl("AppInstance:#{self.id}:Refreshing") : 0
      end

      def mark_for_refresh
        return defined?(Redis.current) ? Redis.current.set("AppInstance:#{self.id}:Refreshing", true, {:nx => true, :ex => REFRESH_TIMEOUT.to_i}) : true
      end

      def data_lookup(session: {})
        if defined?(PaperTrail)
          PaperTrail.whodunnit = session["#{self.id}::user::email"].present? ? session["#{self.id}::user::email"] : nil if session.present?
        end
        if defined?(Redis.current)
          cached_instance = Redis.current.get("AppInstance:#{self.id}")
          if cached_instance.blank?
            Rails.logger.debug("[#{self.id}] Cached AppInstance Missing")
            return session
          else
            Rails.logger.debug("[#{self.id}] Cached AppInstance Found")
            return decrypt_data(data: cached_instance, rescue_return: session)
          end
        else
          return session
        end
      end

      def cache_app_instance
        if defined?(Redis.current)
          #Task data must be present and the last refresh cannot be old. We dont want to overwite new cache data with old
          if self.task_data.present? &&  (self.last_refresh.to_i > INSTANCE_REFRESH_WINDOW.ago.to_i)
            Rails.logger.info("[#{self.id}] Caching AppInstance")
            Redis.current.setex("AppInstance:#{self.id}", INSTANCE_REDIS_CACHE_PERIOD.to_i, encrypt_data(data: self.save_data))
          end
          Redis.current.del("Deleted:#{self.id}")
        end
      end

      def save_data(session = Hash.new)
        self.logins.each do |key, login|
          if login.tenant_type == "Zuora"
            if login.available_entities.size > 1 && Rails.application.config.session_store != ActionDispatch::Session::CookieStore
              login.available_entities.each do |entity_key|
                session["#{self.id}::#{key}::#{entity_key}:session"] = login.client(entity_key).current_session
              end
            else
              session["#{self.id}::#{key}:session"] = login.client.current_session
            end
          end
        end

        session["#{self.id}::task_data"] = self.task_data

        #Redis is not defined strip out old data
        if !defined?(Redis.current)
          session["#{self.id}::task_data"].delete('applications')
          session["#{self.id}::task_data"].select {|k,v| k.include?('login') && v['tenant_type'] == 'Zuora'}.each do |login_key, login_data|
            session["#{self.id}::task_data"][login_key]['entities'] = (login_data.dig('entities') || []).map {|entity| entity.slice('id', 'tenantId')}
          end
        end

        session["#{self.id}::last_refresh"] = self.last_refresh
        session["appInstance"] = self.id
        return session
      end

      def encryptor
        # Default values for Rails 4 apps
        key_iter_num, key_size, salt, signed_salt = [1000, 64, "encrypted cookie", "signed encrypted cookie"]
        key_generator = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base, iterations: key_iter_num)
        secret, sign_secret = [key_generator.generate_key(salt), key_generator.generate_key(signed_salt)]
        return ActiveSupport::MessageEncryptor.new(secret, sign_secret)
      end

      def decrypt_data(data: nil, rescue_return: nil)
        return data if data.blank?
        begin
          if Rails.env == 'development'
            return JSON.parse(data)
          else
            begin
              return JSON.parse(encryptor.decrypt_and_verify(CGI::unescape(data)))
            rescue ActiveSupport::MessageVerifier::InvalidSignature => ex
              Rails.logger.fatal('Error Decrypting')
              return rescue_return
            end
          end
        rescue JSON::ParserError => ex
          Rails.logger.fatal('Error Parsing')
          return rescue_return
        end
      end

      def encrypt_data(data: nil)
        return data if data.blank?

        if Rails.env == 'development'
          return data.to_json
        else
          return encryptor.encrypt_and_sign(data.to_json)
        end
      end
    #### END AppInstance Temporary Persistance Methods ####

    ### START Resque Helping Methods ####
      def api_limit(start: true, time: API_LIMIT_TIMEOUT.to_i)
        if start
          Redis.current.setex("APILimits:#{self.id}", time, true)
        else
          Redis.current.del("APILimits:#{self.id}")
        end
      end

      def api_limit?
        return Redis.current.get("APILimits:#{self.id}").to_bool
      end

      def queue_paused?
        return Redis.current.get("resque:PauseQueue:#{self.id}").to_bool
      end

      def queue_pause(time: nil)
        if time.present?
          raise "Time must be fixnum of seconds." if time.class != Fixnum
          Redis.current.setex("resque:PauseQueue:#{self.id}", time, true)
        else
          Redis.current.set("resque:PauseQueue:#{self.id}", true)
        end
      end

      def queue_start
        Redis.current.del("resque:PauseQueue:#{self.id}")
      end
    ### END Resque Helping Methods ####

    ### START Catalog Helping Methods #####
      def get_catalog(page_size: 5, zuora_login: self.login_lookup(type: "Zuora").first, entity_id: nil)
        self.update_column(:catalog_update_attempt_at, Time.now.utc)

        entity_reference = entity_id.blank? ? 'Default' : entity_id
        Rails.logger.debug("Fetch Catalog")
        Rails.logger.debug("Zuora Entity: #{entity_id.blank? ? 'default' : entity_id}")

        login = zuora_login.client(entity_reference)

        old_logger = ActiveRecord::Base.logger
        ActiveRecord::Base.logger = nil
        ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{tmp}\', \'{}\'), "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp}\', \'{}\') where "id" = %{id}' % {:id => self.id})

        response = {'nextPage' => login.rest_endpoint("catalog/products?pageSize=#{page_size}")}
        while !response["nextPage"].blank?
          url = login.rest_endpoint(response["nextPage"].split('/v1/').last)
          Rails.logger.debug("Fetch Catalog URL #{url}")
          output_json, response = login.rest_call(:debug => false, :url => url, :errors => [ZuoraAPI::Exceptions::ZuoraAPISessionError], :timeout_retry => true)
          Rails.logger.debug("Fetch Catalog Response Code #{response.code}")

          if !output_json['success'] =~ (/(true|t|yes|y|1)$/i) || output_json['success'].class != TrueClass
            Rails.logger.error("Fetch Catalog DATA #{output_json.to_json}")
            raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Error Getting Catalog: #{output_json}")
          end

          output_json["products"].each do |product|
            ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp, %s}\', \'%s\') where "id" = %s' % [product["id"], {"productId" => product["id"]}.to_json.gsub("'", "''"), self.id])
            rateplans = {}

            product["productRatePlans"].each do |rateplan|
              ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp, %s}\', \'%s\') where "id" = %s' % [rateplan["id"],  {"productId" => product["id"], "productRatePlanId" => rateplan["id"]}.to_json.gsub("'", "''"), self.id])
              charges = {}

              rateplan["productRatePlanCharges"].each do |charge|
                ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp, %s}\', \'%s\') where "id" = %s' % [charge["id"],  {"productId" => product["id"], "productRatePlanId" => rateplan["id"], "productRatePlanChargeId" => charge["id"]}.to_json.gsub("'", "''"), self.id])

                charges[charge["id"]] = charge.merge({"productId" => product["id"], "productName" => product["name"], "productRatePlanId" => rateplan["id"], "productRatePlanName" => rateplan["name"] })
              end

              rateplan["productRatePlanCharges"] = charges
              rateplans[rateplan["id"]] = rateplan.merge({"productId" => product["id"], "productName" => product["name"]})
            end
            product["productRatePlans"] = rateplans

            ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{tmp, %s}\', \'%s\') where "id" = %s' % [product["id"], product.to_json.gsub("'", "''"), self.id])
          end
        end

        # Move from tmp to actual
        ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{%{entity}}\', "catalog" #> \'{tmp}\'), "catalog_mapping" = jsonb_set("catalog_mapping", \'{%{entity}}\',  "catalog_mapping" #> \'{tmp}\') where "id" = %{id}' % {:entity => entity_reference, :id => self.id})
        if defined?(Redis.current)
          Redis.current.keys("Catalog:#{self.id}:*").each do |key|
            Redis.current.del(key.to_s)
          end
        end
        # Clear tmp holder
        ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{tmp}\', \'{}\'), "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp}\', \'{}\') where "id" = %{id}' % {:id => self.id})

        ActiveRecord::Base.logger = old_logger
        self.update_column(:catalog_updated_at, Time.now.utc)
        self.touch

        # DO NOT RETURN CATALOG. THIS IS NOT SCALABLE WITH LARGE CATALOGS. USE THE  CATALOG_LOOKUP method provided
        return true
      end

      def catalog_outdated?(time: Time.now - 12.hours)
        return self.catalog_updated_at.blank? || (self.catalog_updated_at < time)
      end

      def catalog_loaded?
        return ActiveRecord::Base.connection.execute('SELECT id FROM "public"."zuora_connect_app_instances" WHERE "id" = %s AND catalog = \'{}\' LIMIT 1' % [self.id]).first.nil?
      end

      # Catalog lookup provides method to lookup zuora catalog efficiently.
      # entity_id: If the using catalog json be field to store multiple entity product catalogs.
      # object: The Object class desired to be returned. Available [:product, :rateplan, :charge]
      # object_id: The id or id's of the object/objects to be returned.
      # child_objects: Whether to include child objects of the object in question.
      # cache: Store individual "1" object lookup in redis for caching.
      def catalog_lookup(entity_id: nil, object: :product, object_id: nil, child_objects: false, cache: false)
        entity_reference = entity_id.blank? ? 'Default' : entity_id

        if object_id.present? && ![Array, String].include?(object_id.class)
          raise "Object Id can only be a string or an array of strings"
        end

        if defined?(Redis.current) && object_id.present? && object_id.class == String && object_id.present?
          stub_catalog = decrypt_data(data: Redis.current.get("Catalog:#{self.id}:#{object_id}:Children:#{child_objects}"))
          object_hierarchy = decrypt_data(data: Redis.current.get("Catalog:#{self.id}:#{object_id}:Hierarchy"))
        end

        if defined?(object_hierarchy)
          object_hierarchy ||= (JSON.parse(ActiveRecord::Base.connection.execute('SELECT catalog_mapping #> \'{%s}\' AS item FROM "public"."zuora_connect_app_instances" WHERE "id" = %s LIMIT 1' % [entity_reference, self.id]).first["item"] || "{}") [object_id] || {"productId" => "SAFTEY", "productRatePlanId" => "SAFTEY", "productRatePlanChargeId" => "SAFTEY"})
        end

        case object
        when :product
          if object_id.nil?
            string =
              "SELECT "\
                "json_object_agg(product_id, product #{child_objects ? '' : '- \'productRatePlans\''}) AS item "\
              "FROM "\
                "\"public\".\"zuora_connect_app_instances\", "\
                "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product) "\
              "WHERE "\
                "\"id\" = %s" % [entity_reference, self.id]
          else
            if object_id.class == String
              string =
                "SELECT "\
                  "(catalog #> '{%s, %s}') #{child_objects ? '' : '- \'productRatePlans\''} AS item "\
                "FROM "\
                  "\"public\".\"zuora_connect_app_instances\" "\
                "WHERE "\
                  "\"id\" = %s" % [entity_reference, object_id.blank? ? BLANK_OBJECT_ID_LOOKUP : object_id, self.id]
            elsif object_id.class == Array
              string =
                "SELECT "\
                  "json_object_agg(product_id, product #{child_objects ? '' : '- \'productRatePlans\''}) AS item "\
                "FROM "\
                  "\"public\".\"zuora_connect_app_instances\", "\
                  "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product) "\
                "WHERE "\
                  "\"product_id\" IN (\'%s\') AND "\
                  "\"id\" = %s" % [entity_reference, object_id.join("\',\'"), self.id]
            end
          end

        when :rateplan
          if object_id.nil?
            string =
              "SELECT "\
                "json_object_agg(rateplan_id, rateplan #{child_objects ? '' : '- \'productRatePlanCharges\''}) AS item "\
              "FROM "\
                "\"public\".\"zuora_connect_app_instances\", "\
                "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\
                "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan) "\
              "WHERE "\
                "\"id\" = %s" % [entity_reference, self.id]
          else
            if object_id.class == String
              string =
                "SELECT "\
                  "(catalog #> '{%s, %s, productRatePlans, %s}') #{child_objects ? '' : '- \'productRatePlanCharges\''} AS item "\
                "FROM "\
                  "\"public\".\"zuora_connect_app_instances\" "\
                "WHERE "\
                  "\"id\" = %s" % [entity_reference, object_hierarchy['productId'], object_id.blank? ? BLANK_OBJECT_ID_LOOKUP : object_id,  self.id]
            elsif object_id.class == Array
              string =
                "SELECT "\
                  "json_object_agg(rateplan_id, rateplan #{child_objects ? '' : '- \'productRatePlanCharges\''}) AS item "\
                "FROM "\
                  "\"public\".\"zuora_connect_app_instances\", "\
                  "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\
                  "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan) "\
                "WHERE "\
                  "\"rateplan_id\" IN (\'%s\') AND "\
                  "\"id\" = %s" % [entity_reference, object_id.join("\',\'"), self.id]
            end
          end

        when :charge
          if object_id.nil?
            string =
              "SELECT "\
                "json_object_agg(charge_id, charge) as item "\
              "FROM "\
                "\"public\".\"zuora_connect_app_instances\", "\
                "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\
                "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan), "\
                "jsonb_each(rateplan #> '{productRatePlanCharges}') AS eee(charge_id, charge) "\
              "WHERE "\
                "\"id\" = %s" % [entity_reference, self.id]
          else
            if object_id.class == String
              string =
                "SELECT "\
                  "catalog #> '{%s, %s, productRatePlans, %s, productRatePlanCharges, %s}' AS item "\
                "FROM "\
                  "\"public\".\"zuora_connect_app_instances\" "\
                "WHERE "\
                  "\"id\" = %s" % [entity_reference, object_hierarchy['productId'], object_hierarchy['productRatePlanId'], object_id.blank? ? BLANK_OBJECT_ID_LOOKUP : object_id , self.id]

            elsif object_id.class == Array
              string =
                "SELECT "\
                  "json_object_agg(charge_id, charge) AS item "\
                "FROM "\
                  "\"public\".\"zuora_connect_app_instances\", "\
                  "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\
                  "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan), "\
                  "jsonb_each(rateplan #> '{productRatePlanCharges}') AS eee(charge_id, charge) "\
                "WHERE "\
                  "\"charge_id\" IN (\'%s\') AND "\
                  "\"id\" = %s" % [entity_reference, object_id.join("\',\'"), self.id]
            end
          end
        else
          raise "Available objects include [:product, :rateplan, :charge]"
        end

        stub_catalog ||= JSON.parse(ActiveRecord::Base.connection.execute(string).first["item"] || "{}")

        if defined?(Redis.current) && object_id.present? && object_id.class == String && object_id.present?
          Redis.current.set("Catalog:#{self.id}:#{object_id}:Hierarchy", encrypt_data(data: object_hierarchy))
          Redis.current.set("Catalog:#{self.id}:#{object_id}:Children:#{child_objects}", encrypt_data(data: stub_catalog))  if cache
        end

        return stub_catalog
      end
    ### END Catalog Helping Methods #####

    ### START S3 Helping Methods #####
      def s3_client
        require 'aws-sdk-s3'
        if ZuoraConnect.configuration.mode == "Development"
          @s3_client ||= Aws::S3::Resource.new(region: ZuoraConnect.configuration.aws_region,access_key_id: ZuoraConnect.configuration.dev_mode_access_key_id,secret_access_key: ZuoraConnect.configuration.dev_mode_secret_access_key)
        else
          @s3_client ||= Aws::S3::Resource.new(region: ZuoraConnect.configuration.aws_region)
        end
      end

      def upload_to_s3(local_file,s3_path = nil)
        s3_path = local_file.split("/").last if s3_path.nil?
        obj = self.s3_client.bucket(ZuoraConnect.configuration.s3_bucket_name).object("#{ZuoraConnect.configuration.s3_folder_name}/#{self.id.to_s}/#{s3_path}}")
        obj.upload_file(local_file, :server_side_encryption => 'AES256')
      end

      def get_s3_file_url(key)
        require 'aws-sdk-s3'
        signer = Aws::S3::Presigner.new(client: self.s3_client)
        url = signer.presigned_url(:get_object, bucket: ZuoraConnect.configuration.s3_bucket_name, key: "#{ZuoraConnect.configuration.s3_folder_name}/#{self.id.to_s}/#{key}")
      end
    ### END S3 Helping Methods #####

    ### START Aggregate Grouping Helping Methods ####
      def self.refresh_aggregate_table(aggregate_name: 'all_tasks_processing', table_name: 'tasks', where_clause: "where status in ('Processing', 'Queued')", index_table: true)
        self.update_functions
        #Broke function into two parts to ensure transaction size was small enough
        ActiveRecord::Base.connection.execute('SELECT "shared_extensions".refresh_aggregate_table(\'%s\', \'%s\', %s, \'Table\');' % [aggregate_name, table_name, ActiveRecord::Base.connection.quote(where_clause)])
        ActiveRecord::Base.connection.execute('SELECT "shared_extensions".refresh_aggregate_table(\'%s\', \'%s\', %s, \'Index\');' % [aggregate_name, table_name, ActiveRecord::Base.connection.quote(where_clause)]) if index_table
      end

      def self.update_functions
        ActiveRecord::Base.connection.execute(File.read("#{Gem.loaded_specs["zuora_connect"].gem_dir}/app/views/sql/refresh_aggregate_table.txt"))
      end
    ### END Aggregate Grouping Helping Methods #####

    # Overide this method to avoid the new session call for api requests that use the before filter authenticate_app_api_request.
    # This can be usefull for apps that dont need connect metadata call, or credentials, to operate for api requests
    def new_session_for_api_requests(params: {})
      return true
    end

    # Overide this method to avoid the new session call for ui requests that use the before filter authenticate_connect_app_request.
    # This can be usefull for apps that dont need connect metadata call, or credentials, to operate for ui requests
    def new_session_for_ui_requests(params: {})
      return true
    end

    def reload_attributes(selected_attributes)
      raise "Attibutes must be array" if selected_attributes.class != Array
      value_attributes = self.class.unscoped.where(:id=>id).select(selected_attributes).first.attributes
      value_attributes.each do |key, value|
        next if key == "id" && value.blank?
        self.send(:write_attribute, key, value)
      end
      return self
    end

    def instance_failure(failure)
      raise failure
    end

    def send_email
    end

    def login_lookup(type: "Zuora")
      results = []
      self.logins.each do |name, login|
        results << login if login.tenant_type == type
      end
      return results
    end

    def self.decrypt_response(resp)
      OpenSSL::PKey::RSA.new(ZuoraConnect.configuration.private_key).private_decrypt(resp)
    end

    def attr_builder(field,val)
      singleton_class.class_eval { attr_accessor "#{field}" }
      send("#{field}=", val)
    end

    def method_missing(method_sym, *arguments, &block)
      if method_sym.to_s.include?("login")
        Rails.logger.fatal("Method Missing #{method_sym}")
        Rails.logger.fatal("Instance Data: #{self.task_data}")
        Rails.logger.fatal("Instance Logins: #{self.logins}")
      end
      super
    end


    method_hook :refresh, :updateOption, :update_logins, :before => :check_oauth_state
    method_hook :new_session, :refresh, :build_task, :after => :apartment_switch
  end
end