# typed: strict
# frozen_string_literal: true

require "livecheck/constants"
require "livecheck/error"
require "livecheck/livecheck_version"
require "livecheck/skip_conditions"
require "livecheck/strategy"
require "formula_versions"
require "uri"
require "utils/git"
require "utils/output"

module Homebrew
  # The {Livecheck} module consists of methods used by the `brew livecheck`
  # command. These methods print the requested livecheck information
  # for formulae.
  module Livecheck
    extend Utils::Output::Mixin

    NO_CURRENT_VERSION_MSG = "Unable to identify current version"
    NO_VERSIONS_MSG = "Unable to get versions"

    UNSTABLE_VERSION_KEYWORDS = %w[
      alpha
      beta
      bpo
      dev
      experimental
      prerelease
      preview
      rc
    ].freeze
    private_constant :UNSTABLE_VERSION_KEYWORDS

    sig { params(strategy_class: T::Class[Strategic]).returns(String) }
    def self.livecheck_strategy_names(strategy_class)
      @livecheck_strategy_names ||= T.let({}, T.nilable(T::Hash[T::Class[Strategic], String]))
      @livecheck_strategy_names[strategy_class] ||= Utils.demodulize(strategy_class.name)
    end

    sig { params(strategy_class: T::Class[Strategic]).returns(T::Array[Symbol]) }
    def self.livecheck_find_versions_parameters(strategy_class)
      @livecheck_find_versions_parameters ||= T.let({}, T.nilable(T::Hash[T::Class[Strategic], T::Array[Symbol]]))
      @livecheck_find_versions_parameters[strategy_class] ||=
        (T::Utils.signature_for_method(strategy_class.method(:find_versions))&.parameters ||
         strategy_class.method(:find_versions).parameters).map(&:second)
    end

    # Uses `formulae_and_casks_to_check` to identify taps in use other than
    # homebrew/core and homebrew/cask and loads strategies from them.
    sig { params(formulae_and_casks_to_check: T::Array[T.any(Formula, Cask::Cask)]).void }
    def self.load_other_tap_strategies(formulae_and_casks_to_check)
      other_taps = {}
      formulae_and_casks_to_check.each do |formula_or_cask|
        tap = formula_or_cask.tap
        next unless tap
        next if tap.core_tap?
        next if tap.core_cask_tap?
        next if other_taps[tap.name]

        other_taps[tap.name] = tap
      end
      other_taps = other_taps.sort.to_h

      other_taps.each_value do |tap|
        tap_strategy_path = "#{tap.path}/livecheck/strategy"
        Dir["#{tap_strategy_path}/*.rb"].each { require(it) } if Dir.exist?(tap_strategy_path)
      end
    end

    # Resolve formula/cask references in `livecheck` blocks to a final formula
    # or cask.
    sig {
      params(
        formula_or_cask:       T.any(Formula, Cask::Cask),
        first_formula_or_cask: T.any(Formula, Cask::Cask),
        references:            T::Array[T.any(Formula, Cask::Cask)],
        full_name:             T::Boolean,
        debug:                 T::Boolean,
      ).returns(T.nilable(T::Array[T.untyped]))
    }
    def self.resolve_livecheck_reference(
      formula_or_cask,
      first_formula_or_cask = formula_or_cask,
      references = [],
      full_name: false,
      debug: false
    )
      # Check the `livecheck` block for a formula or cask reference
      livecheck = formula_or_cask.livecheck
      livecheck_formula = livecheck.formula
      livecheck_cask = livecheck.cask
      return [nil, references] if livecheck_formula.blank? && livecheck_cask.blank?

      # Load the referenced formula or cask
      referenced_formula_or_cask = Homebrew.with_no_api_env do
        if livecheck_formula
          Formulary.factory(livecheck_formula)
        elsif livecheck_cask
          Cask::CaskLoader.load(livecheck_cask)
        else
          raise "livecheck formula or cask not found"
        end
      end

      # Error if a `livecheck` block references a formula/cask that was already
      # referenced (or itself)
      if referenced_formula_or_cask == first_formula_or_cask ||
         referenced_formula_or_cask == formula_or_cask ||
         references.include?(referenced_formula_or_cask)
        if debug
          # Print the chain of references for debugging
          puts "Reference Chain:"
          puts package_or_resource_name(first_formula_or_cask, full_name:)

          references << referenced_formula_or_cask
          references.each do |ref_formula_or_cask|
            puts package_or_resource_name(ref_formula_or_cask, full_name:)
          end
        end

        raise "Circular formula/cask reference encountered"
      end
      references << referenced_formula_or_cask

      # Check the referenced formula/cask for a reference
      next_referenced_formula_or_cask, next_references = resolve_livecheck_reference(
        referenced_formula_or_cask,
        first_formula_or_cask,
        references,
        full_name:,
        debug:,
      )

      # Returning references along with the final referenced formula/cask
      # allows us to print the chain of references in the debug output
      [
        next_referenced_formula_or_cask || referenced_formula_or_cask,
        next_references,
      ]
    end

    # Executes the livecheck logic for each formula/cask in the
    # `formulae_and_casks_to_check` array and prints the results.
    sig {
      params(
        formulae_and_casks_to_check: T::Array[T.any(Formula, Cask::Cask)],
        full_name:                   T::Boolean,
        handle_name_conflict:        T::Boolean,
        check_resources:             T::Boolean,
        json:                        T::Boolean,
        newer_only:                  T::Boolean,
        extract_plist:               T::Boolean,
        debug:                       T::Boolean,
        quiet:                       T::Boolean,
        verbose:                     T::Boolean,
      ).void
    }
    def self.run_checks(
      formulae_and_casks_to_check,
      full_name: false, handle_name_conflict: false, check_resources: false, json: false, newer_only: false,
      extract_plist: false, debug: false, quiet: false, verbose: false
    )
      load_other_tap_strategies(formulae_and_casks_to_check)

      ambiguous_casks = []
      if handle_name_conflict
        ambiguous_casks = formulae_and_casks_to_check
                          .group_by { |item| package_or_resource_name(item, full_name: true) }
                          .values
                          .select { |items| items.length > 1 }
                          .flatten
                          .grep(Cask::Cask)
      end

      ambiguous_names = []
      unless full_name
        grouped = (formulae_and_casks_to_check - ambiguous_casks).group_by { |item| package_or_resource_name(item) }
        ambiguous_names = grouped.values.select { |items| items.length > 1 }.flatten
      end

      has_a_newer_upstream_version = T.let(false, T::Boolean)

      formulae_and_casks_total = formulae_and_casks_to_check.count
      if json && !quiet && $stderr.tty?
        Tty.with($stderr) do |stderr|
          stderr.puts Formatter.headline("Running checks", color: :blue)
        end

        require "ruby-progressbar"
        progress = ProgressBar.create(
          total:          formulae_and_casks_total,
          progress_mark:  "#",
          remainder_mark: ".",
          format:         " %t: [%B] %c/%C ",
          output:         $stderr,
        )
      end

      # Allow ExtractPlist strategy if only one formula/cask is being checked.
      extract_plist = true if formulae_and_casks_total == 1

      formulae_checked = formulae_and_casks_to_check.map.with_index do |formula_or_cask, i|
        case formula_or_cask
        when Formula
          formula = formula_or_cask
          formula.head&.downloader&.quiet!
        when Cask::Cask
          cask = formula_or_cask
        end

        use_full_name = full_name || ambiguous_names.include?(formula_or_cask)
        name = package_or_resource_name(formula_or_cask, full_name: use_full_name)

        referenced_formula_or_cask, livecheck_references =
          resolve_livecheck_reference(formula_or_cask, full_name: use_full_name, debug:)

        if debug && i.positive?
          puts <<~EOS

            ----------

          EOS
        elsif debug
          puts
        end

        # Check skip conditions for a referenced formula/cask
        if referenced_formula_or_cask
          skip_info = SkipConditions.referenced_skip_information(
            referenced_formula_or_cask,
            name,
            full_name:     use_full_name,
            verbose:,
            extract_plist:,
          )
        end

        skip_info ||= SkipConditions.skip_information(
          formula_or_cask,
          full_name:     use_full_name,
          verbose:,
          extract_plist:,
        )
        if skip_info.present?
          next skip_info if json && !newer_only

          SkipConditions.print_skip_information(skip_info) if !newer_only && !quiet
          next
        end

        # Use the `stable` version for comparison except for installed
        # HEAD-only formulae. A formula with `stable` and `head` that's
        # installed using `--head` will still use the `stable` version for
        # comparison.
        current = if formula
          if formula.head_only?
            formula_commit = formula.any_installed_version&.version&.commit
            Version.new(formula_commit) if formula_commit
          elsif (stable = formula.stable)
            stable.version
          end
        else
          Version.new(formula_or_cask.version)
        end
        unless current
          raise Livecheck::Error, NO_CURRENT_VERSION_MSG unless json
          next if quiet

          next status_hash(formula_or_cask, "error", [NO_CURRENT_VERSION_MSG], full_name: use_full_name, verbose:)
        end

        current_str = current.to_s
        current = LivecheckVersion.create(formula_or_cask, current)

        latest = if formula&.head_only?
          Version.new(T.must(formula.head).downloader.fetch_last_commit)
        else
          version_info = latest_version(
            formula_or_cask,
            referenced_formula_or_cask:,
            livecheck_references:,
            json:, full_name: use_full_name, verbose:, debug:
          )
          version_info[:latest] if version_info.present?
        end

        check_for_resources = check_resources && formula_or_cask.is_a?(Formula) && formula_or_cask.resources.present?
        if check_for_resources
          resource_version_info = formula_or_cask.resources.map do |resource|
            res_skip_info ||= SkipConditions.skip_information(resource, verbose:)
            if res_skip_info.present?
              res_skip_info
            else
              res_version_info = resource_version(
                resource,
                latest.to_s,
                json:,
                full_name: use_full_name,
                debug:,
                quiet:,
                verbose:,
              )
              if res_version_info.empty?
                status_hash(resource, "error", [NO_VERSIONS_MSG], verbose:)
              else
                res_version_info
              end
            end
          end.compact_blank
          Homebrew.failed = true if resource_version_info.any? { |info| info[:status] == "error" }
        end

        if latest.blank?
          raise Livecheck::Error, NO_VERSIONS_MSG unless json
          next if quiet

          next version_info if version_info.is_a?(Hash) && version_info[:status] && version_info[:messages]

          latest_info = status_hash(formula_or_cask, "error", [NO_VERSIONS_MSG], full_name: use_full_name,
                                                                                 verbose:)
          if check_for_resources
            unless verbose
              resource_version_info.map! do |info|
                info.delete(:meta)
                info
              end
            end
            latest_info[:resources] = resource_version_info
          end

          next latest_info
        end

        if (m = latest.to_s.match(/(.*)-release$/)) && !current.to_s.match(/.*-release$/)
          latest = Version.new(m[1])
        end

        latest_str = latest.to_s
        latest = LivecheckVersion.create(formula_or_cask, latest)

        is_outdated = if formula&.head_only?
          # A HEAD-only formula is considered outdated if the latest upstream
          # commit hash is different than the installed version's commit hash
          (current != latest)
        else
          (current < latest)
        end

        is_newer_than_upstream = (formula&.stable? || cask) && (current > latest)

        info = {}
        info[:formula] = name if formula
        info[:cask] = name if cask
        info[:version] = {
          current:             current_str,
          latest:              latest_str,
          latest_throttled:    version_info&.dig(:latest_throttled),
          outdated:            is_outdated,
          newer_than_upstream: is_newer_than_upstream,
        }.compact
        info[:meta] = {
          livecheck_defined: formula_or_cask.livecheck_defined?,
        }
        info[:meta][:head_only] = true if formula&.head_only?
        info[:meta].merge!(version_info[:meta]) if version_info.present? && version_info.key?(:meta)

        info[:resources] = resource_version_info if check_for_resources

        next if newer_only && !info[:version][:outdated]

        has_a_newer_upstream_version ||= true

        if json
          progress&.increment
          info.delete(:meta) unless verbose
          if check_for_resources && !verbose
            resource_version_info.map! do |resource_info|
              resource_info.delete(:meta)
              resource_info
            end
          end
          next info
        end
        puts if debug
        print_latest_version(info, verbose:, ambiguous_cask: ambiguous_casks.include?(formula_or_cask))
        print_resources_info(resource_version_info, verbose:) if check_for_resources
        nil
      rescue => e
        Homebrew.failed = true
        use_full_name = full_name || ambiguous_names.include?(formula_or_cask)

        if json
          progress&.increment
          unless quiet
            status_hash(formula_or_cask, "error", [e.to_s], full_name: use_full_name,
                                                            verbose:)
          end
        elsif !quiet
          name = package_or_resource_name(formula_or_cask, full_name: use_full_name)
          name += " (cask)" if ambiguous_casks.include?(formula_or_cask)

          onoe "#{Tty.blue}#{name}#{Tty.reset}: #{e}"
          if debug && !e.is_a?(Livecheck::Error)
            require "utils/backtrace"
            $stderr.puts Utils::Backtrace.clean(e)
          end
          print_resources_info(resource_version_info, verbose:) if check_for_resources
          nil
        end
      end

      puts "No newer upstream versions." if newer_only && !has_a_newer_upstream_version && !debug && !json && !quiet

      return unless json

      if progress
        progress.finish
        Tty.with($stderr) do |stderr|
          erase = "#{Tty.up}#{Tty.erase_line}" * 2
          stderr.print "#{Tty.begin_synchronized_update}#{erase}#{Tty.end_synchronized_update}" unless erase.empty?
        end
      end

      puts JSON.pretty_generate(formulae_checked.compact)
    end

    sig { params(package_or_resource: T.any(Formula, Cask::Cask, Resource), full_name: T::Boolean).returns(String) }
    def self.package_or_resource_name(package_or_resource, full_name: false)
      case package_or_resource
      when Formula
        formula_name(package_or_resource, full_name:)
      when Cask::Cask
        cask_name(package_or_resource, full_name:)
      when Resource
        package_or_resource.name.to_s
      else
        T.absurd(package_or_resource)
      end
    end

    # Returns the fully-qualified name of a cask if the `full_name` argument is
    # provided; returns the name otherwise.
    sig { params(cask: Cask::Cask, full_name: T::Boolean).returns(String) }
    private_class_method def self.cask_name(cask, full_name: false)
      full_name ? cask.full_name : cask.token
    end

    # Returns the fully-qualified name of a formula if the `full_name` argument is
    # provided; returns the name otherwise.
    sig { params(formula: Formula, full_name: T::Boolean).returns(String) }
    private_class_method def self.formula_name(formula, full_name: false)
      full_name ? formula.full_name : formula.name
    end

    sig {
      params(
        package_or_resource: T.any(Formula, Cask::Cask, Resource),
        status_str:          String,
        messages:            T.nilable(T::Array[String]),
        full_name:           T::Boolean,
        verbose:             T::Boolean,
      ).returns(T::Hash[Symbol, T.untyped])
    }
    def self.status_hash(package_or_resource, status_str, messages = nil, full_name: false, verbose: false)
      formula = package_or_resource if package_or_resource.is_a?(Formula)
      cask = package_or_resource if package_or_resource.is_a?(Cask::Cask)
      resource = package_or_resource if package_or_resource.is_a?(Resource)

      status_hash = {}
      if formula
        status_hash[:formula] = formula_name(formula, full_name:)
      elsif cask
        status_hash[:cask] = cask_name(cask, full_name:)
      elsif resource
        status_hash[:resource] = resource.name
      end
      status_hash[:status] = status_str
      status_hash[:messages] = messages if messages.is_a?(Array)

      status_hash[:meta] = {
        livecheck_defined: package_or_resource.livecheck_defined?,
      }
      status_hash[:meta][:head_only] = true if formula&.head_only?

      status_hash
    end

    # Formats and prints the livecheck result for a formula/cask/resource.
    sig { params(info: T::Hash[Symbol, T.untyped], verbose: T::Boolean, ambiguous_cask: T::Boolean).void }
    private_class_method def self.print_latest_version(info, verbose: false, ambiguous_cask: false)
      package_or_resource_s = info[:resource].present? ? "  " : ""
      package_or_resource_s += "#{Tty.blue}#{info[:formula] || info[:cask] || info[:resource]}#{Tty.reset}"
      package_or_resource_s += " (cask)" if ambiguous_cask
      package_or_resource_s += " (guessed)" if verbose && !info[:meta][:livecheck_defined]

      current_s = if info[:version][:newer_than_upstream]
        "#{Tty.red}#{info[:version][:current]}#{Tty.reset}"
      else
        info[:version][:current]
      end

      latest_s = if info[:version][:outdated]
        "#{Tty.green}#{info[:version][:latest]}#{Tty.reset}"
      else
        info[:version][:latest]
      end

      puts "#{package_or_resource_s}: #{current_s} ==> #{latest_s}"
    end

    # Prints the livecheck result for the resources of a given Formula.
    sig { params(info: T::Array[T::Hash[Symbol, T.untyped]], verbose: T::Boolean).void }
    private_class_method def self.print_resources_info(info, verbose: false)
      info.each do |r_info|
        if r_info[:status] && r_info[:messages]
          SkipConditions.print_skip_information(r_info)
        else
          print_latest_version(r_info, verbose:)
        end
      end
    end

    sig {
      params(
        livecheck_url:       T.any(String, Symbol),
        package_or_resource: T.any(Formula, Cask::Cask, Resource),
      ).returns(String)
    }
    def self.livecheck_url_to_string(livecheck_url, package_or_resource)
      livecheck_url_string = case livecheck_url
      when String
        livecheck_url
      when :url
        package_or_resource.url&.to_s if package_or_resource.is_a?(Cask::Cask) || package_or_resource.is_a?(Resource)
      when :head, :stable
        package_or_resource.send(livecheck_url)&.url if package_or_resource.is_a?(Formula)
      when :homepage
        package_or_resource.homepage unless package_or_resource.is_a?(Resource)
      end

      if livecheck_url.is_a?(Symbol) && !livecheck_url_string
        raise ArgumentError, "`url #{livecheck_url.inspect}` does not reference a checkable URL"
      end

      livecheck_url_string
    end

    # Returns an Array containing the formula/cask/resource URLs that can be used by livecheck.
    sig { params(package_or_resource: T.any(Formula, Cask::Cask, Resource)).returns(T::Array[String]) }
    def self.checkable_urls(package_or_resource)
      urls = []

      case package_or_resource
      when Formula
        if package_or_resource.stable
          urls << T.must(package_or_resource.stable).url
          urls.concat(T.must(package_or_resource.stable).mirrors)
        end
        urls << T.must(package_or_resource.head).url if package_or_resource.head
        urls << package_or_resource.homepage if package_or_resource.homepage
      when Cask::Cask
        urls << package_or_resource.url.to_s if package_or_resource.url
        urls << package_or_resource.homepage if package_or_resource.homepage
      when Resource
        urls << package_or_resource.url
      else
        T.absurd(package_or_resource)
      end

      urls.compact.uniq
    end

    # livecheck should fetch a URL using brewed curl if the formula/cask
    # contains a `stable`/`url` or `head` URL `using: :homebrew_curl` that
    # shares the same host or uses it as a parent domain.
    sig { params(formula_or_cask: T.any(Formula, Cask::Cask), url: String).returns(T::Boolean) }
    def self.use_homebrew_curl?(formula_or_cask, url)
      host = url_host(url)
      return false unless host

      homebrew_curl_hosts = case formula_or_cask
      when Formula
        [formula_or_cask.stable, formula_or_cask.head].filter_map do |spec|
          next unless spec
          next if spec.using != :homebrew_curl
          next unless (spec_url = spec.url)

          url_host(spec_url)
        end
      when Cask::Cask
        cask_url = formula_or_cask.url
        return false if cask_url&.using != :homebrew_curl

        [url_host(cask_url.to_s)].compact
      end

      homebrew_curl_hosts.any? do |homebrew_curl_host|
        host == homebrew_curl_host ||
          host.end_with?(".#{homebrew_curl_host}") ||
          homebrew_curl_host.end_with?(".#{host}")
      end
    end

    sig { params(url: String).returns(T.nilable(String)) }
    private_class_method def self.url_host(url)
      URI.parse(url).host&.downcase
    rescue URI::InvalidURIError
      nil
    end

    # Identifies the latest version of the formula/cask and returns a Hash containing
    # the version information. Returns nil if a latest version couldn't be found.
    sig {
      params(
        formula_or_cask:            T.any(Formula, Cask::Cask),
        referenced_formula_or_cask: T.nilable(T.any(Formula, Cask::Cask)),
        livecheck_references:       T::Array[T.any(Formula, Cask::Cask)],
        json:                       T::Boolean,
        full_name:                  T::Boolean,
        verbose:                    T::Boolean,
        debug:                      T::Boolean,
      ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
    }
    def self.latest_version(
      formula_or_cask,
      referenced_formula_or_cask: nil,
      livecheck_references: [],
      json: false, full_name: false, verbose: false, debug: false
    )
      formula = formula_or_cask if formula_or_cask.is_a?(Formula)
      cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask)

      livecheck_defined = formula_or_cask.livecheck_defined?
      livecheck = formula_or_cask.livecheck
      referenced_livecheck = referenced_formula_or_cask&.livecheck

      livecheck_options = livecheck.options || referenced_livecheck&.options
      livecheck_url_options = livecheck_options.url_options.compact
      livecheck_url = livecheck.url || referenced_livecheck&.url
      livecheck_regex = livecheck.regex || referenced_livecheck&.regex
      livecheck_strategy = livecheck.strategy || referenced_livecheck&.strategy
      livecheck_strategy_block = livecheck.strategy_block || referenced_livecheck&.strategy_block
      livecheck_throttle = livecheck.throttle || referenced_livecheck&.throttle
      livecheck_throttle_days = livecheck.throttle_days || referenced_livecheck&.throttle_days

      referenced_package = referenced_formula_or_cask || formula_or_cask

      livecheck_url_string = livecheck_url_to_string(livecheck_url, referenced_package) if livecheck_url

      urls = [livecheck_url_string] if livecheck_url_string
      urls ||= checkable_urls(referenced_package)

      if debug
        if formula
          puts "Formula:          #{formula_name(formula, full_name:)}"
          puts "Head only?:       true" if formula.head_only?
        elsif cask
          puts "Cask:             #{cask_name(formula_or_cask, full_name:)}"
        end
        puts "livecheck block?: #{livecheck_defined ? "Yes" : "No"}"
        if livecheck_throttle || livecheck_throttle_days
          throttle_items = []
          if livecheck_throttle
            throttle_items << "#{livecheck_throttle} #{Utils.pluralize("version", livecheck_throttle)}"
          end
          if livecheck_throttle_days
            throttle_items << "#{livecheck_throttle_days} #{Utils.pluralize("day", livecheck_throttle_days)}"
          end
          puts "Throttle:         #{throttle_items.join(" or ")}"
        end

        livecheck_references.each do |ref_formula_or_cask|
          case ref_formula_or_cask
          when Formula
            puts "Formula Ref:      #{formula_name(ref_formula_or_cask, full_name:)}"
          when Cask::Cask
            puts "Cask Ref:         #{cask_name(ref_formula_or_cask, full_name:)}"
          end
        end
      end

      checked_urls = []
      urls.each_with_index do |original_url, i|
        url = original_url
        next if checked_urls.include?(url)

        strategies = Strategy.from_url(
          url,
          livecheck_strategy:,
          regex_provided:     livecheck_regex.present?,
          block_provided:     livecheck_strategy_block.present?,
        )
        strategy = Strategy.from_symbol(livecheck_strategy) || strategies.first
        next unless strategy

        strategy_name = livecheck_strategy_names(strategy)

        if strategy.respond_to?(:preprocess_url)
          url = strategy.preprocess_url(url)
          next if checked_urls.include?(url)
        end

        if debug
          puts
          if livecheck_url.is_a?(Symbol)
            # This assumes the URL symbol will fit within the available space
            puts "URL (#{livecheck_url}):".ljust(18, " ") + original_url
          elsif original_url.present? && original_url != "None"
            puts "URL:              #{original_url}"
          end
          puts "URL (processed):  #{url}" if url != original_url
          puts "URL Options:      #{livecheck_url_options}" if livecheck_url_options.present?
          if strategies.present? && verbose
            puts "Strategies:       #{strategies.map { |s| livecheck_strategy_names(s) }.join(", ")}"
          end
          puts "Strategy:         #{strategy_name}" if strategy.present?
          puts "Regex:            #{livecheck_regex.inspect}" if livecheck_regex.present?
        end

        if livecheck_strategy.present?
          if livecheck_url.blank? && strategy.method(:find_versions).parameters.include?([:keyreq, :url])
            odebug "#{strategy_name} strategy requires a URL"
            next
          elsif livecheck_strategy != :page_match && strategies.exclude?(strategy)
            odebug "#{strategy_name} strategy does not apply to this URL"
            next
          end
        end

        next if strategy.blank?

        if (livecheck_homebrew_curl = livecheck_options.homebrew_curl).nil?
          case strategy_name
          when "PageMatch", "HeaderMatch"
            if (homebrew_curl = use_homebrew_curl?(referenced_package, url))
              livecheck_options = livecheck_options.merge({ homebrew_curl: })
              livecheck_homebrew_curl = homebrew_curl
            end
          end
        end
        puts "Homebrew curl?:   #{livecheck_homebrew_curl ? "Yes" : "No"}" if debug && !livecheck_homebrew_curl.nil?

        # Only use arguments that the strategy's `#find_versions` method
        # supports
        find_versions_parameters = livecheck_find_versions_parameters(strategy)
        strategy_args = {}
        strategy_args[:cask] = cask if find_versions_parameters.include?(:cask)
        strategy_args[:url] = url if find_versions_parameters.include?(:url)
        strategy_args[:regex] = livecheck_regex if find_versions_parameters.include?(:regex)
        strategy_args[:options] = livecheck_options if find_versions_parameters.include?(:options)
        strategy_args.compact!

        strategy_data = strategy.find_versions(**strategy_args, &livecheck_strategy_block)
        match_version_map = strategy_data[:matches]
        regex = strategy_data[:regex]
        messages = strategy_data[:messages]
        checked_urls << url

        if messages.is_a?(Array) && match_version_map.blank?
          puts messages unless json
          next if i + 1 < urls.length

          return status_hash(formula_or_cask, "error", messages, full_name:, verbose:)
        end

        if debug
          if strategy_data[:url].present? && strategy_data[:url] != url
            puts "URL (strategy):   #{strategy_data[:url]}"
          end
          puts "URL (final):      #{strategy_data[:final_url]}" if strategy_data[:final_url].present?
          if strategy_data[:regex].present? && strategy_data[:regex] != livecheck_regex
            puts "Regex (strategy): #{strategy_data[:regex].inspect}"
          end
          puts "Cached?:          Yes" if strategy_data[:cached] == true
        end

        match_version_map.delete_if do |_match, version|
          next true if version.blank?
          next false if livecheck_defined

          UNSTABLE_VERSION_KEYWORDS.any? do |rejection|
            version.to_s.include?(rejection)
          end
        end
        next if match_version_map.blank?

        if debug
          puts
          puts "Matched Versions:"

          if verbose
            match_version_map.each do |match, version|
              puts "#{match} => #{version.inspect}"
            end
          else
            puts match_version_map.values.join(", ")
          end
        end

        version_info = {
          latest: Version.new(match_version_map.values.max_by { |v| LivecheckVersion.create(formula_or_cask, v) }),
        }

        if livecheck_throttle || livecheck_throttle_days
          if livecheck_throttle
            throttled_match_version_map = match_version_map.select do |_match, version|
              throttle_allows_bump?(formula_or_cask, version, throttle_rate: livecheck_throttle)
            end
          end

          if livecheck_throttle_days &&
             throttle_allows_bump?(formula_or_cask, version_info[:latest], throttle_days: livecheck_throttle_days)
            version_info[:latest_throttled] = version_info[:latest]
          elsif throttled_match_version_map.present?
            version_info[:latest_throttled] = Version.new(
              throttled_match_version_map.values.max_by { |v| LivecheckVersion.create(formula_or_cask, v) },
            )
          else
            version_info[:latest_throttled] = nil
          end

          if debug
            puts
            puts "Matched Throttled Versions:"

            if verbose
              throttled_match_version_map.each do |match, version|
                puts "#{match} => #{version.inspect}"
              end
            elsif throttled_match_version_map.present?
              puts throttled_match_version_map.values.join(", ")
            end

            if version_info[:latest_throttled] == version_info[:latest] && throttled_match_version_map.blank?
              puts "#{version_info[:latest_throttled]} (throttle interval elapsed)"
            end
          end
        end

        if json && verbose
          version_info[:meta] = {}

          if livecheck_references.present?
            version_info[:meta][:references] = livecheck_references.map do |ref_formula_or_cask|
              case ref_formula_or_cask
              when Formula
                { formula: formula_name(ref_formula_or_cask, full_name:) }
              when Cask::Cask
                { cask: cask_name(ref_formula_or_cask, full_name:) }
              end
            end
          end

          if url != "None"
            version_info[:meta][:url] = {}
            version_info[:meta][:url][:symbol] = livecheck_url if livecheck_url.is_a?(Symbol) && livecheck_url_string
            version_info[:meta][:url][:original] = original_url
            version_info[:meta][:url][:processed] = url if url != original_url
            if strategy_data[:url].present? && strategy_data[:url] != url
              version_info[:meta][:url][:strategy] = strategy_data[:url]
            end
            version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data[:final_url]
            version_info[:meta][:url][:options] = livecheck_url_options if livecheck_url_options.present?
          end
          version_info[:meta][:strategy] = strategy_name if strategy.present?
          version_info[:meta][:strategies] = strategies.map { |s| livecheck_strategy_names(s) } if strategies.present?
          version_info[:meta][:regex] = regex.inspect if regex.present?
          version_info[:meta][:cached] = true if strategy_data[:cached] == true
          version_info[:meta][:throttle] = livecheck_throttle if livecheck_throttle
          version_info[:meta][:throttle_days] = livecheck_throttle_days if livecheck_throttle_days

          version_info[:content] = strategy_data[:content] if strategy_data[:content] && strategy_name == "Pypi"
        end

        return version_info
      end
      nil
    end

    # Identifies the latest version of a resource and returns a Hash containing the
    # version information. Returns nil if a latest version couldn't be found.
    sig {
      params(
        resource:       Resource,
        formula_latest: String,
        json:           T::Boolean,
        full_name:      T::Boolean,
        debug:          T::Boolean,
        quiet:          T::Boolean,
        verbose:        T::Boolean,
      ).returns(T::Hash[Symbol, T.untyped])
    }
    def self.resource_version(
      resource,
      formula_latest,
      json: false,
      full_name: false,
      debug: false,
      quiet: false,
      verbose: false
    )
      livecheck_defined = resource.livecheck_defined?

      if debug
        puts "\n\n"
        puts "Resource:         #{resource.name}"
        puts "livecheck block?: #{livecheck_defined ? "Yes" : "No"}"
      end

      resource_version_info = {}

      livecheck = resource.livecheck
      livecheck_options = livecheck.options
      livecheck_url_options = livecheck_options.url_options.compact
      livecheck_reference = livecheck.formula
      livecheck_url = livecheck.url
      livecheck_regex = livecheck.regex
      livecheck_strategy = livecheck.strategy
      livecheck_strategy_block = livecheck.strategy_block

      livecheck_url_string = livecheck_url_to_string(livecheck_url, resource) if livecheck_url

      urls = [livecheck_url_string] if livecheck_url_string
      urls = ["None"] if livecheck_reference == :parent
      urls ||= checkable_urls(resource)

      checked_urls = []
      urls.each_with_index do |original_url, i|
        url = original_url.gsub(Constants::LATEST_VERSION, formula_latest)
        next if checked_urls.include?(url)

        strategies = Strategy.from_url(
          url,
          livecheck_strategy:,
          regex_provided:     livecheck_regex.present?,
          block_provided:     livecheck_strategy_block.present?,
        )
        strategy = Strategy.from_symbol(livecheck_strategy) || strategies.first
        next if strategy.blank? && livecheck_reference != :parent

        strategy_name = livecheck_strategy_names(strategy) if strategy.present?

        if strategy.respond_to?(:preprocess_url)
          url = strategy.preprocess_url(url)
          next if checked_urls.include?(url)
        end

        if debug
          puts
          if livecheck_url.is_a?(Symbol)
            # This assumes the URL symbol will fit within the available space
            puts "URL (#{livecheck_url}):".ljust(18, " ") + original_url
          elsif original_url.present? && original_url != "None"
            puts "URL:              #{original_url}"
          end
          puts "URL (processed):  #{url}" if url != original_url
          puts "URL Options:      #{livecheck_url_options}" if livecheck_url_options.present?
          if strategies.present? && verbose
            puts "Strategies:       #{strategies.map { |s| livecheck_strategy_names(s) }.join(", ")}"
          end
          puts "Strategy:         #{strategy_name}" if strategy.present?
          puts "Regex:            #{livecheck_regex.inspect}" if livecheck_regex.present?
          if livecheck_reference == :parent
            resource_owner = resource.owner
            raise "Resource owner is nil" if resource_owner.nil?

            formula = if full_name
              T.cast(resource_owner, ::Formula).full_name
            else
              resource_owner.name
            end
            puts "Formula Ref:      #{formula} (parent)"
          end
        end

        if livecheck_strategy.present?
          if livecheck_url.blank? && strategy.method(:find_versions).parameters.include?([:keyreq, :url])
            odebug "#{strategy_name} strategy requires a URL"
            next
          elsif livecheck_strategy != :page_match && strategies.exclude?(strategy)
            odebug "#{strategy_name} strategy does not apply to this URL"
            next
          end
        end
        puts if debug && strategy.blank? && livecheck_reference != :parent
        next if strategy.blank? && livecheck_reference != :parent

        if debug && !(livecheck_homebrew_curl = livecheck_options.homebrew_curl).nil?
          puts "Homebrew curl?:   #{livecheck_homebrew_curl ? "Yes" : "No"}"
        end

        if livecheck_reference == :parent
          match_version_map = { formula_latest => Version.new(formula_latest) }
          cached = true
        else
          # Only use arguments that the strategy's `#find_versions` method
          # supports
          find_versions_parameters = livecheck_find_versions_parameters(strategy)
          strategy_args = {}
          strategy_args[:url] = url if find_versions_parameters.include?(:url)
          strategy_args[:regex] = livecheck_regex if find_versions_parameters.include?(:regex)
          strategy_args[:options] = livecheck_options if find_versions_parameters.include?(:options)
          strategy_args.compact!

          strategy_data = strategy.find_versions(**strategy_args, &livecheck_strategy_block)
          match_version_map = strategy_data[:matches]
          regex = strategy_data[:regex]
          messages = strategy_data[:messages]
          cached = strategy_data[:cached]
        end

        checked_urls << url

        if messages.is_a?(Array) && match_version_map.blank?
          puts messages unless json
          next if i + 1 < urls.length

          return status_hash(resource, "error", messages, verbose:)
        end

        if debug
          if strategy_data&.dig(:url).present? && strategy_data[:url] != url
            puts "URL (strategy):   #{strategy_data[:url]}"
          end
          puts "URL (final):      #{strategy_data[:final_url]}" if strategy_data&.dig(:final_url).present?
          if strategy_data&.dig(:regex).present? && strategy_data[:regex] != livecheck_regex
            puts "Regex (strategy): #{strategy_data[:regex].inspect}"
          end
          puts "Cached?:          Yes" if cached == true
        end

        match_version_map.delete_if do |_match, version|
          next true if version.blank?
          next false if livecheck_defined

          UNSTABLE_VERSION_KEYWORDS.any? do |rejection|
            version.to_s.include?(rejection)
          end
        end
        next if match_version_map.blank?

        if debug
          puts
          puts "Matched Versions:"

          if verbose
            match_version_map.each do |match, version|
              puts "#{match} => #{version.inspect}"
            end
          else
            puts match_version_map.values.join(", ")
          end
        end

        res_current = T.must(resource.version)
        res_latest = Version.new(match_version_map.values.max_by { |v| LivecheckVersion.create(resource, v) })

        return status_hash(resource, "error", [NO_VERSIONS_MSG], verbose:) if res_latest.blank?

        is_outdated = res_current < res_latest
        is_newer_than_upstream = res_current > res_latest

        resource_version_info = {
          resource: resource.name,
          version:  {
            current:             res_current.to_s,
            latest:              res_latest.to_s,
            outdated:            is_outdated,
            newer_than_upstream: is_newer_than_upstream,
          },
        }

        resource_version_info[:meta] = {
          livecheck_defined: livecheck_defined,
        }
        if livecheck_reference == :parent
          resource_owner = resource.owner
          raise "Resource owner is nil" if resource_owner.nil?

          formula = if full_name
            T.cast(resource_owner, ::Formula).full_name
          else
            resource_owner.name
          end
          resource_version_info[:meta][:references] =
            [{ formula:, symbol: :parent }]
        end
        if url != "None"
          resource_version_info[:meta][:url] = {}
          if livecheck_url.is_a?(Symbol) && livecheck_url_string
            resource_version_info[:meta][:url][:symbol] = livecheck_url
          end
          resource_version_info[:meta][:url][:original] = original_url
          resource_version_info[:meta][:url][:processed] = url if url != original_url
          if strategy_data&.dig(:url).present? && strategy_data[:url] != url
            resource_version_info[:meta][:url][:strategy] = strategy_data[:url]
          end
          resource_version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data&.dig(:final_url)
          resource_version_info[:meta][:url][:options] = livecheck_url_options if livecheck_url_options.present?
        end
        resource_version_info[:meta][:strategy] = strategy_name if strategy.present?
        if strategies.present?
          resource_version_info[:meta][:strategies] = strategies.map { |s| livecheck_strategy_names(s) }
        end
        resource_version_info[:meta][:regex] = regex.inspect if regex.present?
        resource_version_info[:meta][:cached] = true if cached == true
      rescue => e
        Homebrew.failed = true
        if json
          status_hash(resource, "error", [e.to_s], verbose:)
        elsif !quiet
          onoe "#{Tty.blue}#{resource.name}#{Tty.reset}: #{e}"
          if debug && !e.is_a?(Livecheck::Error)
            require "utils/backtrace"
            $stderr.puts Utils::Backtrace.clean(e)
          end
          nil
        end
      end
      resource_version_info
    end

    sig {
      params(
        formula_or_cask: T.any(Formula, Cask::Cask),
        version:         T.any(String, Version),
        throttle_rate:   T.nilable(Integer),
        throttle_days:   T.nilable(Integer),
      ).returns(T::Boolean)
    }
    def self.throttle_allows_bump?(formula_or_cask, version, throttle_rate: nil, throttle_days: nil)
      return true if throttle_rate.nil? && throttle_days.nil?

      unless throttle_rate.nil?
        version = Version.new(version) unless version.is_a?(Version)
        return true if version.patch.to_i.modulo(throttle_rate).zero?
      end

      !throttle_days.nil? && throttle_interval_elapsed?(formula_or_cask, throttle_days)
    end

    sig { params(package_or_resource: T.any(Formula, Cask::Cask)).returns(T.nilable(Integer)) }
    def self.formula_or_cask_last_updated_timestamp(package_or_resource)
      tap = package_or_resource.tap
      return if tap.nil?
      return unless tap.git?
      return unless Utils::Git.available?

      if package_or_resource.is_a?(Formula)
        timestamp = formula_last_version_update_timestamp(package_or_resource, tap:)
        return timestamp if timestamp.present?
      end

      formula_or_cask_last_commit_timestamp(package_or_resource, tap)
    end

    sig { params(formula: Formula, tap: Tap).returns(T.nilable(Integer)) }
    private_class_method def self.formula_last_version_update_timestamp(formula, tap:)
      stable = formula.stable
      return if stable.blank?

      version_update_revision = find_version_update_revision(formula, stable.version)
      return if version_update_revision.nil?

      timestamp_for_revision(tap.path, version_update_revision)
    end

    sig { params(formula: Formula, current_version: Version).returns(T.nilable(String)) }
    private_class_method def self.find_version_update_revision(formula, current_version)
      version_update_revision = T.let(nil, T.nilable(String))
      found_current_version = T.let(false, T::Boolean)

      formula_versions = FormulaVersions.new(formula)
      current_version = origin_stable_version(formula, formula_versions) || current_version

      formula_versions.rev_list("HEAD") do |revision, path|
        formula_versions.formula_at_revision(revision, path) do |historical_formula|
          historical_stable = historical_formula.stable
          next if historical_stable.blank?

          if historical_stable.version == current_version
            found_current_version = true
            version_update_revision = revision
          elsif found_current_version
            return version_update_revision
          end
        end
      rescue MacOSVersion::Error, LegacyDSLError
        break
      end

      version_update_revision
    end

    sig { params(formula: Formula, formula_versions: FormulaVersions).returns(T.nilable(Version)) }
    private_class_method def self.origin_stable_version(formula, formula_versions)
      tap = formula.tap
      return if tap.nil?

      revision = Utils.popen_read(
        Utils::Git.git, "rev-parse", "origin/HEAD",
        chdir: tap.path
      ).chomp.presence
      return if revision.nil?

      relative_path = formula.path.relative_path_from(tap.path).to_s
      version = T.let(nil, T.nilable(Version))
      formula_versions.formula_at_revision(revision, relative_path) do |historical_formula|
        version = historical_formula.stable&.version
      end
      version
    rescue MacOSVersion::Error, LegacyDSLError
      nil
    end

    sig {
      params(package_or_resource: T.any(Formula, Cask::Cask), tap: Tap).returns(T.nilable(Integer))
    }
    private_class_method def self.formula_or_cask_last_commit_timestamp(package_or_resource, tap)
      sourcefile = case package_or_resource
      when Formula
        package_or_resource.path
      when Cask::Cask
        package_or_resource.sourcefile_path
      end
      return if sourcefile.nil?

      default_branch = Utils.popen_read(
        Utils::Git.git,
        "symbolic-ref",
        "refs/remotes/origin/HEAD",
        "--short",
      ).chomp.delete_prefix("origin/").presence || "main"

      relative_sourcefile = sourcefile.relative_path_from(tap.path).to_s
      timestamp = Utils.popen_read(
        Utils::Git.git,
        "log",
        default_branch,
        "-1",
        "--format=%ct",
        "--",
        relative_sourcefile,
        chdir: tap.path,
      ).chomp.presence
      return if timestamp.nil?

      Integer(timestamp, exception: false)
    rescue ArgumentError
      nil
    end

    sig { params(repository_path: Pathname, revision: String).returns(T.nilable(Integer)) }
    private_class_method def self.timestamp_for_revision(repository_path, revision)
      timestamp = Utils.popen_read(
        Utils::Git.git,
        "show",
        "-s",
        "--format=%ct",
        revision,
        chdir: repository_path,
      ).chomp.presence
      return if timestamp.nil?

      Integer(timestamp, exception: false)
    rescue ArgumentError
      nil
    end

    sig { params(package_or_resource: T.any(Formula, Cask::Cask), days: Integer).returns(T::Boolean) }
    def self.throttle_interval_elapsed?(package_or_resource, days)
      return false if days <= 0

      last_updated_timestamp = formula_or_cask_last_updated_timestamp(package_or_resource)
      return false if last_updated_timestamp.nil?

      elapsed_seconds = Time.now.to_i - last_updated_timestamp
      elapsed_seconds >= (days * 24 * 60 * 60)
    end
  end
end
