# typed: true
# frozen_string_literal: true

require "open3"

module Homebrew
  module Livecheck
    module Strategy
      # The {Git} strategy identifies versions of software in a Git repository
      # by checking the tags using `git ls-remote --tags`.
      #
      # Livecheck has historically prioritized the {Git} strategy over others
      # and this behavior was continued when the priority setup was created.
      # This is partly related to Livecheck checking formula URLs in order of
      # `head`, `stable`, and then `homepage`. The higher priority here may
      # be removed (or altered) in the future if we reevaluate this particular
      # behavior.
      #
      # This strategy does not have a default regex. Instead, it simply removes
      # any non-digit text from the start of tags and parses the rest as a
      # {Version}. This works for some simple situations but even one unusual
      # tag can cause a bad result. It's better to provide a regex in a
      # `livecheck` block, so `livecheck` only matches what we really want.
      #
      # @api public
      class Git
        # The priority of the strategy on an informal scale of 1 to 10 (from
        # lowest to highest).
        PRIORITY = 8

        # The default regex used to naively identify versions from tags when a
        # regex isn't provided.
        DEFAULT_REGEX = /\D*(.+)/.freeze

        # Whether the strategy can be applied to the provided URL.
        #
        # @param url [String] the URL to match against
        # @return [Boolean]
        sig { params(url: String).returns(T::Boolean) }
        def self.match?(url)
          (DownloadStrategyDetector.detect(url) <= GitDownloadStrategy) == true
        end

        # Fetches a remote Git repository's tags using `git ls-remote --tags`
        # and parses the command's output. If a regex is provided, it will be
        # used to filter out any tags that don't match it.
        #
        # @param url [String] the URL of the Git repository to check
        # @param regex [Regexp] the regex to use for filtering tags
        # @return [Hash]
        sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) }
        def self.tag_info(url, regex = nil)
          stdout, stderr, _status = system_command(
            "git",
            args:         ["ls-remote", "--tags", url],
            env:          { "GIT_TERMINAL_PROMPT" => "0" },
            print_stdout: false,
            print_stderr: false,
            debug:        false,
            verbose:      false,
          )

          tags_data = { tags: [] }
          tags_data[:messages] = stderr.split("\n") if stderr.present?
          return tags_data if stdout.blank?

          # Isolate tag strings and filter by regex
          tags = stdout.gsub(%r{^.*\trefs/tags/|\^{}$}, "").split("\n").uniq.sort
          tags.select! { |t| t =~ regex } if regex
          tags_data[:tags] = tags

          tags_data
        end

        # Identify versions from tag strings using a provided regex or the
        # `DEFAULT_REGEX`. The regex is expected to use a capture group around
        # the version text.
        #
        # @param tags [Array] the tags to identify versions from
        # @param regex [Regexp, nil] a regex to identify versions
        # @return [Array]
        sig {
          params(
            tags:  T::Array[String],
            regex: T.nilable(Regexp),
            block: T.nilable(Proc),
          ).returns(T::Array[String])
        }
        def self.versions_from_tags(tags, regex = nil, &block)
          if block
            block_return_value = if regex.present?
              yield(tags, regex)
            elsif block.arity == 2
              yield(tags, DEFAULT_REGEX)
            else
              yield(tags)
            end
            return Strategy.handle_block_return(block_return_value)
          end

          tags.map do |tag|
            if regex
              # Use the first capture group (the version)
              # This code is not typesafe unless the regex includes a capture group
              T.unsafe(tag.scan(regex).first)&.first
            else
              # Remove non-digits from the start of the tag and use that as the
              # version text
              tag[DEFAULT_REGEX, 1]
            end
          end.compact.uniq
        end

        # Checks the Git tags for new versions. When a regex isn't provided,
        # this strategy simply removes non-digits from the start of tag
        # strings and parses the remaining text as a {Version}.
        #
        # @param url [String] the URL of the Git repository to check
        # @param regex [Regexp, nil] a regex used for matching versions
        # @return [Hash]
        sig {
          params(
            url:     String,
            regex:   T.nilable(Regexp),
            _unused: T.nilable(T::Hash[Symbol, T.untyped]),
            block:   T.nilable(Proc),
          ).returns(T::Hash[Symbol, T.untyped])
        }
        def self.find_versions(url:, regex: nil, **_unused, &block)
          match_data = { matches: {}, regex: regex, url: url }

          tags_data = tag_info(url, regex)
          tags = tags_data[:tags]

          if tags_data.key?(:messages)
            match_data[:messages] = tags_data[:messages]
            return match_data if tags.blank?
          end

          versions_from_tags(tags, regex, &block).each do |version_text|
            match_data[:matches][version_text] = Version.new(version_text)
          rescue TypeError
            next
          end

          match_data
        end
      end
    end
  end
end
