# frozen_string_literal: true

module ReleaseTools
  module PublicRelease
    # A release of our CNG image using the API.
    class CNGImageRelease
      include Release

      attr_reader :version, :client, :release_metadata

      DEFAULT_UBI_VERSION = '8'
      GITLAB_FIPS_MINIMUM_VERSION = '15.1.0' # When https://gitlab.com/gitlab-org/build/CNG/-/merge_requests/981 was introduced
      GITLAB_MAILROOM_MINIMUM_VERSION = '15.11.0' # When https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116494 was merged
      UBI_DROP_SUFFIX_VERSION = '17.3.0' # When https://gitlab.com/gitlab-org/build/CNG/-/merge_requests/1888 was merged

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

      # Variables that stores version information.
      # Even though it says GITLAB_REF_SLUG, because of what CNG expects in
      # tag/release pipelines, it stores the non-slugified form of the tag. CNG
      # expects the slug form only when pulling GitLab rails from a branch, not
      # when from a tag. Hence, this variable is under `GITLAB_VERSION_FILES`
      # and not `GITLAB_SLUG_FILES`.
      GITLAB_VERSION_FILES =
        %w[GITLAB_VERSION GITLAB_REF_SLUG].freeze

      # Variables that stores version slug information.
      GITLAB_SLUG_FILES = %w[GITLAB_ASSETS_TAG].freeze

      VARIABLES_FILE = 'ci_files/variables.yml'

      def initialize(
        version,
        client: GitlabClient,
        release_metadata: ReleaseMetadata.new,
        ubi_version: DEFAULT_UBI_VERSION,
        commit: nil
      )
        @version = version.to_ee
        @gitlab_version = Version.new(@version.to_normalized_version)
        @client = client
        @release_metadata = release_metadata
        @ubi_version = ubi_version
        @commit = commit
      end

      def execute
        logger.info('Starting release of CNG', version: version)

        create_target_branch

        return if SharedStatus.dry_run?

        ce_tag, ee_tag, ubi_tag, fips_tag = update_component_versions

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

      def update_component_versions
        logger.info(
          'Updating component versions',
          project: project_path,
          branch: target_branch
        )

        ce_data, ee_data = distribution_component_versions

        [
          create_ce_tag(ce_data),
          create_ee_tag(ee_data),
          create_ubi_tag,
          create_fips_tag
        ]
      end

      def create_ce_tag(variables)
        find_or_create_tag(version.to_ce.tag, variables)
      end

      def create_ee_tag(variables)
        find_or_create_tag(version.to_ee.tag, variables)
      end

      def create_ubi_tag
        tag = if version >= Version.new(UBI_DROP_SUFFIX_VERSION) && Feature.enabled?(:drop_ubi_version_suffix)
                version.tag(ee: true).gsub(/-ee$/, "-ubi")
              else
                version.tag(ee: true).gsub(/-ee$/, "-ubi#{@ubi_version}")
              end

        logger.info('Creating UBI tag', tag: tag, project: project_path)

        # UBI tags contain the same data as EE, so we don't need to bump any
        # version files.
        client.find_or_create_tag(
          project_path,
          tag,
          target_branch,
          message: "Version #{tag}"
        )
      end

      def create_fips_tag
        return unless version >= Version.new(GITLAB_FIPS_MINIMUM_VERSION)

        tag = version.tag(ee: true).gsub(/-ee$/, "-fips")

        logger.info('Creating FIPS tag', tag: tag, project: project_path)

        # FIPS tags contain the same data as EE, so we don't need to bump any
        # version files.
        client.find_or_create_tag(
          project_path,
          tag,
          target_branch,
          message: "Version #{tag}"
        )
      end

      def find_or_create_tag(tag, variables)
        Retriable.with_context(:api) do
          client.tag(project_path, tag: tag)
        rescue Gitlab::Error::NotFound
          logger.info('Creating new tag', tag: tag, project: project_path)

          commit_version_files(
            target_branch,
            { VARIABLES_FILE => YAML.dump(variables) }
          )

          client.create_tag(project_path, tag, target_branch, "Version #{tag}")
        end
      end

      def distribution_component_versions
        variables_yaml = read_file(VARIABLES_FILE)
        versions = component_versions
        ce_data = YAML.safe_load(variables_yaml)
        ee_data = YAML.safe_load(variables_yaml)

        ce_data['variables'].merge!(versions)
        ee_data['variables'].merge!(versions)

        # For the version of GitLab itself, we only want the -ee suffix for an
        # EE CNG release.
        GITLAB_VERSION_FILES.each do |file|
          ce_data['variables'][file] = @gitlab_version.tag(ee: false)
          ee_data['variables'][file] = @gitlab_version.tag(ee: true)
        end

        GITLAB_SLUG_FILES.each do |file|
          ce_data['variables'][file] = @gitlab_version.slug.tag(ee: false)
          ee_data['variables'][file] = @gitlab_version.slug.tag(ee: true)
        end

        [ce_data, ee_data]
      end

      def component_versions
        gemfile_parser = GemfileParser.new(read_ee_file('Gemfile.lock'))
        components = {}

        VERSION_FILES.each do |file|
          components[file] = version_string(read_ee_file(file))
        end

        Project::GitlabEe.gems.each do |pattern, file|
          # Ensure ci/variables.yml are always used for ignored versions
          next if ignore?(file)

          components[file] = gemfile_parser.gem_version(pattern)
        end

        components
      end

      def add_release_data_for_tags(ce_tag, ee_tag, ubi_tag, fips_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,
          ubi_tag: ubi_tag.name,
          fips_tag: fips_tag&.name
        )

        add_release_data('cng-ce', meta_version, ce_tag)
        add_release_data('cng-ee', meta_version, ee_tag)
        add_release_data('cng-ee-ubi', meta_version, ubi_tag)
        add_release_data('cng-ee-fips', meta_version, fips_tag) if fips_tag
      end

      def add_release_data(name, version, tag)
        release_metadata.add_release(
          name: name,
          version: version,
          sha: tag.commit.id,
          ref: tag.name,
          tag: true
        )
      end

      def read_ee_file(file)
        path = Project::GitlabEe.canonical_or_security_path
        branch = @gitlab_version.stable_branch(ee: true)

        read_file(file, path, branch)
      end

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

        content.strip
      end

      def version_string(version)
        # If it looks like SemVer, assume it's a tag, which we prepend with `v`
        if version.match?(/\A\d+\.\d+\.\d+/)
          "v#{version}"
        else
          version
        end
      end

      def project
        Project::CNGImage
      end

      def ignore?(file)
        ignored = Project::GitlabEe.ignore_versions
        ignored -= ['MAILROOM_VERSION'] if version >= Version.new(GITLAB_MAILROOM_MINIMUM_VERSION)

        ignored.include?(file)
      end

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

        @commit || project.default_branch
      end
    end
  end
end
