# frozen_string_literal: true

module Projects
  module ImportExport
    class ExportService < BaseService
      include Gitlab::InternalEventsTracking

      def initialize(*args)
        super

        @shared = project.import_export_shared
        @logger = Gitlab::Export::Logger.build
      end

      def execute(after_export_strategy = nil)
        unless project.template_source? || can?(current_user, :admin_project, project)
          raise ::Gitlab::ImportExport::Error.permission_error(current_user, project)
        end

        save_all!
        execute_after_export_action(after_export_strategy)
      ensure
        cleanup
      end

      def exporters
        [
          version_saver, avatar_saver, project_tree_saver, uploads_saver,
          repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver, design_repo_saver,
          max_iids_saver,
          (commit_notes_saver if commit_notes_export_via_repo?)
        ].compact
      end

      protected

      def extra_attributes_for_measurement
        {
          current_user: current_user&.name,
          project_full_path: project&.full_path,
          file_path: shared.export_path
        }
      end

      private

      attr_accessor :shared
      attr_reader :logger

      def execute_after_export_action(after_export_strategy)
        return unless after_export_strategy

        unless after_export_strategy.execute(current_user, project)
          notify_error
        end
      end

      def save_all!
        log_info('Project export started')

        track_internal_event(
          'start_project_export',
          user: current_user,
          project: project,
          namespace: project.namespace
        )

        if save_exporters && save_export_archive
          log_info('Project successfully exported')
        else
          notify_error!
        end
      end

      def save_exporters
        exporters.all? do |exporter|
          log_info("#{exporter.class.name} saver started")

          exporter.save
        end
      end

      def save_export_archive
        @export_saver ||= Gitlab::ImportExport::Saver.save(exportable: project, shared: shared, user: current_user)
      end

      def version_saver
        @version_saver ||= Gitlab::ImportExport::VersionSaver.new(shared: shared)
      end

      def avatar_saver
        @avatar_saver ||= Gitlab::ImportExport::AvatarSaver.new(project: project, shared: shared)
      end

      def project_tree_saver
        @project_tree_saver ||= tree_saver_class.new(
          project: project,
          current_user: current_user,
          shared: shared,
          params: tree_saver_params,
          logger: logger)
      end

      # Excludes commit_notes from the tree saver so it doesn't overwrite the
      # `tree/project/commit_notes.ndjson` written by CommitNotesSaver.
      def tree_saver_params
        return params unless commit_notes_export_via_repo?

        existing = Array.wrap(params[:excluded_relations])
        params.merge(excluded_relations: existing + [Projects::ImportExport::RelationExport::COMMIT_NOTES_RELATION])
      end

      def commit_notes_export_via_repo?
        Feature.enabled?(:commit_notes_export_via_repo, project.root_ancestor)
      end

      def tree_saver_class
        Gitlab::ImportExport::Project::TreeSaver
      end

      def uploads_saver
        @uploads_saver ||= Gitlab::ImportExport::UploadsSaver.new(project: project, shared: shared)
      end

      def repo_saver
        @repo_saver ||= Gitlab::ImportExport::RepoSaver.new(exportable: project, shared: shared)
      end

      def wiki_repo_saver
        @wiki_repo_saver ||= Gitlab::ImportExport::WikiRepoSaver.new(exportable: project, shared: shared)
      end

      def lfs_saver
        @lfs_saver ||= Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared)
      end

      def snippets_repo_saver
        @snippets_repo_saver ||= Gitlab::ImportExport::SnippetsRepoSaver.new(
          current_user: current_user,
          project: project,
          shared: shared
        )
      end

      def design_repo_saver
        @design_repo_saver ||= Gitlab::ImportExport::DesignRepoSaver.new(exportable: project, shared: shared)
      end

      def max_iids_saver
        @max_iids_saver ||= Gitlab::ImportExport::Project::MaxIidsSaver.new(project: project, shared: shared)
      end

      def commit_notes_saver
        @commit_notes_saver ||= ::Import::Export::Project::CommitNotesSaver.new(
          project: project,
          shared: shared,
          user: current_user,
          params: params
        )
      end

      def cleanup
        FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
      end

      def notify_error!
        notify_error

        raise Gitlab::ImportExport::Error, shared.errors.to_sentence
      end

      def log_info(message)
        logger.info(
          message: message,
          **log_base_data
        )
      end

      def notify_error
        logger.error(
          message: 'Project export error',
          export_errors: shared.errors.join(', '),
          **log_base_data
        )

        user = current_user
        errors = shared.errors

        project.run_after_commit_or_now do |project|
          NotificationService.new.project_not_exported(project, user, errors)
        end
      end

      def log_base_data
        @log_base_data ||= Gitlab::ImportExport::LogUtil.exportable_to_log_payload(project)
      end
    end
  end
end
