# frozen_string_literal: true

require 'json'
require 'bolt/error'
require 'bolt/result'

module Bolt
  class ApplyResult < Result
    def self.puppet_missing_error(result)
      error_hash = result.error_hash
      exit_code = error_hash['details']['exit_code'] if error_hash && error_hash['details']
      # If we get exit code 126 or 127 back, it means the shebang command wasn't found; Puppet isn't present
      if [126, 127].include?(exit_code)
        {
          'msg' => "Puppet is not installed on the target, please install it to enable 'apply'",
          'kind' => 'bolt/apply-error'
        }
      elsif exit_code == 1 &&
            (error_hash['msg'] =~ /Could not find executable 'ruby.exe'/ ||
             error_hash['msg'] =~ /The term 'ruby.exe' is not recognized as the name of a cmdlet/)
        # Windows does not have Ruby present
        {
          'msg' => "Puppet is not installed on the target in $env:ProgramFiles, please install it to enable 'apply'",
          'kind' => 'bolt/apply-error'
        }
      elsif exit_code == 1 && error_hash['msg'] =~ /cannot load such file -- puppet \(LoadError\)/
        # Windows uses a Ruby that doesn't have Puppet installed
        # TODO: fix so we don't find other Rubies, or point to a known issues URL for more info
        { 'msg' => 'Found a Ruby without Puppet present, please install Puppet ' \
                   "or remove Ruby from $env:Path to enable 'apply'",
          'kind' => 'bolt/apply-error' }
      end
    end

    def self.resource_error(result)
      if result.value['status'] == 'failed'
        resources = result.value['resource_statuses']
        failed = resources.select { |_, r| r['failed'] }.flat_map do |key, resource|
          resource['events'].select { |e| e['status'] == 'failure' }.map do |event|
            "\n  #{key}: #{event['message']}"
          end
        end

        { 'msg' => "Resources failed to apply for #{result.target.name}#{failed.join}",
          'kind' => 'bolt/resource-failure' }
      end
    end

    def self.invalid_report_error(result)
      # These are the keys ApplyResult methods rely on.
      expected_report_keys = %w[metrics resource_statuses status]
      missing_keys = expected_report_keys.reject { |k| result.value.include?(k) }

      unless missing_keys.empty?
        if result['_output']
          # rubocop:disable Metrics/LineLength
          msg = "Report result contains an '_output' key. Catalog application may have printed extraneous output to stdout: #{result['_output']}"
          # rubocop:enable Metrics/LineLength
        else
          msg = "Report did not contain all expected keys missing: #{missing_keys.join(' ,')}"
        end

        { 'msg' => msg,
          'kind' => 'bolt/invalid-report' }
      end
    end

    def self.from_task_result(result)
      if (puppet_missing = puppet_missing_error(result))
        new(result.target,
            error: puppet_missing,
            report: result.value.reject { |k| k == '_error' })
      elsif !result.ok?
        new(result.target, error: result.error_hash)
      elsif (invalid_report = invalid_report_error(result))
        new(result.target,
            error: invalid_report,
            report: result.value.reject { |k| %w[_error _output].include?(k) })
      elsif (resource_error = resource_error(result))
        new(result.target,
            error: resource_error,
            report: result.value.reject { |k| k == '_error' })
      else
        new(result.target, report: result.value)
      end
    end

    def initialize(target, error: nil, report: nil)
      @target = target
      @value = {}
      @action = 'apply'
      value['report'] = report if report
      value['_error'] = error if error
      value['_output'] = metrics_message if metrics_message
    end

    def event_metrics
      if (events = value.dig('report', 'metrics', 'resources', 'values'))
        events.each_with_object({}) { |ev, h| h[ev[0]] = ev[2] }
      end
    end

    def logs
      value.dig('report', 'logs') || []
    end

    # Return only log messages associated with resources
    def resource_logs
      logs.reject { |log| log['source'] == 'Puppet' }
    end

    def metrics_message
      if (metrics = event_metrics)
        changed = metrics['changed']
        failed = metrics['failed']
        skipped = metrics['skipped']
        unchanged = metrics['total'] - changed - failed - skipped
        noop = metrics['out_of_sync'] - changed - failed
        "changed: #{changed}, failed: #{failed}, unchanged: #{unchanged} skipped: #{skipped}, noop: #{noop}"
      end
    end

    def report
      @value['report']
    end
  end
end