# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true

require "utils/svn"

module Homebrew
  # Auditor for checking common violations in {Resource}s.
  class ResourceAuditor
    include Utils::Curl

    attr_reader :name, :version, :checksum, :url, :mirrors, :using, :specs, :owner, :spec_name, :problems

    def initialize(resource, spec_name, options = {})
      @name     = resource.name
      @version  = resource.version
      @checksum = resource.checksum
      @url      = resource.url
      @mirrors  = resource.mirrors
      @using    = resource.using
      @specs    = resource.specs
      @owner    = resource.owner
      @spec_name = spec_name
      @online    = options[:online]
      @strict    = options[:strict]
      @only      = options[:only]
      @except    = options[:except]
      @core_tap  = options[:core_tap]
      @use_homebrew_curl = options[:use_homebrew_curl]
      @problems = []
    end

    def audit
      only_audits = @only
      except_audits = @except

      methods.map(&:to_s).grep(/^audit_/).each do |audit_method_name|
        name = audit_method_name.delete_prefix("audit_")
        next if only_audits&.exclude?(name)
        next if except_audits&.include?(name)

        send(audit_method_name)
      end

      self
    end

    def audit_version
      if version.nil?
        problem "Missing version"
      elsif owner.is_a?(Formula) && !version.to_s.match?(GitHubPackages::VALID_OCI_TAG_REGEX) &&
            (owner.core_formula? ||
            (owner.bottle_defined? && GitHubPackages::URL_REGEX.match?(owner.bottle_specification.root_url)))
        problem "`version #{version}` does not match #{GitHubPackages::VALID_OCI_TAG_REGEX.source}"
      elsif !version.detected_from_url?
        version_text = version
        version_url = Version.detect(url, **specs)
        if version_url.to_s == version_text.to_s && version.instance_of?(Version)
          problem "`version #{version_text}` is redundant with version scanned from URL"
        end
      end
    end

    def audit_download_strategy
      url_strategy = DownloadStrategyDetector.detect(url)

      if (using == :git || url_strategy == GitDownloadStrategy) && specs[:tag] && !specs[:revision]
        problem "Git should specify `revision:` when a `tag:` is specified."
      end

      return unless using

      if using == :cvs
        mod = specs[:module]

        problem "Redundant `module:` value in URL" if mod == name

        if url.match?(%r{:[^/]+$})
          mod = url.split(":").last

          if mod == name
            problem "Redundant CVS module appended to URL"
          else
            problem "Specify CVS module as `module: \"#{mod}\"` instead of appending it to the URL"
          end
        end
      end

      return if url_strategy != DownloadStrategyDetector.detect("", using)

      problem "Redundant `using:` value in URL"
    end

    def audit_checksum
      return if spec_name == :head
      # This condition is non-invertible.
      # rubocop:disable Style/InvertibleUnlessCondition
      return unless DownloadStrategyDetector.detect(url, using) <= CurlDownloadStrategy
      # rubocop:enable Style/InvertibleUnlessCondition

      problem "Checksum is missing" if checksum.blank?
    end

    def self.curl_deps
      @curl_deps ||= begin
        ["curl"] + Formula["curl"].recursive_dependencies.map(&:name).uniq
      rescue FormulaUnavailableError
        []
      end
    end

    def audit_resource_name_matches_pypi_package_name_in_url
      return unless url.match?(%r{^https?://files\.pythonhosted\.org/packages/})
      return if name == owner.name # Skip the top-level package name as we only care about `resource "foo"` blocks.

      if url.end_with? ".whl"
        path = URI(url).path
        return unless path.present?

        pypi_package_name, = File.basename(path).split("-", 2)
      else
        url =~ %r{/(?<package_name>[^/]+)-}
        pypi_package_name = Regexp.last_match(:package_name).to_s
      end

      T.must(pypi_package_name).gsub!(/[_.]/, "-")

      return if name.casecmp(pypi_package_name).zero?

      problem "`resource` name should be '#{pypi_package_name}' to match the PyPI package name"
    end

    def audit_urls
      urls = [url] + mirrors

      curl_dep = self.class.curl_deps.include?(owner.name)
      # Ideally `ca-certificates` would not be excluded here, but sourcing a HTTP mirror was tricky.
      # Instead, we have logic elsewhere to pass `--insecure` to curl when downloading the certs.
      # TODO: try remove the OS/env conditional
      if Homebrew::SimulateSystem.simulating_or_running_on_macos? && spec_name == :stable &&
         owner.name != "ca-certificates" && curl_dep && !urls.find { |u| u.start_with?("http://") }
        problem "Should always include at least one HTTP mirror"
      end

      return unless @online

      urls.each do |url|
        next if !@strict && mirrors.include?(url)

        strategy = DownloadStrategyDetector.detect(url, using)
        if strategy <= CurlDownloadStrategy && !url.start_with?("file")

          raise HomebrewCurlDownloadStrategyError, url if
            strategy <= HomebrewCurlDownloadStrategy && !Formula["curl"].any_version_installed?

          # Skip https audit for curl dependencies
          if !curl_dep && (http_content_problem = curl_check_http_content(
            url,
            "source URL",
            specs:,
            use_homebrew_curl: @use_homebrew_curl,
          ))
            problem http_content_problem
          end
        elsif strategy <= GitDownloadStrategy
          attempts = 0
          remote_exists = T.let(false, T::Boolean)
          while !remote_exists && attempts < Homebrew::EnvConfig.curl_retries.to_i
            remote_exists = Utils::Git.remote_exists?(url)
            attempts += 1
          end
          problem "The URL #{url} is not a valid Git URL" unless remote_exists
        elsif strategy <= SubversionDownloadStrategy
          next unless DevelopmentTools.subversion_handles_most_https_certificates?
          next unless Utils::Svn.available?

          problem "The URL #{url} is not a valid SVN URL" unless Utils::Svn.remote_exists? url
        end
      end
    end

    def audit_head_branch
      return unless @online
      return if spec_name != :head
      return if specs[:tag].present?
      return if specs[:revision].present?
      # Skip `resource` URLs as they use SHAs instead of branch specifiers.
      return if name != owner.name
      return unless url.end_with?(".git")
      return unless Utils::Git.remote_exists?(url)

      detected_branch = Utils.popen_read("git", "ls-remote", "--symref", url, "HEAD")
                             .match(%r{ref: refs/heads/(.*?)\s+HEAD})&.to_a&.second

      if specs[:branch].blank?
        problem "Git `head` URL must specify a branch name"
        return
      end

      return unless @core_tap
      return if specs[:branch] == detected_branch

      problem "To use a non-default HEAD branch, add the formula to `head_non_default_branch_allowlist.json`."
    end

    def problem(text)
      @problems << text
    end
  end
end
