module Gitlab
  module ImportExport
    class RelationFactory
      OVERRIDES = { snippets: :project_snippets,
                    pipelines: 'Ci::Pipeline',
                    statuses: 'commit_status',
                    variables: 'Ci::Variable',
                    triggers: 'Ci::Trigger',
                    builds: 'Ci::Build',
                    hooks: 'ProjectHook' }.freeze

      USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze

      BUILD_MODELS = %w[Ci::Build commit_status].freeze

      def self.create(*args)
        new(*args).create
      end

      def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
        @relation_name = OVERRIDES[relation_sym] || relation_sym
        @relation_hash = relation_hash.except('id', 'noteable_id')
        @members_mapper = members_mapper
        @user = user
      end

      # Creates an object from an actual model with name "relation_sym" with params from
      # the relation_hash, updating references with new object IDs, mapping users using
      # the "members_mapper" object, also updating notes if required.
      def create
        set_note_author if @relation_name == :notes
        update_user_references
        update_project_references
        reset_ci_tokens if @relation_name == 'Ci::Trigger'
        @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']

        generate_imported_object
      end

      private

      def update_user_references
        USER_REFERENCES.each do |reference|
          if @relation_hash[reference]
            @relation_hash[reference] = @members_mapper.map[@relation_hash[reference]]
          end
        end
      end

      # Sets the author for a note. If the user importing the project
      # has admin access, an actual mapping with new project members
      # will be used. Otherwise, a note stating the original author name
      # is left.
      def set_note_author
        old_author_id = @relation_hash['author_id']

        # Users with admin access can map users
        @relation_hash['author_id'] = admin_user? ? @members_mapper.map[old_author_id] : @members_mapper.default_user_id

        author = @relation_hash.delete('author')

        update_note_for_missing_author(author['name']) if missing_author?(old_author_id)
      end

      def missing_author?(old_author_id)
        !admin_user? || @members_mapper.missing_author_ids.include?(old_author_id)
      end

      def missing_author_note(updated_at, author_name)
        timestamp = updated_at.split('.').first
        "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*"
      end

      def generate_imported_object
        if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
          trace = @relation_hash.delete('trace')
          imported_object do |object|
            object.trace = trace
            object.commit_id = nil
          end
        else
          imported_object
        end
      end

      def update_project_references
        project_id = @relation_hash.delete('project_id')

        # project_id may not be part of the export, but we always need to populate it if required.
        @relation_hash['project_id'] = project_id if relation_class.column_names.include?('project_id')
        @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
        @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
        @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id']

        # If source and target are the same, populate them with the new project ID.
        if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] &&
          @relation_hash['target_project_id'] == @relation_hash['source_project_id']
          @relation_hash['source_project_id'] = project_id
        end
      end

      def reset_ci_tokens
        return unless Gitlab::ImportExport.reset_tokens?

        # If we import/export a project to the same instance, tokens will have to be reset.
        @relation_hash['token'] = nil
      end

      def relation_class
        @relation_class ||= @relation_name.to_s.classify.constantize
      end

      def imported_object
        imported_object = relation_class.new(@relation_hash)
        yield(imported_object) if block_given?
        imported_object.importing = true if imported_object.respond_to?(:importing)
        imported_object
      end

      def update_note_for_missing_author(author_name)
        @relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank?
        @relation_hash['note'] += missing_author_note(@relation_hash['updated_at'], author_name)
      end

      def admin_user?
        @user.is_admin?
      end
    end
  end
end