# frozen_string_literal: true

module Gitlab
  module QA
    module Support
      class GitlabVersionInfo
        VERSION_PATTERN = /^(?<version>\d+\.\d+\.\d+)/
        COMPONENT_PATTERN = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/

        VersionNotFoundError = Class.new(RuntimeError)

        # Get previous gitlab version
        #
        # @param [String] current_version
        # @param [String] edition GitLab edition - ee or ce
        def initialize(current_version, edition)
          @current_version = current_version
          @edition = edition
          @logger = Runtime::Logger.logger
        end

        # Get N - 1 version number
        #
        # @param [String] semver_component version number component for previous version detection - major|minor|patch
        # @return [Gem::Version]
        def previous_version(semver_component)
          case semver_component
          when "major"
            previous_major
          when "minor"
            previous_minor
          when "patch"
            previous_patch
          else
            raise("Unsupported semver component, must be major|minor|patch")
          end
        end

        # Check if specific version exists in GitLab releases
        #
        # @example
        # version_exists?("17.10.5") => true
        # version_exists?("17.10.28") => false
        #
        # @param [String] version Version to check
        # @return [Boolean] true if version exists in GitLab releases, false otherwise
        def version_exists?(version)
          !!versions.find { |ver| ver.to_s == version }
        end

        # Get latest patch for specific version number
        #
        # @example
        # latest_patch(Gem::Version.new("14.10")) => "14.10.5"
        # latest_patch(Gem::Version.new("14.10.5")) => "14.10.5"
        #
        # @param [Gem::Version] version
        # @return [String]
        def latest_patch(version)
          # check if version is already a patch version
          return version if version.to_s.split('.').size == 3

          versions.find { |ver| ver.to_s.match?(/^#{version}\./) }.tap do |ver|
            raise_version_not_found("Latest patch version for version #{version}") unless ver
          end
        end

        # Get next version major.minor from available releases
        #
        # @example
        # next_version("17.7.4") => "17.8"
        # next_version("17.12.5") => "18.0"
        # next_version("18.0.3") => nil # when no next version exists
        #
        # @param [String] version Current version
        # @return [String, nil] Next version in major.minor format or nil if no next version exists
        def next_version(version)
          current_ver = Gem::Version.new(version)

          # Since versions are already sorted in descending order (newest first),
          # we need to reverse them to find the next version after current
          next_ver = versions.reverse.find { |ver| ver > current_ver }

          return nil unless next_ver

          [next_ver.segments[0], next_ver.segments[1]].join('.') # major.minor
        end

        private

        MAX_TAGS_HTTP_REQUESTS = 50
        # https://docs.docker.com/docker-hub/api/latest/#tag/images/operation/GetNamespacesRepositoriesImages
        TAGS_PER_PAGE = 100

        attr_reader :current_version, :edition, :logger

        # Current versions major version
        #
        # @return [Integer]
        def current_major
          @current_major ||= current_version.match(COMPONENT_PATTERN)[:major].to_i
        end

        # Current versions minor version
        #
        # @return [Integer]
        def current_minor
          @current_minor ||= current_version.match(COMPONENT_PATTERN)[:minor].to_i
        end

        # Current versions patch version
        #
        # @return [Integer]
        def current_patch
          @current_patch ||= current_version.match(COMPONENT_PATTERN)[:patch].to_i
        end

        # Previous major version
        #
        # @return [String]
        def previous_major
          return fallback_major unless tags

          versions.find { |version| version.to_s.start_with?((current_major - 1).to_s) }
        end

        # Previous first major version image
        #
        # @return [String]
        def fallback_major
          previous_fallback_version(current_major - 1)
        end

        # Previous minor version
        #
        # @return [String]
        def previous_minor
          return fallback_minor unless tags
          return previous_major if current_minor.zero?

          versions.find { |version| version.to_s.match?(/^#{current_major}\.#{current_minor - 1}/) }.tap do |ver|
            raise_version_not_found("Previous minor version for current version #{current_version}") unless ver
          end
        end

        # Previous first minor version
        #
        # @return [String]
        def fallback_minor
          return previous_fallback_version(current_major, current_minor - 1) unless current_minor.zero?

          previous_major
        end

        # Previous patch version
        #
        # @return [String]
        def previous_patch
          return fallback_patch unless tags
          return previous_minor if current_patch.zero?

          versions.find { |version| version.to_s.match?(/^#{current_major}\.#{current_minor}\.#{current_patch - 1}/) }
        end

        # Previous first patch version
        #
        # @return [String]
        def fallback_patch
          return previous_fallback_version(current_major, current_minor, current_patch - 1) unless current_patch.zero?

          previous_minor
        end

        # Version number from docker tag
        #
        # @param [String] tag
        # @return [String]
        def version(tag)
          tag.match(VERSION_PATTERN)[:version]
        end

        # Fallback version
        #
        # @param [Integer] major_component
        # @param [Integer] minor_component
        # @param [Integer] patch_component
        # @return [Gem::Version]
        def previous_fallback_version(major_component, minor_component = 0, patch_component = 0)
          Gem::Version.new("#{major_component}.#{minor_component}.#{patch_component}")
        end

        # All available gitlab versions
        #
        # @return [Array<String>]
        def versions
          @versions = tags
            .map { |tag| Gem::Version.new(tag.match(VERSION_PATTERN)[:version]) }
            .sort
            .reverse # reverse array so first match by .find always returns latest version
        end

        # All available docker tags
        #
        # @return [Array<String>]
        def tags
          return @tags if defined?(@tags)

          MAX_TAGS_HTTP_REQUESTS.times do |index|
            tag_list, more_data = fetch_tags(page: index + 1)

            if tag_list
              @tags = Array(@tags)
              @tags += tag_list
            end

            break if tag_list.nil? || more_data.nil?
          end

          @tags
        end

        def fetch_tags(page:, per_page: TAGS_PER_PAGE)
          logger.info("Fetching Docker tags page #{page} from 'gitlab/gitlab-#{edition}' registry")
          response = HttpRequest.make_http_request(
            url: "https://registry.hub.docker.com/v2/namespaces/gitlab/repositories/gitlab-#{edition}/tags?page=#{page}&page_size=#{per_page}",
            fail_on_error: false
          )

          unless response.code == 200
            logger.error("  failed to fetch docker tags - code: #{response.code}, response: '#{response.body}'")
            return nil
          end

          response = JSON.parse(response.body, symbolize_names: true)
          matching_tags = response
            .fetch(:results)
            .map { |tag| tag[:name] }
            .grep(VERSION_PATTERN)
          more_data = response.fetch(:next)

          [matching_tags, more_data]
        end

        def raise_version_not_found(error_prefix)
          raise(VersionNotFoundError, "#{error_prefix} not available on Dockerhub (yet)")
        end
      end
    end
  end
end
