module Katello
  module LazyAccessor
    def self.included(base)
      base.send :include, LazyAccessor::InstanceMethods
      base.send :extend, LazyAccessor::ClassMethods
    end

    module ClassMethods
      attr_accessor :lazy_attributes

      def lazy_attributes_options(attr)
        if @lazy_attributes_options&.key?(attr)
          @lazy_attributes_options.fetch(attr.to_s)
        elsif superclass.respond_to?(:lazy_attributes_options)
          superclass.lazy_attributes_options(attr.to_s)
        else
          fail "lazy attribute #{attr} not defined"
        end
      end

      # @example lazy_accessor :a, :b, :c,
      #   :initializer => lambda { json = Resources::Candlepin::Product.get(cp_id)[0] },
      #   :unless => lambda { cp_id.nil? }
      def lazy_accessor(*args)
        options = args.extract_options!
        @lazy_attributes = [] if @lazy_attributes.nil?
        @lazy_attributes.concat args
        @lazy_attributes_options ||= {}
        fail ArgumentError, "Attribute names must be symbols" if args.any? { |attribute| !attribute.is_a?(Symbol) }
        redefined_attr = args.find { |attribute| instance_methods.include?(attribute.to_s) }
        Rails.logger.warn "Remote attribute '#{redefined_attr}' has already been defined" if redefined_attr

        fail ArgumentError, "Please provide an initializer" if options[:initializer].nil?

        args.each do |symbol|
          options[:in_group] = args.size > 1
          @lazy_attributes_options[symbol.to_s] = options

          send :define_method, "#{symbol.to_s}=" do |val|
            lazy_attribute_set(symbol, val)
          end

          send :define_method, symbol do
            lazy_attribute_get(symbol)
          end
        end
      end
    end

    module InstanceMethods
      def changed_remote_attributes
        @changed_remote_attributes ||= {}
      end

      def changed_remote_attributes=(val)
        @changed_remote_attributes = val
      end

      def save(...)
        if (status = super(...))
          changed_remote_attributes.clear
        end
        status
      end

      def save!(...)
        super(...).tap do
          changed_remote_attributes.clear
        end
      end

      def reload(...)
        super(...).tap do
          changed_remote_attributes.clear
        end
      end

      def lazy_attributes
        attrs = (self.class.superclass.respond_to? :lazy_attributes) ? self.class.superclass.lazy_attributes : []
        attrs += (self.class.lazy_attributes || [])
        attrs.uniq
      end

      private

      def lazy_attribute_set(attr, val)
        instance_variable_set("@#{attr.to_s}", val)
      end

      def lazy_attribute_get(attr)
        attr = attr.to_s

        options = self.class.lazy_attributes_options(attr)

        excepted = options.key?(:unless) ? self.instance_eval(&options[:unless]) : (self.is_a?(ActiveRecord::Base) && self.new_record?)
        if !instance_variable_defined?("@#{attr}") && !excepted
          remote_values = run_initializer(options[:in_group], options[:initializer])
          if options[:in_group]
            prepopulate(remote_values)
          else
            instance_variable_set("@#{attr}", remote_values) if respond_to?("#{attr}=")
          end
        end
        instance_variable_get("@#{attr}")
      end

      def remote_attribute_value(attr)
        return nil if self.is_a?(ActiveRecord::Base) && self.new_record?

        options = self.class.lazy_attributes_options(attr)
        initializer, in_group = options[:initializer], options[:in_group]

        remote_values = run_initializer(in_group, initializer)
        changed_remote_attributes[attr] = in_group ? remote_values["#{attr}"] : remote_values
      end

      def run_initializer(in_group, initializer)
        remote_values = self.instance_eval(&initializer)
        if in_group && !remote_values.is_a?(Hash)
          fail "Expect initializer to return hash if a group of attributes is defined by lazy_accessor"
        end
        remote_values
      end

      def prepopulate(remote_values)
        attrs = self.lazy_attributes
        # if +load_remote_data+ is defined, use it to populate the instance variables
        if self.respond_to?(:load_remote_data)
          load_remote_data(remote_values)
        else
          remote_values.each_pair do |k, v|
            instance_variable_set("@#{k.to_s}", v) if (attrs&.include?(k.to_sym) && respond_to?("#{k.to_s}="))
          end
        end
      end
    end
  end
end
