# frozen_string_literal: true

module ReleaseTools
  module PublicRelease
    # A release of Omnibus GitLab using the API.
    class OmnibusGitlabRelease
      include Release

      attr_reader :version, :client, :release_metadata

      VERSION_FILE = 'VERSION'

      VERSION_FILES = [
        Project::Gitaly.version_file,
        Project::GitlabPages.version_file,
        Project::GitlabShell.version_file,
        Project::GitlabElasticsearchIndexer.version_file,
        Project::Kas.version_file
      ].freeze

      VARIABLES_FILE = 'gitlab-ci-config/variables.yml'
      # We started updating the QA ref as part of release, and thus using a
      # separate variables.yml file, in GitLab 17.3.0
      # https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/7805
      SEPARATE_VARIABLES_FILE_VERSION = '17.3.0'

      PRE_INSTALL_SCRIPT = 'config/templates/package-scripts/preinst.erb'
      UPGRADE_CHECK_SCRIPT = 'files/gitlab-ctl-commands/lib/gitlab_ctl/upgrade_check.rb'

      def initialize(
        version,
        client: GitlabClient,
        release_metadata: ReleaseMetadata.new,
        commit: nil
      )
        unless version.ee?
          raise ArgumentError, "#{version} is not an EE version"
        end

        @version = version
        @gitlab_version = Version.new(version.to_normalized_version)
        @client = client
        @release_metadata = release_metadata
        @commit = commit
      end

      def execute
        logger.info(
          'Starting release of Omnibus GitLab',
          ee_version: version,
          ce_version: version.to_ce
        )

        verify_version_file
        create_target_branch

        return if SharedStatus.dry_run?

        compile_changelog
        update_component_versions
        update_last_upgrade_stop

        ce_tag = create_ce_tag
        ee_tag = create_ee_tag

        add_release_data_for_tags(ce_tag, ee_tag)
        notify_slack(project, version)
      end

      def verify_version_file
        return if SharedStatus.dry_run?

        path = ee_project_path
        branch = ee_stable_branch
        from_file = read_version(path, VERSION_FILE, branch)

        return if @gitlab_version == from_file

        raise "The VERSION file in #{path} on branch #{branch} specifies " \
              "version #{from_file}, but the GitLab version we are releasing " \
              "is version #{@gitlab_version}. These versions must be identical " \
              "before we can proceed"
      end

      def compile_changelog
        return if version.rc?

        logger.info('Compiling changelog', project: project_path)

        ChangelogCompiler
          .new(project_path, client: client)
          .compile(version, branch: target_branch)
      end

      def create_ce_tag
        tag = version.to_ce.tag

        logger.info('Creating CE tag', tag: tag, project: ce_project_path)
        find_or_create_tag(tag, ce_project_path, ce_stable_branch)
      end

      def create_ee_tag
        tag = tag_name

        logger.info('Creating EE tag', tag: tag, project: ee_project_path)
        find_or_create_tag(tag, ee_project_path, ee_stable_branch)
      end

      def find_or_create_tag(tag, gitlab_project, gitlab_branch)
        Retriable.with_context(:api) do
          client.tag(project_path, tag: tag)
        rescue Gitlab::Error::NotFound
          gitlab_version =
            read_version(gitlab_project, VERSION_FILE, gitlab_branch)

          update_variables(gitlab_project, gitlab_branch) if version >= Version.new(SEPARATE_VARIABLES_FILE_VERSION)

          update_version_file(gitlab_version)

          # Omnibus requires annotated tags to build packages, so we must
          # specify a message.
          client.create_tag(project_path, tag, target_branch, "Version #{tag}")
        end
      end

      def update_variables(gitlab_project, gitlab_branch)
        variables_yaml = read_file(VARIABLES_FILE)
        variables_data = YAML.safe_load(variables_yaml)
        variables_data['variables'].merge!(
          'QA_TESTS_UPSTREAM_PROJECT' => gitlab_project,
          'QA_TESTS_REF' => gitlab_branch
        )

        commit_version_files(
          target_branch,
          { VARIABLES_FILE => YAML.dump(variables_data) },
          message: "Set QA test targets for #{version}\n\n[ci skip]"
        )
      end

      # Updates the contents of all component versions (e.g.
      # GITALY_SERVER_VERSION) according to their contents in the GitLab EE
      # repository.
      def update_component_versions
        logger.info(
          'Updating component versions',
          project: project_path,
          branch: target_branch
        )

        branch = ee_stable_branch

        versions = VERSION_FILES.each_with_object({}) do |file, hash|
          hash[file] = read_version(ee_project_path, file, branch).to_s
        end

        commit_version_files(
          target_branch,
          versions,
          message: 'Update component version files'
        )
      end

      def update_version_file(version)
        commit_version_files(
          target_branch,
          { VERSION_FILE => version },
          message: "Update #{VERSION_FILE} to #{version}"
        )
      end

      def update_last_upgrade_stop
        return if ReleaseTools::Feature.disabled?(:maintain_upgrade_stops)

        begin
          stop = UpgradeStop.new.last_required_stop
          logger.info('Last required stop for version detected', last_required_stop: stop)

          actions = []
          actions << change_upgrade_stop_action!(PRE_INSTALL_SCRIPT, /(?<=[\s]|^)MIN_VERSION=[^\n]*/, "MIN_VERSION=#{stop}")
          actions << change_upgrade_stop_action!(UPGRADE_CHECK_SCRIPT, /ENV\['MIN_VERSION'\] [\|]{2} '[0-9.]*'/, "ENV['MIN_VERSION'] || '#{stop}'")
          actions.compact!

          if actions.empty?
            logger.info('Nothing to be done to apply upgrade stop to Omnibus', version: version.to_minor, last_required_stop: stop)
            return
          end

          Retriable.with_context(:api) do
            client.create_commit(
              project_path,
              project.default_branch,
              "Update upgrade stop to #{stop}\n\n[ci skip]",
              actions
            )
          end
        rescue StandardError
          logger.fatal('Something went wrong with the Omnibus upgrade stop. Disable the \'maintain_upgrade_stops\' feature flag, and retry the job. Please notify Distribution about this error.')

          raise
        end
      end

      def change_upgrade_stop_action!(filename, regexp, replacement)
        content = read_file(filename, branch: project.default_branch)
        raise "The regexp '#{regexp}' does not match in #{filename}" unless regexp.match?(content)

        new_content = content.sub(regexp, replacement)

        return nil if content == new_content

        {
          action: 'update',
          file_path: filename,
          content: new_content
        }
      end

      def add_release_data_for_tags(ce_tag, ee_tag)
        meta_version = version.to_normalized_version

        logger.info(
          'Recording release data',
          project: project_path,
          version: meta_version,
          ce_tag: ce_tag.name,
          ee_tag: ee_tag.name
        )

        release_metadata.add_release(
          name: 'omnibus-gitlab-ce',
          version: meta_version,
          sha: ce_tag.commit.id,
          ref: ce_tag.name,
          tag: true
        )

        release_metadata.add_release(
          name: 'omnibus-gitlab-ee',
          version: meta_version,
          sha: ee_tag.commit.id,
          ref: ee_tag.name,
          tag: true
        )
      end

      def read_file(file, project: project_path, branch: target_branch)
        Retriable.with_context(:api) do
          client.file_contents(project, file, branch)
        end
      end

      def read_version(project, name, branch)
        Version.new(read_file(name, project: project, branch: branch))
      end

      def source_for_target_branch
        if @commit
          logger.info('Using specific commit', project: project_path, commit: @commit)
        end

        @commit || super
      end

      def project
        Project::OmnibusGitlab
      end

      def ce_project_path
        Project::GitlabCe.canonical_or_security_path
      end

      def ee_project_path
        Project::GitlabEe.canonical_or_security_path
      end

      def ee_stable_branch
        @gitlab_version.stable_branch(ee: true)
      end

      def ce_stable_branch
        @gitlab_version.stable_branch
      end

      # release/metadata is unsuitable for tagging omnibus as it will
      # always track an "update components version" commit that only
      # exists on the security mirror as we don't create auto-deploy
      # branches from canonical.
      #
      # However this is not a problem as the content of that commit
      # will always be overwritten by the public release code commit
      # the final versions of each components from the
      # update_component_versions method.
      def last_production_commit
        last_production_commit_deployments
      end
    end
  end
end
