module Gitlab
  module ImportExport
    class Reader

      attr_reader :tree

      def initialize(shared:)
        @shared = shared
        config_hash = YAML.load_file(Gitlab::ImportExport.config_file).deep_symbolize_keys
        @tree = config_hash[:project_tree]
        @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes],
                                                                        excluded_attributes: config_hash[:excluded_attributes],
                                                                        methods: config_hash[:methods])
      end

      # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
      # for outputting a project in JSON format, including its relations and sub relations.
      def project_tree
        @attributes_finder.find_included(:project).merge(include: build_hash(@tree))
      rescue => e
        @shared.error(e)
        false
      end

      private

      # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
      #
      # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file
      def build_hash(model_list)
        model_list.map do |model_objects|
          if model_objects.is_a?(Hash)
            build_json_config_hash(model_objects)
          else
            @attributes_finder.find(model_objects)
          end
        end
      end

      # Called when the model is actually a hash containing other relations (more models)
      # Returns the config in the right format for calling +to_json+
      # +model_object_hash+ - A model relationship such as:
      #   {:merge_requests=>[:merge_request_diff, :notes]}
      def build_json_config_hash(model_object_hash)
        @json_config_hash = {}

        model_object_hash.values.flatten.each do |model_object|
          current_key = model_object_hash.keys.first

          @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash }

          handle_model_object(current_key, model_object)
          process_sub_model(current_key, model_object) if model_object.is_a?(Hash)
        end
        @json_config_hash
      end


      # If the model is a hash, process the sub_models, which could also be hashes
      # If there is a list, add to an existing array, otherwise use hash syntax
      # +current_key+ main model that will be a key in the hash
      # +model_object+ model or list of models to include in the hash
      def process_sub_model(current_key, model_object)
        sub_model_json = build_json_config_hash(model_object).dup
        @json_config_hash.slice!(current_key)

        if @json_config_hash[current_key] && @json_config_hash[current_key][:include]
          @json_config_hash[current_key][:include] << sub_model_json
        else
          @json_config_hash[current_key] = { include: sub_model_json }
        end
      end

      # Creates or adds to an existing hash an individual model or list
      # +current_key+ main model that will be a key in the hash
      # +model_object+ model or list of models to include in the hash
      def handle_model_object(current_key, model_object)
        if @json_config_hash[current_key]
          add_model_value(current_key, model_object)
        else
          create_model_value(current_key, model_object)
        end
      end

      # Constructs a new hash that will hold the configuration for that particular object
      # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
      # +current_key+ main model that will be a key in the hash
      # +value+ existing model to be included in the hash
      def create_model_value(current_key, value)
        parsed_hash = { include: value }

        @attributes_finder.parse(value) do |hash|
          parsed_hash = { include: hash_or_merge(value, hash) }
        end
        @json_config_hash[current_key] = parsed_hash
      end

      # Adds new model configuration to an existing hash with key +current_key+
      # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
      # +current_key+ main model that will be a key in the hash
      # +value+ existing model to be included in the hash
      def add_model_value(current_key, value)
        @attributes_finder.parse(value) { |hash| value = { value => hash } }
        old_values = @json_config_hash[current_key][:include]
        @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
      end

      # Construct a new hash or merge with an existing one a model configuration
      # This is to fulfil +to_json+ requirements.
      # +value+ existing model to be included in the hash
      # +hash+ hash containing configuration generated mainly from +@attributes_finder+
      def hash_or_merge(value, hash)
        value.is_a?(Hash) ? value.merge(hash) : { value => hash }
      end
    end
  end
end
