# typed: strict
# frozen_string_literal: true

require "utils/output"

# Helper functions for updating CPAN resources.
module CPAN
  METACPAN_URL_PREFIX = "https://cpan.metacpan.org/authors/id/"
  CPAN_ARCHIVE_REGEX = /^(.+)-([0-9.v]+)\.(?:tar\.gz|tgz)$/
  private_constant :METACPAN_URL_PREFIX, :CPAN_ARCHIVE_REGEX

  extend Utils::Output::Mixin

  # Represents a Perl package from an existing resource.
  class Package
    sig { params(resource_name: String, resource_url: String).void }
    def initialize(resource_name, resource_url)
      @cpan_info = T.let(nil, T.nilable(T::Array[String]))
      @resource_name = resource_name
      @resource_url = resource_url
      @is_cpan_url = T.let(resource_url.start_with?(METACPAN_URL_PREFIX), T::Boolean)
    end

    sig { returns(String) }
    def name
      @resource_name
    end

    sig { returns(T.nilable(String)) }
    def current_version
      extract_version_from_url if @current_version.blank?
      @current_version
    end

    sig { returns(T::Boolean) }
    def valid_cpan_package?
      @is_cpan_url
    end

    # Get latest release information from MetaCPAN API.
    sig { returns(T.nilable(T::Array[String])) }
    def latest_cpan_info
      return @cpan_info if @cpan_info.present?
      return unless valid_cpan_package?

      metadata_url = "https://fastapi.metacpan.org/v1/download_url/#{@resource_name}"
      result = Utils::Curl.curl_output(metadata_url, "--location", "--fail")
      return unless result.status.success?

      begin
        json = JSON.parse(result.stdout)
      rescue JSON::ParserError
        return
      end

      download_url = json["download_url"]
      return unless download_url

      checksum = json["checksum_sha256"]
      return unless checksum

      @cpan_info = [@resource_name, download_url, checksum, json["version"]]
    end

    sig { returns(String) }
    def to_s
      @resource_name
    end

    private

    sig { returns(T.nilable(String)) }
    def extract_version_from_url
      return unless @is_cpan_url

      match = File.basename(@resource_url).match(CPAN_ARCHIVE_REGEX)
      return unless match

      @current_version = T.let(match[2], T.nilable(String))
    end
  end

  # Update CPAN resources in a formula.
  sig {
    params(
      formula:       Formula,
      print_only:    T.nilable(T::Boolean),
      quiet:         T.nilable(T::Boolean),
      verbose:       T.nilable(T::Boolean),
      ignore_errors: T.nilable(T::Boolean),
    ).returns(T.nilable(T::Boolean))
  }
  def self.update_perl_resources!(formula, print_only: false, quiet: false, verbose: false, ignore_errors: false)
    cpan_resources = formula.resources.select { |resource| resource.url.start_with?(METACPAN_URL_PREFIX) }

    odie "\"#{formula.name}\" has no CPAN resources to update." if cpan_resources.empty?

    show_info = !print_only && !quiet

    non_cpan_resources = formula.resources.reject { |resource| resource.url.start_with?(METACPAN_URL_PREFIX) }
    ohai "Skipping #{non_cpan_resources.length} non-CPAN resources" if non_cpan_resources.any? && show_info
    ohai "Found #{cpan_resources.length} CPAN resources to update" if show_info

    new_resource_blocks = ""
    package_errors = ""
    updated_count = 0

    cpan_resources.each do |resource|
      package = Package.new(resource.name, resource.url)

      unless package.valid_cpan_package?
        if ignore_errors
          package_errors += "  # RESOURCE-ERROR: \"#{resource.name}\" is not a valid CPAN resource\n"
          next
        else
          odie "\"#{resource.name}\" is not a valid CPAN resource"
        end
      end

      ohai "Checking \"#{resource.name}\" for updates..." if show_info

      info = package.latest_cpan_info

      unless info
        if ignore_errors
          package_errors += "  # RESOURCE-ERROR: Unable to resolve \"#{resource.name}\"\n"
          next
        else
          odie "Unable to resolve \"#{resource.name}\""
        end
      end

      name, url, checksum, new_version = info
      current_version = package.current_version

      if current_version && new_version && current_version != new_version
        ohai "\"#{resource.name}\": #{current_version} -> #{new_version}" if show_info
        updated_count += 1
      elsif show_info
        ohai "\"#{resource.name}\": already up to date (#{current_version})" if current_version
      end

      new_resource_blocks += <<-EOS
  resource "#{name}" do
    url "#{url}"
    sha256 "#{checksum}"
  end

      EOS
    end

    package_errors += "\n" if package_errors.present?
    resource_section = "#{package_errors}#{new_resource_blocks}"

    if print_only
      puts resource_section.chomp
      return true
    end

    ohai "Updating resource blocks" unless quiet
    require "utils/ast"

    formula_ast = Utils::AST::FormulaAST.new(formula.path.read)
    if formula_ast.replace_resource_stanzas(
      resource_section,
      replace_existing: formula.resources.any? { |resource| !resource.name.start_with?("homebrew-") },
    ) == :multiple_groups
      odie "Unable to update resource blocks for \"#{formula.name}\" automatically. Please update them manually."
    end
    formula.path.atomic_write(formula_ast.process)

    if package_errors.present?
      ofail "Unable to resolve some dependencies. Please check #{formula.path} for RESOURCE-ERROR comments."
    elsif updated_count.positive?
      ohai "Updated #{updated_count} CPAN resource#{"s" if updated_count != 1}" unless quiet
    end

    true
  end
end
