# frozen_string_literal: true

module ReleaseTools
  class Version < String
    VERSION_REGEX = %r{
      \A(?<major>\d+)
      \.(?<minor>\d+)
      (\.(?<patch>\d+))?
      (-(?<rc>rc(?<rc_number>\d*)))?
      (-(?<internal>internal(?<internal_number>\d+)))?
      (-\h+\.\h+)?
      (-ee|\.ee\.\d+)?\z
    }x

    # GitLab releases monthly, so the minor version runs from 0 to 11
    MAX_MINOR_VERSION = 11

    def initialize(version_string)
      super(version_string.strip)

      if valid? && extract_from_version(:patch, fallback: nil).nil?
        if rc?
          super(to_rc(rc))
        elsif internal?
          super(to_internal(internal))
        else
          super(to_patch)
        end
      end
    end

    def ==(other)
      if other.respond_to?(:to_ce)
        to_ce.eql?(other.to_ce)
      else
        super
      end
    end

    def <=>(other)
      return nil unless other.is_a?(Version)
      return 0 if self == other

      if major > other.major ||
          (major >= other.major && minor > other.minor) ||
          (major >= other.major && minor >= other.minor && patch > other.patch) ||
          (major >= other.major && minor >= other.minor && patch >= other.patch && other.rc?)

        compare_rc_versions(other)
      else
        compare_internal_versions(other)
      end
    end

    def ee?
      end_with?('-ee')
    end

    def milestone_name
      to_minor
    end

    def monthly?
      patch.zero? && !rc? && !internal?
    end

    def patch?
      patch.positive? && !internal?
    end

    def minor?
      !patch? && minor.positive?
    end

    def major?
      !patch? && !minor? && major.positive?
    end

    def major
      @major ||= extract_from_version(:major).to_i
    end

    def minor
      @minor ||= extract_from_version(:minor).to_i
    end

    def patch
      @patch ||= extract_from_version(:patch).to_i
    end

    def rc
      return unless rc?

      @rc ||= extract_from_version(:rc_number).to_i
    end

    def internal
      return unless internal?

      @internal ||= extract_from_version(:internal_number).to_i
    end

    # rubocop:disable Naming/MemoizedInstanceVariableName
    def rc?
      return @is_rc if defined?(@is_rc)

      @is_rc = extract_from_version(:rc, fallback: false)
    end

    def internal?
      return @is_internal if defined?(@is_internal)

      @is_internal = extract_from_version(:internal, fallback: false)
    end
    # rubocop:enable Naming/MemoizedInstanceVariableName

    def version?
      self =~ self.class::VERSION_REGEX
    end

    def release?
      valid? && !rc? && !ee? && !internal?
    end

    def to_upcoming_pre_release
      "#{next_milestone}.0-pre"
    end

    def next_milestone
      ProductMilestone.after(self).title
    end

    def next_major
      "#{major + 1}.0.0"
    end

    def next_minor
      if minor >= MAX_MINOR_VERSION
        "#{major + 1}.0.0" # Roll over to next major version
      else
        "#{major}.#{minor + 1}.0"
      end
    end

    def next_internal
      if internal
        "#{major}.#{minor}.#{patch}-internal#{internal + 1}"
      else
        "#{previous_patch}-internal0"
      end
    end

    def previous_minor
      if minor.positive?
        "#{major}.#{minor - 1}.0"
      else
        # When we are creating a new major release, we can not retrieve the
        # previous stable branch from the current version. Instead we must
        # find the last minor version of the previous major release.
        version = Versions.last_version_for_major(major - 1)

        unless version
          raise "The last version before #{self} could not be found"
        end

        version
      end
    end

    def previous_patch
      return unless patch?

      new_patch = self.class.new("#{major}.#{minor}.#{patch - 1}")

      ee? ? new_patch.to_ee : new_patch
    end

    def next_patch
      new_patch = self.class.new("#{major}.#{minor}.#{patch + 1}")

      ee? ? new_patch.to_ee : new_patch
    end

    def stable_branch(ee: false)
      to_minor.tr('.', '-').tap do |prefix|
        if ee || ee?
          prefix << '-stable-ee'
        else
          prefix << '-stable'
        end
      end
    end

    def tag(ee: false)
      tag_for(self, ee: ee)
    end

    def previous_tag(ee: false)
      return unless patch?
      return if rc?

      tag_for(previous_patch, ee: ee)
    end

    # Convert the current version to CE if it isn't already
    def to_ce
      return self unless ee?

      self.class.new(to_s.gsub(/-ee$/, ''))
    end

    # Convert the current version to EE if it isn't already
    def to_ee
      return self if ee?

      self.class.new("#{self}-ee")
    end

    def to_minor
      "#{major}.#{minor}"
    end

    def to_omnibus(ee: false)
      str = "#{to_patch}+"

      str << "rc#{rc}." if rc?
      str << "internal#{internal}." if internal?
      str << (ee ? 'ee' : 'ce')
      str << '.0'
    end

    def to_docker(ee: false)
      to_omnibus(ee: ee).tr('+', '-')
    end

    def to_patch
      "#{major}.#{minor}.#{patch}"
    end

    def to_normalized_version
      if rc?
        "#{to_patch}-rc#{rc}"
      elsif internal?
        "#{to_patch}-internal#{internal}"
      else
        to_patch
      end
    end

    def to_rc(number = 1)
      "#{to_patch}-rc#{number}".tap do |version|
        version << '-ee' if ee?
      end
    end

    def to_internal(number = 0)
      "#{to_patch}-internal#{number}"
    end

    def slug
      self.class.new(
        downcase
          .gsub(/[^a-z0-9]/, '-')[0..62]
          .gsub(/(\A-+|-+\z)/, '')
      )
    end

    def valid?
      self.class::VERSION_REGEX.match?(self)
    end

    private

    def tag_for(version, ee: false)
      version = version.to_ee if ee

      "v#{version}"
    end

    def extract_from_version(part, fallback: 0)
      match_data = self.class::VERSION_REGEX.match(self)
      if match_data && match_data.names.include?(part.to_s) && match_data[part]
        String.new(match_data[part])
      else
        fallback
      end
    end

    def compare_rc_versions(other)
      rc? && other.rc? ? (rc - other.rc) : 1
    end

    def compare_internal_versions(other)
      internal? && other.internal? ? (internal - other.internal) : -1
    end
  end
end
