# frozen_string_literal: true

module ReleaseTools
  # ProductVersion is a container for any type of GitLab - The DevOps Platform package.
  # Given a Product version we will be able to fetch its own metadata and find
  # what references to both components and packagers.
  #
  # A ProductVersion is always a normalized version (see: Version#to_normalized_version)
  class ProductVersion
    include ::SemanticLogger::Loggable
    include Comparable
    extend Forwardable

    MetadataInfo = Struct.new(:commit_id, :content, keyword_init: true)
    METADATA_PATH_REGEXP = %r[releases/(?<major>\d+)/(?<normalized_version>[^/]+).json]

    AUTO_DEPLOY_TAG_REGEX = %r{\A (?<major>\d+) \.(?<minor>\d+) \.(?<timestamp>\d+) \+ .* \z}x

    # delegate data accessors
    def_delegators :@version, :major, :minor, :patch, :rc, :to_s
    # delegate predicate functions for detecting the type of version
    def_delegators :@version, :rc?, :monthly?, :patch?

    # Instance of ::Version
    attr_reader :version

    # Creates a `ProductVersion` from an auto-deploy tag or a package version
    #
    # @param version [String, AutoDeploy::Version] the auto-deploy version
    # @return [ProductVersion, nil] the product version of the given auto-deploy
    #   version or nil if not an auto-deploy version.
    def self.from_auto_deploy(version)
      version = AutoDeploy::Version.new(version) unless version.respond_to?(:to_package)

      from_package_version(version.to_package)
    rescue ArgumentError
      nil
    end

    # Creates a `ProductVersion` from an auto-deploy tag
    #
    # @param version [String] the auto-deploy tag
    # @return [ProductVersion, nil] the product version of the given auto-deploy
    #   tag or nil if not an auto-deploy tag.
    def self.from_auto_deploy_tag(version)
      captures = AUTO_DEPLOY_TAG_REGEX.match(version)
      return nil unless captures

      new("#{captures['major']}.#{captures['minor']}.#{captures['timestamp']}")
    end

    # Creates a `ProductVersion` from an omnibus package version (i.e. $DEPLOY_VERSION)
    #
    # @param version [String, Version] a valid Omnibus package version
    # @return [ProductVersion, nil] the product version of the given version
    #   or nil if not a valid version.
    def self.from_package_version(version)
      version = Version.new(version) unless version.is_a?(Version)
      return nil unless version.version?

      new(version.to_normalized_version)
    end

    # Creates a `ProductVersion` from a release/metadata commit id.
    #
    # @param sha [String] the commit id introducting a release metadata
    # @return [ProductVersion, nil] the product version of the given version
    #   or nil if not a valid version.
    def self.from_metadata_sha(sha)
      Retriable.with_context(:api) do
        logger.info('Fetching release metadata commit', sha: sha)

        diffs = GitlabOpsClient.commit_diff(ReleaseMetadataUploader::PROJECT, sha)
        diff = diffs.detect do |d|
          d.new_file && METADATA_PATH_REGEXP.match(d.new_path)
        end

        return nil unless diff

        from_package_version(METADATA_PATH_REGEXP.match(diff.new_path)[:normalized_version])
      rescue Gitlab::Error::NotFound
        nil
      end
    end

    # NOTE(nolith): here we implement Enumerable at class level to iterate over all the
    #  product versions in release-metadata. It's implemented as a lazy enumerator in order
    #  to only fetch commits one by one. Because release-metadata acts as a ledger, we can
    #  navigate it commit by commit and build the product version history.
    class << self
      include Enumerable

      def each(&block)
        return lazy_enumerator unless block

        lazy_enumerator.each(&block)
      end

      def last_auto_deploy
        @last_auto_deploy ||= find(&:auto_deploy?)
      end

      private

      def lazy_enumerator
        Enumerator.new do |yielder|
          commits = Retriable.with_context(:api) do
            GitlabOpsClient.commits(ReleaseMetadataUploader::PROJECT, trailers: true)
          end

          loop do
            commits.each do |commit|
              version = commit.trailers.to_h['Product-Version']
              # If a commit does not have the trailer, it is not a commit that
              # recorded a new version.
              next unless version

              product_version = new(version)

              yielder << product_version
            end

            break unless commits.has_next_page?

            Retriable.with_context(:api) do
              commits = commits.next_page
            end
          end
        end.lazy
      end
    end

    def initialize(version)
      @version = Version.new(version)
    end

    def ==(other)
      @version.to_s == other.to_s
    end

    def <=>(other)
      @version <=> other.version
    end

    def metadata
      full_metadata.content
    end

    def metadata_commit_id
      full_metadata.commit_id
    end

    # product version for auto_deploy is MAJOR.MINOR.TIMESTAMP<YYYYMMDDhhmm>
    #  it's safe to assume that we will never have a 12 digit patch version
    #  so we can identify auto_deploy packages by checking the patch value
    def auto_deploy?
      #                 YYYYMMDDhhmm first gitlab commit by DZ
      patch? && patch > 201110082230 # rubocop:disable Style/NumericLiterals
    end

    def auto_deploy_package
      metadata
        .dig('releases', Project::OmnibusGitlab.metadata_project_name, 'ref')
        .tr('+', '-')
    end

    # extract the release metadata for the given released_item
    #
    # @param released_item [String, Symbol, Project] the component
    # @return [ReleaseMetadata::Release, nil] the release information
    #   or nil if the component does not exists
    def [](released_item)
      released_item = released_item.metadata_project_name if released_item.respond_to?(:metadata_project_name)
      name = released_item.to_s
      raw = metadata.dig('releases', name)
      return unless raw

      ReleaseMetadata::Release.new(name: name, **raw)
    end

    private

    def metadata_path
      "releases/#{major}/#{@version.to_normalized_version}.json"
    end

    def full_metadata
      @full_metadata ||= Retriable.with_context(:api) do
        logger.info('Fetching release metadata', version: to_s)

        meta_info = GitlabOpsClient.get_file(
          ReleaseMetadataUploader::PROJECT,
          metadata_path,
          ReleaseMetadataUploader::PROJECT.default_branch
        )

        json = Base64.strict_decode64(meta_info.content)
        MetadataInfo.new(
          commit_id: meta_info.last_commit_id,
          content: JSON.parse(json)
        )
      rescue Gitlab::Error::NotFound
        MetadataInfo.new(content: {})
      end
    end
  end
end
