# frozen_string_literal: true

require 'bolt/task'

# Installs the puppet-agent package on targets if needed then collects facts, including any custom
# facts found in Bolt's modulepath.
#
# Agent detection will be skipped if the target includes the 'puppet-agent' feature, either as a
# property of its transport (PCP) or by explicitly setting it as a feature in Bolt's inventory.
#
# If no agent is detected on the target using the 'puppet_agent::version' task, it's installed
# using 'puppet_agent::install' and the puppet service is stopped/disabled using the 'service' task.
#
# **NOTE:** Not available in apply block
Puppet::Functions.create_function(:apply_prep) do
  # @param targets A pattern or array of patterns identifying a set of targets.
  # @example Prepare targets by name.
  #   apply_prep('target1,target2')
  dispatch :apply_prep do
    param 'Boltlib::TargetSpec', :targets
  end

  def script_compiler
    @script_compiler ||= Puppet::Pal::ScriptCompiler.new(closure_scope.compiler)
  end

  def run_task(executor, targets, name, args = {})
    tasksig = script_compiler.task_signature(name)
    raise Bolt::Error.new("#{name} could not be found", 'bolt/apply-prep') unless tasksig

    task = Bolt::Task.new(tasksig.task_hash)
    results = executor.run_task(targets, task, args)
    raise Bolt::RunFailure.new(results, 'run_task', task.name) unless results.ok?
    results
  end

  # Returns true if the target has the puppet-agent feature defined, either from inventory or transport.
  def agent?(target, executor, inventory)
    inventory.features(target).include?('puppet-agent') ||
      executor.transport(target.transport).provided_features.include?('puppet-agent') || target.remote?
  end

  def apply_prep(target_spec)
    unless Puppet[:tasks]
      raise Puppet::ParseErrorWithIssue
        .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep')
    end

    applicator = Puppet.lookup(:apply_executor)
    executor = Puppet.lookup(:bolt_executor)
    inventory = Puppet.lookup(:bolt_inventory)

    executor.report_function_call(self.class.name)

    targets = inventory.get_targets(target_spec)

    executor.log_action('install puppet and gather facts', targets) do
      executor.without_default_logging do
        # Skip targets that include the puppet-agent feature, as we know an agent will be available.
        agent_targets, unknown_targets = targets.partition { |target| agent?(target, executor, inventory) }
        agent_targets.each { |target| Puppet.debug "Puppet Agent feature declared for #{target.name}" }
        unless unknown_targets.empty?
          # Ensure Puppet is installed
          versions = run_task(executor, unknown_targets, 'puppet_agent::version')
          need_install, installed = versions.partition { |r| r['version'].nil? }
          installed.each do |r|
            Puppet.debug "Puppet Agent #{r['version']} installed on #{r.target.name}"
            inventory.set_feature(r.target, 'puppet-agent')
          end

          unless need_install.empty?
            need_install_targets = need_install.map(&:target)
            run_task(executor, need_install_targets, 'puppet_agent::install')
            # Service task works best when targets have puppet-agent feature
            need_install_targets.each { |target| inventory.set_feature(target, 'puppet-agent') }
            # Ensure the Puppet service is stopped after new install
            run_task(executor, need_install_targets, 'service', 'action' => 'stop', 'name' => 'puppet')
            run_task(executor, need_install_targets, 'service', 'action' => 'disable', 'name' => 'puppet')
          end
        end

        # Gather facts, including custom facts
        plugins = applicator.build_plugin_tarball do |mod|
          search_dirs = []
          search_dirs << mod.plugins if mod.plugins?
          search_dirs << mod.pluginfacts if mod.pluginfacts?
          search_dirs
        end

        task = applicator.custom_facts_task
        arguments = { 'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins) }
        results = executor.run_task(targets, task, arguments)
        raise Bolt::RunFailure.new(results, 'run_task', task.name) unless results.ok?

        results.each do |result|
          inventory.add_facts(result.target, result.value)
        end
      end
    end

    # Return nothing
    nil
  end
end