# typed: strict
# frozen_string_literal: true

require "formula_installer"
require "unpack_strategy"
require "utils/topological_hash"
require "utils/analytics"
require "utils/output"

require "api/cask_download"
require "cask/config"
require "cask/dsl"
require "cask/download"
require "cask/migrator"
require "cask/quarantine"
require "cask/tab"
require "trust"

module Cask
  # Installer for a {Cask}.
  class Installer
    extend ::Utils::Output::Mixin
    include ::Utils::Output::Mixin

    sig { returns(::Cask::Cask) }
    attr_reader :cask

    sig {
      params(
        cask: ::Cask::Cask, command: T.class_of(SystemCommand), force: T::Boolean, adopt: T::Boolean,
        skip_cask_deps: T::Boolean, binaries: T::Boolean, verbose: T::Boolean, zap: T::Boolean,
        require_sha: T::Boolean, upgrade: T::Boolean, reinstall: T::Boolean,
        installed_on_request: T::Boolean, verify_download_integrity: T::Boolean,
        quiet: T::Boolean, download_queue: Homebrew::DownloadQueue, defer_fetch: T::Boolean,
        default_uninstall_artifacts: T.nilable(ArtifactSet)
      ).void
    }
    def initialize(cask, command: SystemCommand, force: false, adopt: false,
                   skip_cask_deps: false, binaries: true, verbose: false,
                   zap: false, require_sha: false, upgrade: false, reinstall: false,
                   installed_on_request: true,
                   verify_download_integrity: true, quiet: false,
                   download_queue: Homebrew.default_download_queue, defer_fetch: false,
                   default_uninstall_artifacts: nil)
      @cask = cask
      @command = command
      @force = force
      @adopt = adopt
      @skip_cask_deps = skip_cask_deps
      @binaries = binaries
      @verbose = verbose
      @zap = zap
      @require_sha = require_sha
      @reinstall = reinstall
      @upgrade = upgrade
      @installed_on_request = installed_on_request
      @verify_download_integrity = verify_download_integrity
      @quiet = quiet
      @download_queue = download_queue
      @defer_fetch = defer_fetch
      @source_download = T.let(nil, T.nilable(Homebrew::API::SourceDownload))
      # Restricts what `#uninstall` removes, for artifacts that are shared with a cask
      # which must be kept installed.
      @default_uninstall_artifacts = default_uninstall_artifacts
      @ran_prelude_fetch = T.let(false, T::Boolean)
      @ran_prelude = T.let(false, T::Boolean)
      @cask_and_formula_dependencies = T.let(nil, T.nilable(T::Array[T.any(Formula, ::Cask::Cask)]))
      @installed_uninstall_artifacts_missing = T.let(false, T::Boolean)
    end

    sig { returns(T::Boolean) }
    def adopt? = @adopt

    sig { returns(T::Boolean) }
    def binaries? = @binaries

    sig { returns(T::Boolean) }
    def force? = @force

    sig { returns(T::Boolean) }
    def installed_on_request? = @installed_on_request

    sig { returns(T::Boolean) }
    def quiet? = @quiet

    sig { returns(T::Boolean) }
    def reinstall? = @reinstall

    sig { returns(T::Boolean) }
    def require_sha? = @require_sha

    sig { returns(T::Boolean) }
    def skip_cask_deps? = @skip_cask_deps

    sig { returns(T::Boolean) }
    def upgrade? = @upgrade

    sig { returns(T::Boolean) }
    def verbose? = @verbose

    sig { returns(T::Boolean) }
    def zap? = @zap

    sig { params(cask: ::Cask::Cask).returns(T.nilable(String)) }
    def self.caveats(cask)
      odebug "Printing caveats"

      caveats = cask.caveats
      return if caveats.empty?

      Homebrew.messages.record_caveats(cask.token, caveats)

      <<~EOS
        #{ohai_title "Caveats"}
        #{caveats}
      EOS
    end

    sig { params(quiet: T.nilable(T::Boolean), timeout: T.nilable(T.any(Integer, Float))).void }
    def fetch(quiet: nil, timeout: nil)
      odebug "Cask::Installer#fetch"

      prelude

      download(quiet:, timeout:) unless @defer_fetch

      satisfy_cask_and_formula_dependencies
    end

    sig { void }
    def stage
      odebug "Cask::Installer#stage"

      Caskroom.ensure_caskroom_exists

      queued_staged_path = downloader.staged_path_from_download_queue
      queued_staged_marker = downloader.staged_path_from_download_queue_marker
      if @defer_fetch && queued_staged_marker.exist? && !@cask.staged_path.exist?
        @cask.staged_path.dirname.mkpath
        FileUtils.mv(queued_staged_path, @cask.staged_path)
        downloader.purge_staged_from_download_queue(command: @command)
      else
        downloader.purge_staged_from_download_queue(command: @command) if @defer_fetch
        extract_primary_container
        process_rename_operations
      end
      save_caskfile
    rescue => e
      downloader.purge_staged_from_download_queue(command: @command) if @defer_fetch
      purge_versioned_files
      raise e
    end

    sig { void }
    def install
      start_time = Time.now
      odebug "Cask::Installer#install"

      Migrator.migrate_if_needed(@cask)

      old_config = @cask.config
      predecessor = @cask if reinstall? && @cask.installed?

      prelude

      print caveats
      fetch
      uninstall_existing_cask if reinstall?

      backup if force? && @cask.staged_path.exist? && @cask.metadata_versioned_path.exist?

      oh1 "Installing Cask #{Formatter.identifier(@cask)}"
      stage

      @cask.config = @cask.default_config.merge(old_config)

      install_artifacts(predecessor:)

      tab = Tab.create(@cask)
      tab.installed_on_request = installed_on_request?
      tab.write

      if (tap = @cask.tap) && tap.should_report_analytics?
        ::Utils::Analytics.report_package_event(:cask_install, package_name: @cask.token, tap_name: tap.name,
on_request: true)
      end

      purge_backed_up_versioned_files

      puts summary
      end_time = Time.now
      Homebrew.messages.package_installed(@cask.token, end_time - start_time)
    rescue
      restore_backup
      raise
    end

    sig { void }
    def check_deprecate_disable
      deprecate_disable_type = DeprecateDisable.type(@cask)
      return if deprecate_disable_type.nil?

      message = DeprecateDisable.message(@cask).to_s
      message_full = "#{@cask.token} has been #{message}"

      case deprecate_disable_type
      when :deprecated
        opoo message_full
      when :disabled
        GitHub::Actions.puts_annotation_if_env_set!(:error, message)
        raise CaskCannotBeInstalledError.new(@cask, message)
      end
    end

    sig { void }
    def check_conflicts
      return unless @cask.conflicts_with

      @cask.conflicts_with[:cask].each do |conflicting_cask|
        if (conflicting_cask_tap_with_token = Tap.with_cask_token(conflicting_cask))
          conflicting_cask_tap, = conflicting_cask_tap_with_token
          next unless conflicting_cask_tap.installed?
        end

        if (installed_caskfile = Caskroom.cask_installed_caskfile(conflicting_cask))
          raise CaskConflictError.new(
            @cask,
            ::Cask::Cask.new(CaskLoader.token_from_path(installed_caskfile)),
          )
        end

        conflicting_cask = CaskLoader.load(conflicting_cask)
        raise CaskConflictError.new(@cask, conflicting_cask) if conflicting_cask.installed?
      rescue CaskUnavailableError, Homebrew::UntrustedTapError
        next # Ignore conflicting Casks that are unavailable or untrusted.
      end
    end

    sig { void }
    def uninstall_existing_cask
      return unless @cask.installed?

      # Always force uninstallation, ignore method parameter
      cask_installer = Installer.new(@cask, verbose: verbose?, force: true, upgrade: upgrade?, reinstall: true)
      zap? ? cask_installer.zap : cask_installer.uninstall(successor: @cask)
    end

    sig { returns(String) }
    def summary
      s = +""
      s << "#{Homebrew::EnvConfig.install_badge}  " unless Homebrew::EnvConfig.no_emoji?
      s << "#{@cask} was successfully #{upgrade? ? "upgraded" : "installed"}!"
      s.freeze
    end

    sig { returns(Download) }
    def downloader
      @downloader ||= T.let(
        (if @cask.loaded_from_internal_api? && !@cask.caskfile_only?
           Homebrew::API::CaskDownload.download(
             token:       @cask.token,
             cask_struct: Homebrew::API::Internal.cask_struct(@cask.token),
             languages:   @cask.config.languages,
             require_sha: require_sha? && !force?,
           )
        end) || Download.new(@cask, require_sha: require_sha? && !force?),
        T.nilable(Download),
      )
    end

    sig { params(quiet: T.nilable(T::Boolean), timeout: T.nilable(T.any(Integer, Float))).returns(Pathname) }
    def download(quiet: nil, timeout: nil)
      # Store cask download path in cask to prevent multiple downloads in a row when checking if it's outdated
      @cask.download ||= downloader.fetch(quiet:, verify_download_integrity: @verify_download_integrity,
                                          timeout:)
    end

    sig { returns(UnpackStrategy) }
    def primary_container
      download(quiet: true) if @cask.download.nil?

      downloader.primary_container
    end

    sig { returns(ArtifactSet) }
    def artifacts
      @default_uninstall_artifacts || @cask.artifacts
    end

    sig { params(to: Pathname).void }
    def extract_primary_container(to: @cask.staged_path)
      downloader.extract_primary_container(to:, verbose: verbose?)
    end

    sig { params(target_dir: T.nilable(Pathname)).void }
    def process_rename_operations(target_dir: nil)
      downloader.process_rename_operations(target_dir: target_dir || @cask.staged_path)
    end

    sig { params(predecessor: T.nilable(Cask)).void }
    def install_artifacts(predecessor: nil)
      already_installed_artifacts = []

      odebug "Installing artifacts"

      artifacts.each do |artifact|
        next unless artifact.respond_to?(:install_phase)

        odebug "Installing artifact of class #{artifact.class}"

        next if artifact.is_a?(Artifact::Binary) && !binaries?

        artifact = T.cast(
          artifact,
          T.any(
            Artifact::AbstractFlightBlock,
            Artifact::GeneratedCompletion,
            Artifact::Installer,
            Artifact::KeyboardLayout,
            Artifact::Mdimporter,
            Artifact::Moved,
            Artifact::PostflightSteps,
            Artifact::PreflightSteps,
            Artifact::Pkg,
            Artifact::Qlplugin,
            Artifact::Symlinked,
          ),
        )

        artifact.install_phase(
          command: @command, verbose: verbose?, adopt: adopt?, auto_updates: @cask.auto_updates,
          force: force?, predecessor:
        )
        already_installed_artifacts.unshift(artifact)
      end

      save_config_file
      save_download_sha if @cask.version.latest?
    rescue => e
      begin
        already_installed_artifacts&.each do |artifact|
          if artifact.respond_to?(:uninstall_phase)
            odebug "Reverting installation of artifact of class #{artifact.class}"
            artifact.uninstall_phase(command: @command, verbose: verbose?, force: force?)
          end

          next unless artifact.respond_to?(:post_uninstall_phase)

          odebug "Reverting installation of artifact of class #{artifact.class}"
          artifact.post_uninstall_phase(command: @command, verbose: verbose?, force: force?)
        end
      ensure
        purge_versioned_files
        raise e
      end
    end

    sig { void }
    def check_requirements
      check_stanza_os_requirements
      check_supported_system
      check_macos_requirements
      check_arch_requirements
    end

    sig { void }
    def check_stanza_os_requirements
      return if @cask.supports_macos?

      raise CaskError, "#{@cask}: This cask requires Linux."
    end

    sig { void }
    def check_supported_system
      # Audited casks always have an activatable artifact for the systems they
      # support, so API data without one means this system is unsupported.
      # Source loads keep working for unaudited casks, e.g. naked containers.
      return unless @cask.loaded_from_api?
      return if @cask.artifacts.any? do |artifact|
        artifact.respond_to?(:install_phase) || artifact.is_a?(Artifact::StageOnly)
      end

      os_name = Homebrew::SimulateSystem.simulating_or_running_on_macos? ? "macOS" : "Linux"
      raise CaskError, "#{@cask}: This cask is not available on #{os_name}."
    end

    sig { void }
    def check_macos_requirements
      macos_requirement = [@cask.depends_on.macos, @cask.depends_on.maximum_macos].compact.find { !it.satisfied? }
      return unless macos_requirement

      raise CaskError, "#{@cask}: #{macos_requirement.message(type: :cask)}"
    end

    sig { void }
    def check_arch_requirements
      return if @cask.depends_on.arch.nil?

      @current_arch = T.let(@current_arch, T.nilable(T::Hash[Symbol, T.untyped]))
      @current_arch ||= { type: Hardware::CPU.type, bits: Hardware::CPU.bits }
      return if @cask.depends_on.arch.any? do |arch|
        arch[:type] == @current_arch[:type] &&
        Array(arch[:bits]).include?(@current_arch[:bits])
      end

      raise CaskError,
            "#{@cask}: This cask depends on hardware architecture being one of " \
            "[#{@cask.depends_on.arch.join(", ")}], " \
            "but you are running #{@current_arch}."
    end

    sig { returns(T::Array[T.any(Formula, ::Cask::Cask)]) }
    def cask_and_formula_dependencies
      return @cask_and_formula_dependencies if @cask_and_formula_dependencies

      graph = ::Utils::TopologicalHash.graph_package_dependencies(@cask)

      raise CaskSelfReferencingDependencyError, @cask.token if graph.fetch(@cask).include?(@cask)

      pc = primary_container
      raise "unexpected nil primary_container" unless pc

      ::Utils::TopologicalHash.graph_package_dependencies(pc.dependencies, graph)

      @cask_and_formula_dependencies = graph.tsort_with_cycles do |cycles|
        cyclic_dependencies = cycles.sort_by(&:count).fetch(-1) - [@cask]
        raise CaskCyclicDependencyError.new(@cask.token, cyclic_dependencies.to_sentence)
      end - [@cask]
    end

    sig { returns(T::Array[T.any(Formula, ::Cask::Cask)]) }
    def missing_cask_and_formula_dependencies
      cask_and_formula_dependencies.reject do |cask_or_formula|
        case cask_or_formula
        when Formula
          cask_or_formula.any_version_installed? && cask_or_formula.optlinked?
        when Cask
          cask_or_formula.installed?
        end
      end
    end

    sig { void }
    def satisfy_cask_and_formula_dependencies
      return unless installed_on_request?

      formulae_and_casks = cask_and_formula_dependencies

      return if formulae_and_casks.empty?

      missing_formulae_and_casks = missing_cask_and_formula_dependencies

      if missing_formulae_and_casks.empty?
        puts "All dependencies satisfied."
        return
      end

      ohai "Installing dependencies: #{missing_formulae_and_casks.join(", ")}"
      cask_installers = T.let([], T::Array[Installer])
      formula_installers = T.let([], T::Array[FormulaInstaller])

      missing_formulae_and_casks.each do |cask_or_formula|
        if cask_or_formula.is_a?(Cask)
          if skip_cask_deps?
            opoo "`--skip-cask-deps` is set; skipping installation of #{cask_or_formula}."
            next
          end

          cask_installers << Installer.new(
            cask_or_formula,
            adopt:                adopt?,
            binaries:             binaries?,
            force:                false,
            installed_on_request: false,
            quiet:                quiet?,
            require_sha:          require_sha?,
            verbose:              verbose?,
          )
        else
          formula_installers << FormulaInstaller.new(
            cask_or_formula,
            **{
              show_header:          true,
              installed_on_request: false,
              verbose:              verbose?,
            }.compact,
          )
        end
      end

      cask_installers.each(&:install)
      return if formula_installers.blank?

      Homebrew::Install.perform_preinstall_checks_once
      valid_formula_installers = Homebrew::Install.fetch_formulae(formula_installers)
      valid_formula_installers.each do |formula_installer|
        formula_installer.install
        formula_installer.finish
      end
    end

    sig { returns(T.nilable(String)) }
    def caveats
      self.class.caveats(@cask)
    end

    sig { returns(Pathname) }
    def metadata_subdir
      @metadata_subdir ||= T.let(
        begin
          msd = @cask.metadata_subdir("Casks", timestamp: :now, create: true)
          raise "unexpected nil metadata_subdir" unless msd

          msd
        end,
        T.nilable(Pathname),
      )
    end

    sig { void }
    def save_caskfile
      old_savedir = @cask.metadata_timestamped_path

      return if @cask.source.blank?

      if @cask.uninstall_flight_blocks?
        (metadata_subdir/"#{@cask.token}.rb").write @cask.source.to_s
      else
        installed_json = @cask.to_installed_json_hash
        installed_json["artifacts"] = [] if @cask.artifacts_list(uninstall_only: true).empty?
        (metadata_subdir/"#{@cask.token}.json").write JSON.pretty_generate(installed_json)
      end

      FileUtils.rm_r(old_savedir) if old_savedir
    end

    sig { void }
    def save_config_file
      @cask.config_path.atomic_write(@cask.config.to_json)
    end

    sig { void }
    def save_download_sha
      return unless @cask.checksumable?

      @cask.download_sha_path.atomic_write(@cask.new_download_sha)
    end

    sig { params(successor: T.nilable(Cask)).void }
    def uninstall(successor: nil)
      load_installed_caskfile!
      oh1 "Uninstalling Cask #{Formatter.identifier(@cask)}"
      if !reinstall? && !upgrade? && @installed_uninstall_artifacts_missing && artifacts.empty?
        opoo <<~EOS
          No uninstall artifact metadata is available for Cask '#{@cask}'.
          Homebrew will remove its records, but files installed by the Cask may remain.
        EOS
      end
      uninstall_artifacts(clear: true, successor:)
      if !reinstall? && !upgrade?
        remove_tabfile
        remove_download_sha
        remove_config_file
      end
      purge_versioned_files
      purge_caskroom_path if force?
    end

    sig { void }
    def remove_tabfile
      tabfile = @cask.tab.tabfile
      FileUtils.rm_f tabfile if tabfile
      @cask.config_path.parent.rmdir_if_possible
    end

    sig { void }
    def remove_config_file
      FileUtils.rm_f @cask.config_path
      @cask.config_path.parent.rmdir_if_possible
    end

    sig { void }
    def remove_download_sha
      FileUtils.rm_f @cask.download_sha_path
      @cask.download_sha_path.parent.rmdir_if_possible
    end

    sig { params(successor: T.nilable(Cask), quit: T::Boolean).void }
    def start_upgrade(successor:, quit: true)
      uninstall_artifacts(successor:, quit:)
      backup
    end

    sig { void }
    def backup
      bp = backup_path
      raise "unexpected nil backup_path" unless bp

      bmp = backup_metadata_path
      raise "unexpected nil backup_metadata_path" unless bmp

      # The staged files may already be gone (e.g. an out-of-band cask update);
      # skip the rename rather than raising and aborting the upgrade.
      @cask.staged_path.rename bp.to_s if @cask.staged_path.exist?
      @cask.metadata_versioned_path.rename bmp.to_s if @cask.metadata_versioned_path.exist?
    end

    sig { void }
    def restore_backup
      bp = backup_path
      return unless bp

      bmp = backup_metadata_path
      return unless bmp

      return if !bp.directory? || !bmp.directory?

      FileUtils.rm_r(@cask.staged_path) if @cask.staged_path.exist?
      FileUtils.rm_r(@cask.metadata_versioned_path) if @cask.metadata_versioned_path.exist?

      bp.rename @cask.staged_path.to_s
      bmp.rename @cask.metadata_versioned_path.to_s
    end

    sig { params(predecessor: Cask).void }
    def revert_upgrade(predecessor:)
      opoo "Reverting upgrade for Cask #{@cask}"
      restore_backup
      install_artifacts(predecessor:)
    end

    sig { void }
    def finalize_upgrade
      ohai "Purging files for version #{@cask.version} of Cask #{@cask}"

      purge_backed_up_versioned_files

      puts summary
    end

    sig { params(clear: T::Boolean, successor: T.nilable(Cask), quit: T::Boolean).void }
    def uninstall_artifacts(clear: false, successor: nil, quit: true)
      odebug "Uninstalling artifacts"
      odebug "#{::Utils.pluralize("artifact", artifacts.length, include_count: true)} defined", artifacts

      artifacts.each do |artifact|
        if artifact.respond_to?(:uninstall_phase)
          artifact = T.cast(
            artifact,
            T.any(
              Artifact::AbstractFlightBlock,
              Artifact::GeneratedCompletion,
              Artifact::KeyboardLayout,
              Artifact::Moved,
              Artifact::PostflightSteps,
              Artifact::PreflightSteps,
              Artifact::Qlplugin,
              Artifact::Symlinked,
              Artifact::Uninstall,
              Artifact::UninstallPostflightSteps,
              Artifact::UninstallPreflightSteps,
            ),
          )

          odebug "Uninstalling artifact of class #{artifact.class}"
          uninstall_options = {
            command:   @command,
            verbose:   verbose?,
            skip:      clear,
            force:     force?,
            successor:,
            upgrade:   upgrade?,
            reinstall: reinstall?,
          }
          uninstall_options[:quit] = quit if artifact.is_a?(Artifact::Uninstall)
          artifact.uninstall_phase(**uninstall_options)
        end

        next unless artifact.respond_to?(:post_uninstall_phase)

        artifact = T.cast(artifact, Artifact::Uninstall)

        odebug "Post-uninstalling artifact of class #{artifact.class}"
        artifact.post_uninstall_phase(
          command:   @command,
          verbose:   verbose?,
          skip:      clear,
          force:     force?,
          successor:,
        )
      end
    end

    sig { void }
    def zap
      load_installed_caskfile!
      uninstall_artifacts
      if (zap_stanzas = @cask.artifacts.grep(Artifact::Zap)).empty?
        opoo "No zap stanza present for Cask '#{@cask}'"
      else
        ohai "Dispatching zap stanza"
        zap_stanzas.each do |stanza|
          stanza.zap_phase(command: @command, verbose: verbose?, force: force?)
        end
      end
      ohai "Removing all staged versions of Cask '#{@cask}'"
      purge_caskroom_path
    end

    sig { returns(T.nilable(Pathname)) }
    def backup_path
      return if @cask.staged_path.nil?

      Pathname("#{@cask.staged_path}.upgrading")
    end

    sig { returns(T.nilable(Pathname)) }
    def backup_metadata_path
      return if @cask.metadata_versioned_path.nil?

      Pathname("#{@cask.metadata_versioned_path}.upgrading")
    end

    sig { params(path: Pathname).void }
    def gain_permissions_remove(path)
      Utils.gain_permissions_remove(path, command: @command)
    end

    sig { void }
    def purge_backed_up_versioned_files
      # versioned staged distribution
      gain_permissions_remove(T.must(backup_path)) if backup_path&.exist?

      # Cask metadata
      bmp = backup_metadata_path
      return unless bmp&.directory?

      bmp.children.each do |subdir|
        gain_permissions_remove(subdir)
      end
      bmp.rmdir_if_possible
    end

    sig { void }
    def purge_versioned_files
      ohai "Purging files for version #{@cask.version} of Cask #{@cask}"

      # versioned staged distribution
      gain_permissions_remove(@cask.staged_path) if @cask.staged_path&.exist?

      # Cask metadata
      if @cask.metadata_versioned_path.directory?
        @cask.metadata_versioned_path.children.each do |subdir|
          gain_permissions_remove(subdir)
        end

        @cask.metadata_versioned_path.rmdir_if_possible
      end
      @cask.metadata_main_container_path.rmdir_if_possible unless upgrade?

      # toplevel staged distribution
      @cask.caskroom_path.rmdir_if_possible unless upgrade?

      remove_broken_caskroom_symlinks
    end

    sig { void }
    def purge_caskroom_path
      odebug "Purging all staged versions of Cask #{@cask}"
      gain_permissions_remove(@cask.caskroom_path)
      remove_broken_caskroom_symlinks
    end

    sig { params(cask_only: T::Boolean).void }
    def forbidden_tap_check(cask_only: false)
      return if Tap.allowed_taps.blank? && Tap.forbidden_taps.blank?

      owner = Homebrew::EnvConfig.forbidden_owner
      owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence)
        "\n#{contact}"
      end

      # Check the cask itself before its dependencies, since dependency resolution
      # for casks triggers a download via `primary_container`.
      cask_tap = @cask.tap
      if cask_tap.present? && (!cask_tap.allowed_by_env? || cask_tap.forbidden_by_env?)
        cask_error_message = "The installation of #{@cask.full_name} has the tap #{cask_tap}\n" \
                             "but #{owner} "
        cask_error_message << "has not allowed this tap in `$HOMEBREW_ALLOWED_TAPS`" unless cask_tap.allowed_by_env?
        cask_error_message << " and\n" if !cask_tap.allowed_by_env? && cask_tap.forbidden_by_env?
        cask_error_message << "has forbidden this tap in `$HOMEBREW_FORBIDDEN_TAPS`" if cask_tap.forbidden_by_env?
        cask_error_message << ".#{owner_contact}"

        raise CaskCannotBeInstalledError.new(@cask, cask_error_message)
      end

      return if cask_only
      return if skip_cask_deps?

      cask_and_formula_dependencies.each do |cask_or_formula|
        dep_tap = cask_or_formula.tap
        next if dep_tap.blank? || (dep_tap.allowed_by_env? && !dep_tap.forbidden_by_env?)

        dep_full_name = cask_or_formula.full_name
        error_message = "The installation of #{@cask} has a dependency #{dep_full_name}\n" \
                        "from the #{dep_tap} tap but #{owner} "
        error_message << "has not allowed this tap in `$HOMEBREW_ALLOWED_TAPS`" unless dep_tap.allowed_by_env?
        error_message << " and\n" if !dep_tap.allowed_by_env? && dep_tap.forbidden_by_env?
        error_message << "has forbidden this tap in `$HOMEBREW_FORBIDDEN_TAPS`" if dep_tap.forbidden_by_env?
        error_message << ".#{owner_contact}"

        raise CaskCannotBeInstalledError.new(@cask, error_message)
      end
    end

    sig { params(cask_only: T::Boolean).void }
    def forbidden_cask_and_formula_check(cask_only: false)
      forbid_casks = Homebrew::EnvConfig.forbid_casks?
      forbidden_formulae = Set.new(Homebrew::EnvConfig.forbidden_formulae.to_s.split)
      forbidden_casks = Set.new(Homebrew::EnvConfig.forbidden_casks.to_s.split)
      return if !forbid_casks && forbidden_formulae.blank? && forbidden_casks.blank?

      owner = Homebrew::EnvConfig.forbidden_owner
      owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence)
        "\n#{contact}"
      end

      cask_variable = if forbid_casks
        "HOMEBREW_FORBID_CASKS"
      elsif forbidden_casks.include?(@cask.token) || forbidden_casks.include?(@cask.full_name)
        "HOMEBREW_FORBIDDEN_CASKS"
      end

      if cask_variable
        raise CaskCannotBeInstalledError.new(@cask, <<~EOS
          forbidden for installation by #{owner} in `#{cask_variable}`.#{owner_contact}
        EOS
        )
      end

      return if cask_only
      return if skip_cask_deps?

      cask_and_formula_dependencies.each do |dep_cask_or_formula|
        dep_name, dep_type, variable = if dep_cask_or_formula.is_a?(Cask) && forbidden_casks.present?
          dep_cask = dep_cask_or_formula
          env_variable = "HOMEBREW_FORBIDDEN_CASKS"
          dep_cask_name = if forbidden_casks.include?(dep_cask.token)
            dep_cask.token
          elsif forbidden_casks.include?(dep_cask.full_name)
            dep_cask.full_name
          end
          [dep_cask_name, "cask", env_variable]
        elsif dep_cask_or_formula.is_a?(Formula) && forbidden_formulae.present?
          dep_formula = dep_cask_or_formula
          formula_name = if forbidden_formulae.include?(dep_formula.name)
            dep_formula.name
          elsif dep_formula.tap.present? &&
                forbidden_formulae.include?(dep_formula.full_name)
            dep_formula.full_name
          end
          [formula_name, "formula", "HOMEBREW_FORBIDDEN_FORMULAE"]
        end
        next if dep_name.blank?

        raise CaskCannotBeInstalledError.new(@cask, <<~EOS
          has a dependency #{dep_name} but the
          #{dep_name} #{dep_type} was forbidden for installation by #{owner} in `#{variable}`.#{owner_contact}
        EOS
        )
      end
    end

    sig { void }
    def forbidden_cask_artifacts_check
      forbidden_artifacts = Set.new(Homebrew::EnvConfig.forbidden_cask_artifacts.to_s.split)
      return if forbidden_artifacts.blank?

      owner = Homebrew::EnvConfig.forbidden_owner
      owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence)
        "\n#{contact}"
      end

      artifacts.each do |artifact|
        # Get the artifact class name (e.g., "Pkg", "Installer", "App")
        artifact_name = artifact.class.name
        next if artifact_name.nil?

        artifact_type = artifact_name.split("::").last&.downcase
        next if artifact_type.nil?

        next unless forbidden_artifacts.include?(artifact_type)

        raise CaskCannotBeInstalledError.new(@cask, <<~EOS
          contains a '#{artifact_type}' artifact, which is forbidden for installation by #{owner} in `HOMEBREW_FORBIDDEN_CASK_ARTIFACTS`.#{owner_contact}
        EOS
        )
      end
    end

    sig { void }
    def prelude
      return if @ran_prelude

      check_prelude_requirements unless @ran_prelude_fetch
      load_cask_from_source_api! if cask_from_source_api?
      forbidden_tap_check
      forbidden_cask_and_formula_check
      forbidden_cask_artifacts_check

      @ran_prelude = true
    end

    sig { returns(T::Boolean) }
    def source_download_requires_pre_fetch?
      cask_from_source_api? && @cask.languages.any?
    end

    sig { params(download_queue: Homebrew::DownloadQueue).void }
    def prelude_fetch(download_queue: @download_queue)
      return unless (download = prelude_fetch_download)

      download_queue.enqueue(download)
    end

    sig { returns(T.nilable(Homebrew::API::SourceDownload)) }
    def prelude_fetch_download
      return if @ran_prelude_fetch

      check_prelude_requirements
      @ran_prelude_fetch = true
      return unless source_download_requires_pre_fetch?

      if source_download.downloaded?
        source_download.verify_download_integrity(source_download.cached_download)
        source_download.downloader.create_symlink_to_cached_download(source_download.cached_download)
        return
      end

      source_download
    end

    sig { void }
    def enqueue_downloads
      download_queue = @download_queue
      prelude_fetch(download_queue:) unless @ran_prelude_fetch

      if source_download_requires_pre_fetch?
        load_cask_from_source_api!
      elsif cask_from_source_api?
        Homebrew::API::Cask.source_download(@cask, download_queue:, enqueue: true)
      end

      forbidden_tap_check
      forbidden_cask_and_formula_check
      forbidden_cask_artifacts_check

      download_queue.enqueue(downloader)
    end

    # load the same cask file that was used for installation, if possible
    sig { void }
    def load_installed_caskfile!
      Migrator.migrate_if_needed(@cask)

      installed_caskfile = @cask.installed_caskfile
      @installed_uninstall_artifacts_missing = installed_caskfile.is_a?(Pathname) &&
                                               installed_uninstall_artifacts_missing?(installed_caskfile)

      if installed_caskfile&.exist?
        tab = CaskLoader.load_installed_tab(@cask)
        tap = tab.tap
        tap ||= @cask.tap
        if installed_caskfile.extname == ".rb" &&
           Homebrew::EnvConfig.require_tap_trust? &&
           tap &&
           !Homebrew::Trust.trusted?(:cask, "#{tap.name}/#{@cask.token}")
          opoo "Skipping loading untrusted Cask #{tap.name}/#{@cask.token}; uninstalling recorded artifacts only."

          dsl = DSL.new(@cask)
          default_uninstall_artifact_keys = DSL::ACTIVATABLE_ARTIFACT_CLASSES.filter_map do |klass|
            next if [Artifact::Uninstall, Artifact::Zap].include?(klass)
            next if !klass.method_defined?(:uninstall_phase) && !klass.method_defined?(:post_uninstall_phase)

            klass.dsl_key
          end.to_set
          Array(tab.uninstall_artifacts).each do |artifact_entry|
            next unless artifact_entry.is_a?(Hash)

            artifact_entry.each do |raw_key, raw_args|
              dsl_key = raw_key.to_sym
              next unless default_uninstall_artifact_keys.include?(dsl_key)

              args = Array(raw_args)
              if args.last.is_a?(Hash)
                dsl.public_send(
                  dsl_key,
                  *args[...-1],
                  **T.cast(args.last, T::Hash[T.any(Symbol, String), T.anything]).transform_keys(&:to_sym),
                )
              else
                dsl.public_send(dsl_key, *args)
              end
            end
          end
          @default_uninstall_artifacts ||= dsl.artifacts
          return
        end

        begin
          @cask = CaskLoader.load_from_installed_caskfile(installed_caskfile)
          return
        rescue CaskInvalidError, CaskUnavailableError, MethodDeprecatedError
          # could be caused by trying to load outdated or deleted caskfile
        end

        recovered_cask = CaskLoader.recover_from_installed_caskfile(installed_caskfile, tab:, fallback_cask: @cask)
        if recovered_cask
          @cask = recovered_cask
          return
        end
      end

      load_cask_from_source_api! if cask_from_source_api?
      # otherwise we default to the current cask
    end

    private

    # Remove Caskroom symlinks (e.g. from cask renames) that removing this cask's
    # directory has broken, whatever the symlink is named.
    sig { void }
    def remove_broken_caskroom_symlinks
      return unless Caskroom.path.directory?

      Caskroom.path.children.each do |link|
        next if !link.symlink? || link.exist?
        next if link.readlink.basename != @cask.caskroom_path.basename

        FileUtils.rm link
      end
    end

    sig { params(installed_caskfile: Pathname).returns(T::Boolean) }
    def installed_uninstall_artifacts_missing?(installed_caskfile)
      return false unless CaskLoader.installed_json_caskfile?(installed_caskfile)

      installed_json = CaskLoader.load_installed_json(installed_caskfile)
      return false if installed_json.nil? || installed_json.key?("artifacts")

      CaskLoader.load_installed_tab(@cask).uninstall_artifacts.blank?
    end

    sig { void }
    def check_prelude_requirements
      check_deprecate_disable
      check_conflicts
      check_requirements
      # Run the cask-self forbidden checks before loading the caskfile from the
      # Source API so a forbidden cask never triggers a network fetch.
      forbidden_tap_check(cask_only: true)
      forbidden_cask_and_formula_check(cask_only: true)
    end

    sig { returns(Homebrew::API::SourceDownload) }
    def source_download
      @source_download ||= Homebrew::API::Cask.source_download_for(@cask)
    end

    sig { void }
    def load_cask_from_source_api!
      @cask = Homebrew::API::Cask.source_download_cask(@cask)
    end

    sig { returns(T::Boolean) }
    def cask_from_source_api?
      @cask.loaded_from_api? && @cask.caskfile_only?
    end
  end
end

require "extend/os/cask/installer"
