#
# Copyright:: Copyright (c) Chef Software Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "../resource"
require "chef-utils/dist" unless defined?(ChefUtils::Dist)

class Chef
  class Resource
    class ChefClientScheduledTask < Chef::Resource

      provides :chef_client_scheduled_task

      description "Use the **chef_client_scheduled_task** resource to setup the #{ChefUtils::Dist::Infra::PRODUCT} to run as a Windows scheduled task. This resource will also create the specified log directory if it doesn't already exist."
      introduced "16.0"
      examples <<~DOC
      **Setup #{ChefUtils::Dist::Infra::PRODUCT} to run using the default 30 minute cadence**:

      ```ruby
      chef_client_scheduled_task 'Run #{ChefUtils::Dist::Infra::PRODUCT} as a scheduled task'
      ```

      **Run #{ChefUtils::Dist::Infra::PRODUCT} on system start**:

      ```ruby
      chef_client_scheduled_task '#{ChefUtils::Dist::Infra::PRODUCT} on start' do
        frequency 'onstart'
      end
      ```

      **Run #{ChefUtils::Dist::Infra::PRODUCT} with extra options passed to the client**:

      ```ruby
      chef_client_scheduled_task 'Run an override recipe' do
        daemon_options ['--override-runlist mycorp_base::default']
      end
      ```

      **Run #{ChefUtils::Dist::Infra::PRODUCT} daily at 01:00 am, specifying a named run-list**:

      ```ruby
      chef_client_scheduled_task 'Run chef-client named run-list daily' do
        frequency 'daily'
        start_time '01:00'
        daemon_options ['-n audit_only']
      end
      ```

      **Run #{ChefUtils::Dist::Infra::PRODUCT} with a persistent delay on every run calculated once, similar to how chef_client_cron resource works**:

      ```ruby
      chef_client_scheduled_task 'Run chef-client with persistent splay' do
        use_consistent_splay true
      end
      ```
      DOC

      resource_name :chef_client_scheduled_task

      property :task_name, String,
        description: "The name of the scheduled task to create.",
        default: ChefUtils::Dist::Infra::CLIENT

      property :user, String,
        description: "The name of the user that #{ChefUtils::Dist::Infra::PRODUCT} runs as.",
        default: "System", sensitive: true

      property :password, String,
        description: "The password for the user that #{ChefUtils::Dist::Infra::PRODUCT} runs as.",
        sensitive: true

      property :frequency, String,
        description: "Frequency with which to run the task.",
        default: "minute",
        equal_to: %w{minute hourly daily monthly once on_logon onstart on_idle}

      property :frequency_modifier, [Integer, String],
        coerce: proc { |x| Integer(x) },
        callbacks: { "should be a positive number" => proc { |v| v > 0 } },
        description: "Numeric value to go with the scheduled task frequency",
        default: lazy { frequency == "minute" ? 30 : 1 },
        default_description: "30 if frequency is 'minute', 1 otherwise"

      property :accept_chef_license, [true, false],
        description: "Accept the Chef Online Master License and Services Agreement. See <https://www.chef.io/online-master-agreement>",
        default: false

      property :start_date, String,
        description: "The start date for the task in m:d:Y format (ex: 12/17/2020).",
        regex: [%r{^[0-1][0-9]\/[0-3][0-9]\/\d{4}$}]

      property :start_time, String,
        description: "The start time for the task in HH:mm format (ex: 14:00). If the frequency is minute default start time will be Time.now plus the frequency_modifier number of minutes.",
        regex: [/^\d{2}:\d{2}$/]

      property :splay, [Integer, String],
        coerce: proc { |x| Integer(x) },
        callbacks: { "should be a positive number" => proc { |v| v >= 0 } },
        description: "A random number of seconds between 0 and X to add to interval so that all #{ChefUtils::Dist::Infra::CLIENT} commands don't execute at the same time.",
        default: 300

      property :use_consistent_splay, [true, false],
        description: "Always use the same random splay amount for each node to ensure consistent frequencies between #{ChefUtils::Dist::Infra::CLIENT} execution.",
        introduced: "17.5",
        default: false

      property :run_on_battery, [true, false],
        description: "Run the #{ChefUtils::Dist::Infra::PRODUCT} task when the system is on batteries.",
        default: true

      property :config_directory, String,
        description: "The path of the config directory.",
        default: ChefConfig::Config.etc_chef_dir,
        default_description: ChefConfig::Config.c_chef_dir

      property :log_directory, String,
        description: "The path of the directory to create the log file in.",
        default: lazy { |r| "#{r.config_directory}/log" },
        default_description: "CONFIG_DIRECTORY/log"

      property :log_file_name, String,
        description: "The name of the log file to use.",
        default: "client.log"

      property :chef_binary_path, String,
        description: "The path to the #{ChefUtils::Dist::Infra::CLIENT} binary.",
        default: "C:/#{ChefUtils::Dist::Org::LEGACY_CONF_DIR}/#{ChefUtils::Dist::Infra::DIR_SUFFIX}/bin/#{ChefUtils::Dist::Infra::CLIENT}"

      property :daemon_options, Array,
        description: "An array of options to pass to the #{ChefUtils::Dist::Infra::CLIENT} command.",
        default: []

      property :priority, Integer,
        description: "Use to set Priority Levels range from 0 to 10.",
        introduced: "17.5",
        default: 7, callbacks: { "should be in range of 0 to 10" => proc { |v| v.between?(0, 10) } }

      action :add, description: "Add a Windows Scheduled Task that runs #{ChefUtils::Dist::Infra::PRODUCT}." do
        # TODO: Replace this with a :create_if_missing action on directory when that exists
        unless Dir.exist?(new_resource.log_directory)
          directory new_resource.log_directory do
            inherits true
            recursive true
            action :create
          end
        end

        # According to https://docs.microsoft.com/en-us/windows/desktop/taskschd/schtasks,
        # the :once, :onstart, :onlogon, and :onidle schedules don't accept schedule modifiers

        windows_task new_resource.task_name do
          run_level                      :highest
          command                        full_command
          user                           new_resource.user
          password                       new_resource.password
          frequency                      new_resource.frequency.to_sym
          frequency_modifier             new_resource.frequency_modifier if frequency_supports_frequency_modifier?
          start_time                     new_resource.start_time
          start_day                      new_resource.start_date unless new_resource.start_date.nil?
          random_delay                   new_resource.splay if frequency_supports_random_delay? && !new_resource.use_consistent_splay
          disallow_start_if_on_batteries new_resource.splay unless new_resource.run_on_battery
          priority                       new_resource.priority
          action                         %i{create enable}
        end
      end

      action :remove, description: "Remove a Windows Scheduled Task that runs #{ChefUtils::Dist::Infra::PRODUCT}." do
        windows_task new_resource.task_name do
          action :delete
        end
      end

      action_class do
        #
        # The full command to run in the scheduled task
        #
        # @return [String]
        #
        def full_command
          # Fetch path of cmd.exe through environment variable comspec
          cmd_path = ENV["COMSPEC"]

          "#{cmd_path} /c \"#{consistent_splay_command}#{client_cmd}\""
        end

        #
        # Generate a uniformly distributed unique number to sleep from 0 to the splay time
        #
        # @param [Integer] splay The number of seconds to splay
        #
        # @return [Integer]
        #
        def splay_sleep_time(splay)
          seed = node["shard_seed"] || Digest::MD5.hexdigest(node.name).to_s.hex
          random = Random.new(seed.to_i)
          random.rand(splay)
        end

        #
        # The consistent splay sleep time when use_consistent_splay is true.
        #
        # @return [NilClass,String] The prepended sleep command to run prior to executing the full command.
        #
        def consistent_splay_command
          return unless new_resource.use_consistent_splay

          "C:/windows/system32/windowspowershell/v1.0/powershell.exe Start-Sleep -s #{splay_sleep_time(new_resource.splay)} && "
        end

        #
        # Build command line to pass to cmd.exe
        #
        # @return [String]
        #
        def client_cmd
          cmd = new_resource.chef_binary_path.dup
          cmd << " -L #{::File.join(new_resource.log_directory, new_resource.log_file_name)}"
          cmd << " -c #{::File.join(new_resource.config_directory, "client.rb")}"

          # Add custom options
          cmd << " #{new_resource.daemon_options.join(" ")}" if new_resource.daemon_options.any?
          cmd << " --chef-license accept" if new_resource.accept_chef_license
          cmd
        end

        #
        # not all frequencies in the windows_task resource support random_delay
        #
        # @return [boolean]
        #
        def frequency_supports_random_delay?
          %w{once minute hourly daily weekly monthly}.include?(new_resource.frequency)
        end

        #
        # not all frequencies in the windows_task resource support frequency_modifier
        #
        # @return [boolean]
        #
        def frequency_supports_frequency_modifier?
          # these are the only ones that don't
          !%w{once on_logon onstart on_idle}.include?(new_resource.frequency)
        end
      end
    end
  end
end
