lib/canvas_sync/job_batches/context_hash.rb



module CanvasSync
  module JobBatches
    class ContextHash
      delegate_missing_to :flatten

      def initialize(bid, hash = nil)
        @bid_stack = [bid]
        @hash_map = {}
        @dirty = false
        @flattened = nil
        @hash_map[bid] = hash.with_indifferent_access if hash
      end

      # Local is "the nearest batch with a context value"
      # This allows for, for example, SerialBatchJob to have a modifiable context stored on it's main Batch
      # that can be accessed transparently from one of it's internal, context-less Batches
      def local_bid
        bid = @bid_stack[-1]
        while bid.present?
          bhash = resolve_hash(bid)
          return bid if bhash
          bid = get_parent_bid(bid)
        end
        nil
      end

      def local
        @hash_map[local_bid]
      end

      def own
        resolve_hash(@bid_stack[-1]) || {}
      end

      def set_local(new_hash)
        @dirty = true
        local.clear.merge!(new_hash)
      end

      def clear
        local.clear
        @flattened = nil
        @dirty = true
        self
      end

      def []=(key, value)
        @flattened = nil
        @dirty = true
        local[key] = value
      end

      def [](key)
        bid = @bid_stack[-1]
        while bid.present?
          bhash = resolve_hash(bid)
          return bhash[key] if bhash&.key?(key)
          bid = get_parent_bid(bid)
        end
        nil
      end

      def reload!
        @dirty = false
        @hash_map = {}
        self
      end

      def save!(force: false)
        return unless dirty? || force
        Batch.redis do |r|
          r.hset("BID-#{local_bid}", 'context', JSON.unparse(local))
        end
      end

      def dirty?
        @dirty
      end

      def is_a?(arg)
        return true if Hash <= arg
        super
      end

      def flatten
        return @flattened if @flattened

        load_all
        flattened = {}
        @bid_stack.compact.each do |bid|
          flattened.merge!(@hash_map[bid]) if @hash_map[bid]
        end
        flattened.freeze

        @flattened = flattened.with_indifferent_access
      end

      def to_h
        flatten
      end

      private

      def get_parent_hash(bid)
        resolve_hash(get_parent_bid(bid)).freeze
      end

      def get_parent_bid(bid)
        index = @bid_stack.index(bid)
        raise "Invalid BID #{bid}" if index.nil? # Sanity Check - this shouldn't happen

        index -= 1
        if index >= 0
          @bid_stack[index]
        else
          pbid = Batch.redis do |r|
            callback_params = JSON.parse(r.hget("BID-#{bid}", "callback_params") || "{}")
            callback_params['for_bid'] || r.hget("BID-#{bid}", "parent_bid")
          end
          @bid_stack.unshift(pbid)
          pbid
        end
      end

      def resolve_hash(bid)
        return nil unless bid.present?
        return @hash_map[bid] if @hash_map.key?(bid)

        context_json, editable = Batch.redis do |r|
          r.multi do |r|
            r.hget("BID-#{bid}", "context")
            r.hget("BID-#{bid}", "allow_context_changes")
          end
        end

        if context_json.present?
          context_hash = JSON.parse(context_json)
          context_hash = context_hash.with_indifferent_access
          context_hash.each do |k, v|
            v.freeze
          end
          context_hash.freeze unless editable

          @hash_map[bid] = context_hash
        else
          @hash_map[bid] = nil
        end
      end

      def load_all
        resolve_hash(@bid_stack[0]).freeze
        while @bid_stack[0].present?
          get_parent_hash(@bid_stack[0])
        end
        @hash_map
      end
    end
  end
end