# frozen_string_literal: true

module Storage
  module LegacyNamespace
    extend ActiveSupport::Concern

    include Gitlab::ShellAdapter

    def move_dir
      proj_with_tags = first_project_with_container_registry_tags

      if proj_with_tags
        raise Gitlab::UpdatePathError, "Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry"
      end

      parent_was = if saved_change_to_parent? && parent_id_before_last_save.present?
                     Namespace.find(parent_id_before_last_save) # raise NotFound early if needed
                   end

      move_repositories

      if saved_change_to_parent?
        former_parent_full_path = parent_was&.full_path
        parent_full_path = parent&.full_path
        Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
      else
        Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path)
      end

      # If repositories moved successfully we need to
      # send update instructions to users.
      # However we cannot allow rollback since we moved namespace dir
      # So we basically we mute exceptions in next actions
      begin
        send_update_instructions
        write_projects_repository_config
      rescue StandardError => e
        Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
          full_path_before_last_save: full_path_before_last_save,
          full_path: full_path,
          action: 'move_dir')
      end

      true # false would cancel later callbacks but not rollback
    end

    # Hooks

    # Save the storages before the projects are destroyed to use them on after destroy
    def prepare_for_destroy
      old_repository_storages
    end

    private

    def move_repositories
      # Move the namespace directory in all storages used by member projects
      repository_storages(legacy_only: true).each do |repository_storage|
        # Ensure old directory exists before moving it
        Gitlab::GitalyClient::NamespaceService.allow do
          gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)

          # Ensure new directory exists before moving it (if there's a parent)
          gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent

          unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)

            Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}")

            # if we cannot move namespace directory we should rollback
            # db changes in order to prevent out of sync between db and fs
            raise Gitlab::UpdatePathError, 'namespace directory cannot be moved'
          end
        end
      end
    end

    def old_repository_storages
      @old_repository_storage_paths ||= repository_storages(legacy_only: true)
    end

    def repository_storages(legacy_only: false)
      # We need to get the storage paths for all the projects, even the ones that are
      # pending delete. Unscoping also get rids of the default order, which causes
      # problems with SELECT DISTINCT.
      Project.unscoped do
        namespace_projects = all_projects
        namespace_projects = namespace_projects.without_storage_feature(:repository) if legacy_only
        namespace_projects.pluck(Arel.sql('distinct(repository_storage)'))
      end
    end

    def rm_dir
      # Remove the namespace directory in all storages paths used by member projects
      old_repository_storages.each do |repository_storage|
        # Move namespace directory into trash.
        # We will remove it later async
        new_path = "#{full_path}+#{id}+deleted"

        Gitlab::GitalyClient::NamespaceService.allow do
          if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
            Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")

            # Remove namespace directory async with delay so
            # GitLab has time to remove all projects first
            run_after_commit do
              GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
            end
          end
        end
      end
    end
  end
end
