# frozen_string_literal: true

require "active_support/core_ext/module/delegation"
require "yaml"

module Gitlab
  module QA
    module Support
      class GitlabUpgradePath
        # Get upgrade path between N - 1 and current version not including current release
        #
        # @param [String] current_version
        # @param [String] semver_component version number component for previous version detection - major|minor|patch
        # @param [String] edition GitLab edition - ee or ce
        def initialize(current_version, semver_component, edition)
          @logger = Runtime::Logger.logger

          unless current_version.match?(GitlabVersionInfo::VERSION_PATTERN)
            logger.error("Invalid 'current_version' format: #{current_version}. Expected format: MAJOR.MINOR.PATCH (e.g., 17.8.2)")
            exit 1
          end

          @version_info = GitlabVersionInfo.new(current_version, edition)
          @current_version = Gem::Version.new(current_version.match(GitlabVersionInfo::VERSION_PATTERN)[:version]) # Extract version without postfixes like pre or ee
          @semver_component = semver_component
          @edition = edition
        end

        # Get upgrade path between releases
        #
        # Return array with only previous version for updates from previous minor, patch versions
        #
        # @return [Array<QA::Release>]
        def fetch
          case semver_component
          when "patch"
            patch_upgrade_path
          when "minor"
            minor_upgrade_path
          when "major"
            major_upgrade_path
          when "from_patch"
            from_patch_upgrade_path
          when "internal_patch"
            internal_patch_upgrade_path
          else
            raise ArgumentError, "Unknown semver component: #{semver_component}"
          end
        rescue GitlabVersionInfo::VersionNotFoundError
          logger.error("Failed to construct gitlab upgrade path")
          raise
        end

        private

        delegate :latest_patch, to: :version_info

        attr_reader :version_info, :current_version, :semver_component, :edition, :logger

        # Upgrade path for patch version update
        # Returns array with current version as we're testing upgrade from
        # latest stable patch to development version
        #
        # @return [Array]
        def patch_upgrade_path
          verify_current_version_exists

          [release(latest_patch(current_version))]
        end

        # Upgrade path from previous minor version
        #
        # @return [Array]
        def minor_upgrade_path
          [release(latest_patch(previous_version))]
        end

        # Upgrade path from previous major version
        #
        # @return [Array]
        def major_upgrade_path
          # get versions between previous major and current version in gitlab upgrade path
          path = full_upgrade_path.each_with_object([]) do |ver, arr|
            next if ver <= previous_version || ver >= current_version

            arr << ver
          end

          [previous_version, *path].map do |ver|
            release(version_info.latest_patch(ver))
          end
        end

        # Upgrade path from current version to next stable version
        # Checks if current version exists in releases
        # Gets next available major.minor version and its latest patch
        #
        # @return [Array] Array with next version's latest patch release or exits with message
        def from_patch_upgrade_path
          verify_current_version_exists
          next_version = version_info.next_version(current_version.to_s)

          unless next_version
            logger.info("Skipping upgrade test as next version after #{current_version} is not yet available")
            exit 0
          end

          [release(latest_patch(next_version))]
        end

        # Upgrade path for internal patch version
        # Sets up authentication for internal registry
        # Finds the latest internal build for the current version
        #
        # @return [Array<QA::Release>] Array with the latest internal build for current version or exits with message
        def internal_patch_upgrade_path
          unless Runtime::Env.dev_access_token_variable
            logger.error("Skipping upgrade test as internal patch upgrades are not supported without dev access token")
            exit 0
          end

          verify_current_version_exists
          gitlab_int_reg_repo = "dev.gitlab.org:5005/gitlab/omnibus-gitlab/gitlab-ee"

          # Internal releases are stored in private repo and
          # not available for search with API
          release = QA::Release.new("#{gitlab_int_reg_repo}:latest")
          docker = Docker::Engine.new
          docker.login(**release.login_params) if release.login_params
          latest_internal_tag = find_latest_internal_tag(gitlab_int_reg_repo, docker)

          if latest_internal_tag
            [QA::Release.new("#{gitlab_int_reg_repo}:#{latest_internal_tag}")]
          else
            logger.warn("No internal image found for GitLab version #{current_version}")
            exit 0
          end
        end

        # Find the latest internal tag for the current version
        # Searches for tags in descending order, from 10 down to 0
        # Returns the first available tag or nil if none found
        #
        # @param [String] gitlab_int_reg_repo Registry repository path
        # @param [Docker::Engine] docker_engine Docker engine instance
        # @return [String, nil] Tag name if found, nil otherwise
        def find_latest_internal_tag(gitlab_int_reg_repo, docker)
          # Try to find the highest internal release tag, starting from 10
          latest_internal_tag = nil
          logger.info("Start searching for the latest released internal image for gitlab version: #{current_version}...")

          # Release team note: no more than 10 internal releases expected for version
          10.downto(0) do |internal_num|
            tag = "#{current_version}-internal#{internal_num}-0"
            image_uri = "#{gitlab_int_reg_repo}:#{tag}"

            logger.info("Checking for image: #{image_uri}")

            begin
              # Try to pull the image (this will fail if image doesn't exist)
              docker.pull(image: gitlab_int_reg_repo, tag: tag)

              latest_internal_tag = tag
              logger.info("Found image: #{image_uri}")
              break
            rescue Support::ShellCommand::StatusError => e
              logger.info("x - Image not found: #{image_uri}, \n #{e}")
            end
          end

          latest_internal_tag
        end

        # Docker release image
        #
        # @param [String] version
        # @return [QA::Release]
        def release(version)
          QA::Release.new("gitlab/gitlab-#{edition}:#{version}-#{edition}.0")
        end

        # Previous gitlab version
        #
        # @return [Gem::Version]
        def previous_version
          @previous_version ||= version_info.previous_version(semver_component)
        end

        # Verify if current version exists in GitLab releases
        # Exit with message if version is not yet released
        #
        # @return [void]
        def verify_current_version_exists
          return if version_info.version_exists?(current_version.to_s)

          logger.info("Skipping upgrade test as version #{current_version} is not yet released")
          exit 0
        end

        # Gitlab upgrade path
        #
        # @return [Array<Gem::Version>]
        def full_upgrade_path
          @full_upgrade_path ||= ::YAML
            .safe_load(upgrade_path_yml, symbolize_names: true)
            .map { |version| Gem::Version.new("#{version[:major]}.#{version[:minor]}") }
        end

        # Upgrade path yml
        #
        # @return [String]
        def upgrade_path_yml
          @upgrade_path_yml ||= begin
            logger.info("Fetching gitlab upgrade path from 'gitlab.com/gitlab-org/gitlab' project")
            HttpRequest.make_http_request(
              url: "https://gitlab.com/gitlab-org/gitlab/-/raw/master/config/upgrade_path.yml"
            ).body
          end
        end
      end
    end
  end
end
