#
# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# 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 "chef-utils" unless defined?(ChefUtils::CANARY)
require_relative "../resource"
require_relative "../win32/security" if ChefUtils.windows_ruby?
autoload :ISO8601, "iso8601" if ChefUtils.windows_ruby?
require_relative "../util/path_helper"
require_relative "../util/backup"
require "win32/taskscheduler" if ChefUtils.windows_ruby?

class Chef
  class Resource
    class WindowsTask < Chef::Resource

      provides(:windows_task) { true }

      description "Use the **windows_task** resource to create, delete or run a Windows scheduled task."
      introduced "13.0"
      examples <<~DOC
      **Create a scheduled task to run every 15 minutes as the Administrator user**:

      ```ruby
      windows_task 'chef-client' do
        user 'Administrator'
        password 'password'
        command 'chef-client'
        run_level :highest
        frequency :minute
        frequency_modifier 15
      end
      ```

      **Create a scheduled task to run every 2 days**:

      ```ruby
      windows_task 'chef-client' do
        command 'chef-client'
        run_level :highest
        frequency :daily
        frequency_modifier 2
      end
      ```

      **Create a scheduled task to run on specific days of the week**:

      ```ruby
      windows_task 'chef-client' do
        command 'chef-client'
        run_level :highest
        frequency :weekly
        day 'Mon, Thu'
      end
      ```

      **Create a scheduled task to run only once**:

      ```ruby
      windows_task 'chef-client' do
        command 'chef-client'
        run_level :highest
        frequency :once
        start_time '16:10'
      end
      ```

      **Create a scheduled task to run on current day every 3 weeks and delay upto 1 min**:

      ```ruby
      windows_task 'chef-client' do
        command 'chef-client'
        run_level :highest
        frequency :weekly
        frequency_modifier 3
        random_delay '60'
      end
      ```

      **Create a scheduled task to run weekly starting on Dec 28th 2018**:

      ```ruby
      windows_task 'chef-client 8' do
        command 'chef-client'
        run_level :highest
        frequency :weekly
        start_day '12/28/2018'
      end
      ```

      **Create a scheduled task to run every Monday, Friday every 2 weeks**:

      ```ruby
      windows_task 'chef-client' do
        command 'chef-client'
        run_level :highest
        frequency :weekly
        frequency_modifier 2
        day 'Mon, Fri'
      end
      ```

      **Create a scheduled task to run when computer is idle with idle duration 20 min**:

      ```ruby
      windows_task 'chef-client' do
        command 'chef-client'
        run_level :highest
        frequency :on_idle
        idle_time 20
      end
      ```

      **Delete a task named "old task"**:
      ```ruby
      windows_task 'old task' do
        action :delete
      end
      ```

      **Enable a task named "chef-client"**:
      ```ruby
      windows_task 'chef-client' do
        action :enable
      end
      ```

      **Disable a task named "ProgramDataUpdater" with TaskPath "\\Microsoft\\Windows\\Application Experience\\ProgramDataUpdater"**
      ```ruby
      windows_task '\\Microsoft\\Windows\\Application Experience\\ProgramDataUpdater' do
        action :disable
      end
      ```
      DOC

      allowed_actions :create, :delete, :run, :end, :enable, :disable, :change

      property :task_name, String, regex: [%r{\A[^/\:\*\?\<\>\|]+\z}],
        description: "An optional property to set the task name if it differs from the resource block's name. Example: `Task Name` or `/Task Name`",
        name_property: true

      property :command, String,
        description: "The command to be executed by the windows scheduled task."

      property :cwd, String,
        description: "The directory the task will be run from."

      property :user, String,
        description: "The user to run the task as.",
        default: lazy { Chef::ReservedNames::Win32::Security::SID.LocalSystem.account_simple_name if ChefUtils.windows_ruby? },
        default_description: "The localized SYSTEM user for the node."

      property :password, String,
        description: "The user's password. The user property must be set if using this property."

      property :run_level, Symbol, equal_to: %i{highest limited},
        description: "Run with `:limited` or `:highest` privileges.",
        default: :limited

      property :force, [TrueClass, FalseClass],
        description: "When used with create, will update the task.",
        default: false

      property :interactive_enabled, [TrueClass, FalseClass],
        description: "Allow task to run interactively or non-interactively. Requires user and password to also be set.",
        default: false

      property :frequency_modifier, [Integer, String],
        default: 1,
        description: <<~DOCS
          * For frequency `:minute` valid values are 1 to 1439
          * For frequency `:hourly` valid values are 1 to 23
          * For frequency `:daily` valid values are 1 to 365
          * For frequency `:weekly` valid values are 1 to 52
          * For frequency `:monthly` valid values are `('FIRST', 'SECOND', 'THIRD', 'FOURTH', 'LAST')` OR `1-12`.
            * e.g. If user want to run the task on `second week of the month` use `frequency_modifier` value as `SECOND`. Multiple values for weeks of the month should be comma separated e.g. `"FIRST, THIRD, LAST"`.
            * To run task every (n) months use values 1 to 12.
        DOCS

      property :frequency, Symbol, equal_to: %i{minute hourly daily weekly monthly once on_logon onstart on_idle none},
        description: "The frequency with which to run the task. Note: This property is required in Chef Infra Client 14.1 or later. Note: The `:once` value requires the `start_time` property to be set."

      property :start_day, String,
        description: "Specifies the first date on which the task runs in **MM/DD/YYYY** format.",
        default_description: "The current date."

      property :start_time, String,
        description: "Specifies the start time to run the task, in **HH:mm** format."

      property :day, [String, Integer],
        description:  <<~DOCS
        The day(s) on which the task runs.
          * Use this property when setting `frequency` to `:monthly` or `:weekly`.
          * Valid values with frequency `:weekly` are `MON`-`SUN` or `*`.
          * Valid values with frequency `:monthly` are `1-31`, `MON`-`SUN`, and `LASTDAY`.
          * Use `MON`-`SUN` or `LASTDAY` if you are setting `frequency_modifier` as "FIRST, SECOND, THIRD etc." else use 1-31.
          * Multiple days should be comma separated. e.g `1, 2, 3` or `MON, WED, FRI`.
        DOCS

      property :months, String,
        description: "The Months of the year on which the task runs, such as: `JAN, FEB` or `*`. Multiple months should be comma delimited. e.g. `Jan, Feb, Mar, Dec`."

      property :idle_time, Integer,
        description: "For `:on_idle` frequency, the time (in minutes) without user activity that must pass to trigger the task, from `1` - `999`."

      property :random_delay, [String, Integer],
        description: "Delays the task up to a given time (in seconds)."

      property :execution_time_limit, [String, Integer],
        description: "The maximum time the task will run. This field accepts either seconds or an ISO8601 duration value.",
        default: "PT72H",
        default_description: "PT72H (72 hours in ISO8601 duration format)"

      property :minutes_duration, [String, Integer],
        description: ""

      property :minutes_interval, [String, Integer],
        description: ""

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

      property :disallow_start_if_on_batteries, [TrueClass, FalseClass],
        introduced: "14.4", default: false,
        description: "Disallow start of the task if the system is running on battery power."

      property :stop_if_going_on_batteries, [TrueClass, FalseClass],
        introduced: "14.4", default: false,
        description: "Scheduled task option when system is switching on battery."

      property :description, String,
        introduced: "14.7",
        description: "The task description."

      property :start_when_available, [TrueClass, FalseClass],
        introduced: "14.15", default: false,
        description: "To start the task at any time after its scheduled time has passed."

      property :backup, [Integer, FalseClass],
        introduced: "17.0", default: 5,
        description: "Number of backups to keep of the task when modified/deleted. Set to false to disable backups."

      attr_accessor :exists, :task, :command_arguments

      VALID_WEEK_DAYS = %w{ mon tue wed thu fri sat sun * }.freeze
      VALID_DAYS_OF_MONTH = ("1".."31").to_a << "last" << "lastday"
      VALID_MONTHS = %w{JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC *}.freeze
      VALID_WEEKS = %w{FIRST SECOND THIRD FOURTH LAST LASTDAY}.freeze

      def after_created
        if random_delay
          validate_random_delay(random_delay, frequency)
          random_delay(sec_to_min(random_delay))
        end

        if execution_time_limit
          execution_time_limit(259200) if execution_time_limit == "PT72H"
          raise ArgumentError, "Invalid value passed for `execution_time_limit`. Please pass seconds as an Integer (e.g. 60) or a String with numeric values only (e.g. '60')." unless numeric_value_in_string?(execution_time_limit)

          execution_time_limit(sec_to_min(execution_time_limit))
        end

        validate_frequency(frequency) if action.include?(:create) || action.include?(:change)
        validate_start_time(start_time, frequency)
        validate_start_day(start_day, frequency) if start_day
        validate_user_and_password(user, password)
        validate_create_frequency_modifier(frequency, frequency_modifier) if frequency_modifier
        validate_create_day(day, frequency, frequency_modifier) if day
        validate_create_months(months, frequency) if months
        validate_frequency_monthly(frequency_modifier, months, day) if frequency == :monthly
        validate_idle_time(idle_time, frequency)
        idempotency_warning_for_frequency_weekly(day, start_day) if frequency == :weekly
      end

      private

      ## Resource is not idempotent when day, start_day is not provided with frequency :weekly
      ## we set start_day when not given by user as current date based on which we set the day property for current current date day is monday ..
      ## we set the monday as the day so at next run when  new_resource.day is nil and current_resource day is monday due to which update gets called
      def idempotency_warning_for_frequency_weekly(day, start_day)
        if start_day.nil? && day.nil?
          logger.warn "To maintain idempotency for frequency :weekly provide start_day, start_time and day."
        end
      end

      # Validate the passed value is numeric values only if it is a string
      def numeric_value_in_string?(val)
        true if Integer(val)
      rescue ArgumentError
        false
      end

      def validate_frequency(frequency)
        if frequency.nil? || !(%i{minute hourly daily weekly monthly once on_logon onstart on_idle none}.include?(frequency))
          raise ArgumentError, "Frequency needs to be provided. Valid frequencies are :minute, :hourly, :daily, :weekly, :monthly, :once, :on_logon, :onstart, :on_idle, :none."
        end
      end

      def validate_frequency_monthly(frequency_modifier, months, day)
        # validates the frequency :monthly and raises error if frequency_modifier is first, second, third etc and day is not provided
        if (frequency_modifier != 1) && (frequency_modifier_includes_days_of_weeks?(frequency_modifier)) && !(day)
          raise ArgumentError, "Please select day on which you want to run the task e.g. 'Mon, Tue'. Multiple values must be separated by comma."
        end

        # frequency_modifier 2-12 is used to set every (n) months, so using :months property with frequency_modifier is not valid since they both used to set months.
        # Not checking value 1 here for frequency_modifier since we are setting that as default value it won't break anything since preference is given to months property
        if (frequency_modifier.to_i.between?(2, 12)) && !(months.nil?)
          raise ArgumentError, "For frequency :monthly either use property months or frequency_modifier to set months."
        end
      end

      # returns true if frequency_modifier has values First, second, third, fourth, last, lastday
      def frequency_modifier_includes_days_of_weeks?(frequency_modifier)
        frequency_modifier = frequency_modifier.to_s.split(",")
        frequency_modifier.map! { |value| value.strip.upcase }
        (frequency_modifier - VALID_WEEKS).empty?
      end

      def validate_random_delay(random_delay, frequency)
        if %i{on_logon onstart on_idle none}.include? frequency
          raise ArgumentError, "`random_delay` property is supported only for frequency :once, :minute, :hourly, :daily, :weekly and :monthly"
        end

        raise ArgumentError, "Invalid value passed for `random_delay`. Please pass seconds as an Integer (e.g. 60) or a String with numeric values only (e.g. '60')." unless numeric_value_in_string?(random_delay)
      end

      def validate_start_day(start_day, frequency)
        if start_day && frequency == :none
          raise ArgumentError, "`start_day` property is not supported with frequency: #{frequency}"
        end

        # make sure the start_day is in MM/DD/YYYY format: http://rubular.com/r/cgjHemtWl5
        if start_day
          raise ArgumentError, "`start_day` property must be in the MM/DD/YYYY format." unless %r{^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d$}.match?(start_day)
        end
      end

      def validate_start_time(start_time, frequency)
        if start_time
          raise ArgumentError, "`start_time` property is not supported with `frequency :none`" if frequency == :none
          raise ArgumentError, "`start_time` property must be in the HH:mm format (e.g. 6:20pm -> 18:20)." unless /^[0-2][0-9]:[0-5][0-9]$/.match?(start_time)
        else
          raise ArgumentError, "`start_time` needs to be provided with `frequency :once`" if frequency == :once
        end
      end

      # System users will not require a password
      # Other users will require a password if the task is non-interactive.
      #
      # @param [String] user
      # @param [String] password
      #
      def validate_user_and_password(user, password)
        if non_system_user?(user)
          if password.nil? && !interactive_enabled
            raise ArgumentError, "Please provide a password or check if this task needs to be interactive! Valid passwordless users are: '#{Chef::ReservedNames::Win32::Security::SID::SYSTEM_USER.join("', '")}'"
          end
        else
          unless password.nil?
            raise ArgumentError, "Password is not required for system users."
          end
        end
      end

      # Password is not required for system user and required for non-system user.
      def password_required?(user)
        @password_required ||= (!user.nil? && !Chef::ReservedNames::Win32::Security::SID.system_user?(user))
      end

      alias non_system_user? password_required?

      def validate_create_frequency_modifier(frequency, frequency_modifier)
        if (%i{on_logon onstart on_idle none}.include?(frequency)) && ( frequency_modifier != 1)
          raise ArgumentError, "frequency_modifier property not supported with frequency :#{frequency}"
        end

        if frequency == :monthly
          unless (1..12).cover?(frequency_modifier.to_i) || frequency_modifier_includes_days_of_weeks?(frequency_modifier)
            raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :monthly frequency are 1 - 12, 'FIRST', 'SECOND', 'THIRD', 'FOURTH', 'LAST'."
          end
        else
          unless frequency.nil? || frequency_modifier.nil?
            frequency_modifier = frequency_modifier.to_i
            min = 1
            max = case frequency
                  when :minute
                    1439
                  when :hourly
                    23
                  when :daily
                    365
                  when :weekly
                    52
                  else
                    min
                  end
            unless frequency_modifier.between?(min, max)
              raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :#{frequency} frequency are #{min} - #{max}."
            end
          end
        end
      end

      def validate_create_day(day, frequency, frequency_modifier)
        raise ArgumentError, "day property is only valid for tasks that run monthly or weekly" unless %i{weekly monthly}.include?(frequency)

        # This has been verified with schtask.exe https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks#d-dayday--
        # verified with earlier code if day "*" is given with frequency it raised exception Invalid value for /D option
        raise ArgumentError, "day wild card (*) is only valid with frequency :weekly" if frequency == :monthly && day == "*"

        if day.is_a?(String) && day.to_i.to_s != day
          days = day.split(",")
          if days_includes_days_of_months?(days)
            # Following error will be raise if day is set as 1-31 and frequency is selected as :weekly since those values are valid with only frequency :monthly
            raise ArgumentError, "day values 1-31 or last is only valid with frequency :monthly" if frequency == :weekly
          else
            days.map! { |day| day.to_s.strip.downcase }
            unless (days - VALID_WEEK_DAYS).empty?
              raise ArgumentError, "day property invalid. Only valid values are: #{VALID_WEEK_DAYS.map(&:upcase).join(", ")}. Multiple values must be separated by a comma."
            end
          end
        end
      end

      def validate_create_months(months, frequency)
        raise ArgumentError, "months property is only valid for tasks that run monthly" if frequency != :monthly

        if months.is_a?(String)
          months = months.split(",")
          months.map! { |month| month.strip.upcase }
          unless (months - VALID_MONTHS).empty?
            raise ArgumentError, "months property invalid. Only valid values are: #{VALID_MONTHS.join(", ")}. Multiple values must be separated by a comma."
          end
        end
      end

      # This method returns true if day has values from 1-31 which is a days of moths and used with frequency :monthly
      def days_includes_days_of_months?(days)
        days.map! { |day| day.to_s.strip.downcase }
        (days - VALID_DAYS_OF_MONTH).empty?
      end

      def validate_idle_time(idle_time, frequency)
        if !idle_time.nil? && frequency != :on_idle
          raise ArgumentError, "idle_time property is only valid for tasks that run on_idle"
        end
        if idle_time.nil? && frequency == :on_idle
          raise ArgumentError, "idle_time value should be set for :on_idle frequency."
        end
        unless idle_time.nil? || idle_time > 0 && idle_time <= 999
          raise ArgumentError, "idle_time value #{idle_time} is invalid. Valid values for :on_idle frequency are 1 - 999."
        end
      end

      # Converts the number of seconds to an ISO8601 duration format and returns it.
      # Ref : https://github.com/arnau/ISO8601/blob/master/lib/iso8601/duration.rb#L18-L23
      # e.g.
      # ISO8601::Duration.new(65707200).to_s
      # returns 'PT65707200S'
      def sec_to_dur(seconds)
        ISO8601::Duration.new(seconds.to_i).to_s
      end

      def sec_to_min(seconds)
        seconds.to_i / 60
      end

      action_class do
        if ChefUtils.windows_ruby?
          include ::Win32

          MONTHS = {
            JAN: ::Win32::TaskScheduler::JANUARY,
            FEB: ::Win32::TaskScheduler::FEBRUARY,
            MAR: ::Win32::TaskScheduler::MARCH,
            APR: ::Win32::TaskScheduler::APRIL,
            MAY: ::Win32::TaskScheduler::MAY,
            JUN: ::Win32::TaskScheduler::JUNE,
            JUL: ::Win32::TaskScheduler::JULY,
            AUG: ::Win32::TaskScheduler::AUGUST,
            SEP: ::Win32::TaskScheduler::SEPTEMBER,
            OCT: ::Win32::TaskScheduler::OCTOBER,
            NOV: ::Win32::TaskScheduler::NOVEMBER,
            DEC: ::Win32::TaskScheduler::DECEMBER,
          }.freeze

          DAYS_OF_WEEK = { MON: ::Win32::TaskScheduler::MONDAY,
                           TUE: ::Win32::TaskScheduler::TUESDAY,
                           WED: ::Win32::TaskScheduler::WEDNESDAY,
                           THU: ::Win32::TaskScheduler::THURSDAY,
                           FRI: ::Win32::TaskScheduler::FRIDAY,
                           SAT: ::Win32::TaskScheduler::SATURDAY,
                           SUN: ::Win32::TaskScheduler::SUNDAY }.freeze

          WEEKS_OF_MONTH = {
            FIRST: ::Win32::TaskScheduler::FIRST_WEEK,
            SECOND: ::Win32::TaskScheduler::SECOND_WEEK,
            THIRD: ::Win32::TaskScheduler::THIRD_WEEK,
            FOURTH: ::Win32::TaskScheduler::FOURTH_WEEK,
          }.freeze

          DAYS_OF_MONTH = {
            1 => ::Win32::TaskScheduler::TASK_FIRST,
            2 => ::Win32::TaskScheduler::TASK_SECOND,
            3 => ::Win32::TaskScheduler::TASK_THIRD,
            4 => ::Win32::TaskScheduler::TASK_FOURTH,
            5 => ::Win32::TaskScheduler::TASK_FIFTH,
            6 => ::Win32::TaskScheduler::TASK_SIXTH,
            7 => ::Win32::TaskScheduler::TASK_SEVENTH,
            8 => ::Win32::TaskScheduler::TASK_EIGHTH,
            # cspell:disable-next-line
            9 => ::Win32::TaskScheduler::TASK_NINETH,
            10 => ::Win32::TaskScheduler::TASK_TENTH,
            11 => ::Win32::TaskScheduler::TASK_ELEVENTH,
            12 => ::Win32::TaskScheduler::TASK_TWELFTH,
            13 => ::Win32::TaskScheduler::TASK_THIRTEENTH,
            14 => ::Win32::TaskScheduler::TASK_FOURTEENTH,
            15 => ::Win32::TaskScheduler::TASK_FIFTEENTH,
            16 => ::Win32::TaskScheduler::TASK_SIXTEENTH,
            17 => ::Win32::TaskScheduler::TASK_SEVENTEENTH,
            18 => ::Win32::TaskScheduler::TASK_EIGHTEENTH,
            19 => ::Win32::TaskScheduler::TASK_NINETEENTH,
            20 => ::Win32::TaskScheduler::TASK_TWENTIETH,
            21 => ::Win32::TaskScheduler::TASK_TWENTY_FIRST,
            22 => ::Win32::TaskScheduler::TASK_TWENTY_SECOND,
            23 => ::Win32::TaskScheduler::TASK_TWENTY_THIRD,
            24 => ::Win32::TaskScheduler::TASK_TWENTY_FOURTH,
            25 => ::Win32::TaskScheduler::TASK_TWENTY_FIFTH,
            26 => ::Win32::TaskScheduler::TASK_TWENTY_SIXTH,
            27 => ::Win32::TaskScheduler::TASK_TWENTY_SEVENTH,
            28 => ::Win32::TaskScheduler::TASK_TWENTY_EIGHTH,
            29 => ::Win32::TaskScheduler::TASK_TWENTY_NINTH,
            # cspell:disable-next-line
            30 => ::Win32::TaskScheduler::TASK_THIRTYETH,
            31 => ::Win32::TaskScheduler::TASK_THIRTY_FIRST,
          }.freeze

          PRIORITY = { "critical" => 0, "highest" => 1,  "above_normal_2" => 2 , "above_normal_3" => 3, "normal_4" => 4,
                       "normal_5" => 5, "normal_6" => 6, "below_normal_7" => 7, "below_normal_8" => 8, "lowest" => 9, "idle" => 10 }.freeze
        end

        def load_current_resource
          @current_resource = Chef::Resource::WindowsTask.new(new_resource.name)
          task = ::Win32::TaskScheduler.new(new_resource.task_name, nil, "\\", false)
          @current_resource.exists = task.exists?(new_resource.task_name)
          if @current_resource.exists
            task.get_task(new_resource.task_name)
            @current_resource.task = task
            pathed_task_name = new_resource.task_name.start_with?("\\") ? new_resource.task_name : "\\#{new_resource.task_name}"
            @current_resource.task_name(pathed_task_name)
          end
          @current_resource
        end

        # separated command arguments from :command property
        def set_command_and_arguments
          cmd, *args = Chef::Util::PathHelper.split_args(new_resource.command)
          new_resource.command = cmd
          new_resource.command_arguments = args.join(" ")
        end

        def set_start_day_and_time
          new_resource.start_day = Time.now.strftime("%m/%d/%Y") unless new_resource.start_day
          new_resource.start_time = Time.now.strftime("%H:%M") unless new_resource.start_time
        end

        def update_task(task)
          converge_by("#{new_resource} task updated") do
            do_backup
            task.set_account_information(new_resource.user, new_resource.password, new_resource.interactive_enabled)
            task.application_name = new_resource.command if new_resource.command
            task.parameters = new_resource.command_arguments if new_resource.command_arguments
            task.working_directory = new_resource.cwd if new_resource.cwd
            task.trigger = trigger unless new_resource.frequency == :none
            task.configure_settings(config_settings)
            task.creator = new_resource.user
            task.description = new_resource.description unless new_resource.description.nil?
            task.configure_principals(principal_settings)
          end
        end

        def trigger
          start_month, start_day, start_year = new_resource.start_day.to_s.split("/")
          start_hour, start_minute = new_resource.start_time.to_s.split(":")
          # TODO currently end_month, end_year and end_year needs to be set to 0. If not set win32-taskscheduler throwing nil into integer error.
          trigger_hash = {
            start_year: start_year.to_i,
            start_month: start_month.to_i,
            start_day: start_day.to_i,
            start_hour: start_hour.to_i,
            start_minute: start_minute.to_i,
            end_month: 0,
            end_day: 0,
            end_year: 0,
            trigger_type: trigger_type,
            type: type,
            random_minutes_interval: new_resource.random_delay,
          }

          if new_resource.frequency == :minute
            trigger_hash[:minutes_interval] = new_resource.frequency_modifier
          end

          if new_resource.frequency == :hourly
            minutes = convert_hours_in_minutes(new_resource.frequency_modifier.to_i)
            trigger_hash[:minutes_interval] = minutes
          end

          if new_resource.minutes_interval
            trigger_hash[:minutes_interval] = new_resource.minutes_interval
          end

          if new_resource.minutes_duration
            trigger_hash[:minutes_duration] = new_resource.minutes_duration
          end

          if trigger_type == ::Win32::TaskScheduler::MONTHLYDOW && frequency_modifier_contains_last_week?(new_resource.frequency_modifier)
            trigger_hash[:run_on_last_week_of_month] = true
          else
            trigger_hash[:run_on_last_week_of_month] = false
          end

          if trigger_type == ::Win32::TaskScheduler::MONTHLYDATE && day_includes_last_or_lastday?(new_resource.day)
            trigger_hash[:run_on_last_day_of_month] = true
          else
            trigger_hash[:run_on_last_day_of_month] = false
          end
          trigger_hash
        end

        def frequency_modifier_contains_last_week?(frequency_modifier)
          frequency_modifier = frequency_modifier.to_s.split(",")
          frequency_modifier.map! { |value| value.strip.upcase }
          frequency_modifier.include?("LAST")
        end

        def day_includes_last_or_lastday?(day)
          day = day.to_s.split(",")
          day.map! { |value| value.strip.upcase }
          day.include?("LAST") || day.include?("LASTDAY")
        end

        def convert_hours_in_minutes(hours)
          hours.to_i * 60 if hours
        end

        # TODO : Try to optimize this method
        # known issue : Since start_day and time is not mandatory while updating weekly frequency for which start_day is not mentioned by user idempotency
        # is not getting maintained as new_resource.start_day is nil and we fetch the day of week from start_day to set and its currently coming as nil and don't match with current_task
        def task_needs_update?(task)
          flag = false
          if new_resource.frequency == :none
            flag = (task.author != new_resource.user ||
                    task.application_name != new_resource.command ||
                    description_needs_update?(task) ||
                    task.parameters != new_resource.command_arguments.to_s ||
                    task.principals[:run_level] != run_level ||
                    task.settings[:disallow_start_if_on_batteries] != new_resource.disallow_start_if_on_batteries ||
                    task.settings[:stop_if_going_on_batteries] != new_resource.stop_if_going_on_batteries ||
                    task.settings[:start_when_available] != new_resource.start_when_available)
          else
            current_task_trigger = task.trigger(0)
            new_task_trigger = trigger
            flag = (ISO8601::Duration.new(task.idle_settings[:idle_duration])) != (ISO8601::Duration.new(new_resource.idle_time * 60)) if new_resource.frequency == :on_idle
            flag = (ISO8601::Duration.new(task.execution_time_limit)) != (ISO8601::Duration.new(new_resource.execution_time_limit * 60)) unless new_resource.execution_time_limit.nil?

            # if trigger not found updating the task to add the trigger
            if current_task_trigger.nil?
              flag = true
            else
              flag = true if start_day_updated?(current_task_trigger, new_task_trigger) == true ||
                start_time_updated?(current_task_trigger, new_task_trigger) == true ||
                current_task_trigger[:trigger_type] != new_task_trigger[:trigger_type] ||
                current_task_trigger[:type] != new_task_trigger[:type] ||
                current_task_trigger[:random_minutes_interval].to_i != new_task_trigger[:random_minutes_interval].to_i ||
                current_task_trigger[:minutes_interval].to_i != new_task_trigger[:minutes_interval].to_i ||
                task.author.to_s.casecmp(new_resource.user.to_s) != 0 ||
                task.application_name != new_resource.command ||
                description_needs_update?(task) ||
                task.parameters != new_resource.command_arguments.to_s ||
                task.working_directory != new_resource.cwd.to_s ||
                task.principals[:logon_type] != logon_type ||
                task.principals[:run_level] != run_level ||
                PRIORITY[task.priority] != new_resource.priority ||
                task.settings[:disallow_start_if_on_batteries] != new_resource.disallow_start_if_on_batteries ||
                task.settings[:stop_if_going_on_batteries] != new_resource.stop_if_going_on_batteries ||
                task.settings[:start_when_available] != new_resource.start_when_available
              if trigger_type == ::Win32::TaskScheduler::MONTHLYDATE
                flag = true if current_task_trigger[:run_on_last_day_of_month] != new_task_trigger[:run_on_last_day_of_month]
              end

              if trigger_type == ::Win32::TaskScheduler::MONTHLYDOW
                flag = true if current_task_trigger[:run_on_last_week_of_month] != new_task_trigger[:run_on_last_week_of_month]
              end
            end
          end
          flag
        end

        def start_day_updated?(current_task_trigger, new_task_trigger)
          ( new_resource.start_day && (current_task_trigger[:start_year].to_i != new_task_trigger[:start_year] ||
                                       current_task_trigger[:start_month].to_i != new_task_trigger[:start_month] ||
                                       current_task_trigger[:start_day].to_i != new_task_trigger[:start_day]) )
        end

        def start_time_updated?(current_task_trigger, new_task_trigger)
          ( new_resource.start_time && ( current_task_trigger[:start_hour].to_i != new_task_trigger[:start_hour] ||
                                        current_task_trigger[:start_minute].to_i != new_task_trigger[:start_minute] ) )
        end

        def trigger_type
          case new_resource.frequency
          when :once, :minute, :hourly
            ::Win32::TaskScheduler::ONCE
          when :daily
            ::Win32::TaskScheduler::DAILY
          when :weekly
            ::Win32::TaskScheduler::WEEKLY
          when :monthly
            # If frequency modifier is set with frequency :monthly we are setting taskscheduler as monthlydow
            # Ref https://msdn.microsoft.com/en-us/library/windows/desktop/aa382061(v=vs.85).aspx
            new_resource.frequency_modifier.to_i.between?(1, 12) ? ::Win32::TaskScheduler::MONTHLYDATE : ::Win32::TaskScheduler::MONTHLYDOW
          when :on_idle
            ::Win32::TaskScheduler::ON_IDLE
          when :onstart
            ::Win32::TaskScheduler::AT_SYSTEMSTART
          when :on_logon
            ::Win32::TaskScheduler::AT_LOGON
          else
            raise ArgumentError, "Please set frequency"
          end
        end

        def type
          case trigger_type
          when ::Win32::TaskScheduler::ONCE
            { once: nil }
          when ::Win32::TaskScheduler::DAILY
            { days_interval: new_resource.frequency_modifier.to_i }
          when ::Win32::TaskScheduler::WEEKLY
            { weeks_interval: new_resource.frequency_modifier.to_i, days_of_week: days_of_week.to_i }
          when ::Win32::TaskScheduler::MONTHLYDATE
            { months: months_of_year.to_i, days: days_of_month.to_i }
          when ::Win32::TaskScheduler::MONTHLYDOW
            { months: months_of_year.to_i, days_of_week: days_of_week.to_i, weeks_of_month: weeks_of_month.to_i }
          when ::Win32::TaskScheduler::ON_IDLE
            # TODO: handle option for this trigger
          when ::Win32::TaskScheduler::AT_LOGON
            # TODO: handle option for this trigger
          when ::Win32::TaskScheduler::AT_SYSTEMSTART
            # TODO: handle option for this trigger
          end
        end

        # Deleting last from the array of weeks of month since last week is handled in :run_on_last_week_of_month parameter.
        def weeks_of_month
          weeks_of_month = []
          if new_resource.frequency_modifier
            weeks = new_resource.frequency_modifier.split(",")
            weeks.map! { |week| week.to_s.strip.upcase }
            weeks.delete("LAST") if weeks.include?("LAST")
            weeks_of_month = get_binary_values_from_constants(weeks, WEEKS_OF_MONTH)
          end
          weeks_of_month
        end

        # Deleting the "LAST" and "LASTDAY" from days since last day is handled in :run_on_last_day_of_month parameter.
        def days_of_month
          days_of_month = []
          if new_resource.day
            days = new_resource.day.to_s.split(",")
            days.map! { |day| day.to_s.strip.upcase }
            days.delete("LAST") if days.include?("LAST")
            days.delete("LASTDAY") if days.include?("LASTDAY")
            if days - (1..31).to_a
              days.each do |day|
                days_of_month << DAYS_OF_MONTH[day.to_i]
              end
              days_of_month = days_of_month.size > 1 ? days_of_month.inject(:|) : days_of_month[0]
            end
          else
            days_of_month = DAYS_OF_MONTH[1]
          end
          days_of_month
        end

        def days_of_week
          if new_resource.day
            # this line of code is just to support backward compatibility of wild card *
            new_resource.day = "mon, tue, wed, thu, fri, sat, sun" if new_resource.day == "*" && new_resource.frequency == :weekly
            days = new_resource.day.to_s.split(",")
            days.map! { |day| day.to_s.strip.upcase }
            weeks_days = get_binary_values_from_constants(days, DAYS_OF_WEEK)
          else
            # following condition will make the frequency :weekly idempotent if start_day is not provided by user setting day as the current_resource day
            if (current_resource) && (current_resource.task) && (current_resource.task.trigger(0)[:type][:days_of_week]) && (new_resource.start_day.nil?)
              weeks_days = current_resource.task.trigger(0)[:type][:days_of_week]
            else
              day = get_day(new_resource.start_day).to_sym if new_resource.start_day
              DAYS_OF_WEEK[day]
            end
          end
        end

        def months_of_year
          months_of_year = []
          if new_resource.frequency_modifier.to_i.between?(1, 12) && !(new_resource.months)
            new_resource.months = set_months(new_resource.frequency_modifier.to_i)
          end

          if new_resource.months
            # this line of code is just to support backward compatibility of wild card *
            new_resource.months = "jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec" if new_resource.months == "*" && new_resource.frequency == :monthly
            months = new_resource.months.split(",")
            months.map! { |month| month.to_s.strip.upcase }
            months_of_year = get_binary_values_from_constants(months, MONTHS)
          else
            MONTHS.each do |key, value|
              months_of_year << MONTHS[key]
            end
            months_of_year = months_of_year.inject(:|)
          end
          months_of_year
        end

        # This values are set for frequency_modifier set as 1-12
        # This is to give backward compatibility validated this values with earlier code and running schtask.exe
        # Used this as reference https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks#d-dayday--
        def set_months(frequency_modifier)
          case frequency_modifier
          when 1
            "jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec"
          when 2
            "feb, apr, jun, aug, oct, dec"
          when 3
            "mar, jun, sep, dec"
          when 4
            "apr, aug, dec"
          when 5
            "may, oct"
          when 6
            "jun, dec"
          when 7
            "jul"
          when 8
            "aug"
          when 9
            "sep"
          when 10
            "oct"
          when 11
            "nov"
          when 12
            "dec"
          end
        end

        def get_binary_values_from_constants(array_values, constant)
          data = []
          array_values.each do |value|
            value = value.to_sym
            data << constant[value]
          end
          data.size > 1 ? data.inject(:|) : data[0]
        end

        def run_level
          case new_resource.run_level
          when :highest
            ::Win32::TaskScheduler::TASK_RUNLEVEL_HIGHEST
          when :limited
            ::Win32::TaskScheduler::TASK_RUNLEVEL_LUA
          end
        end

        # TODO: while creating the configuration settings win32-taskscheduler it accepts execution time limit values in ISO8601 format
        def config_settings
          settings = {
            execution_time_limit: new_resource.execution_time_limit,
            enabled: true,
          }
          settings[:idle_duration] = new_resource.idle_time if new_resource.idle_time
          settings[:run_only_if_idle] = true if new_resource.idle_time
          settings[:priority] = new_resource.priority
          settings[:disallow_start_if_on_batteries] = new_resource.disallow_start_if_on_batteries
          settings[:stop_if_going_on_batteries] = new_resource.stop_if_going_on_batteries
          settings[:start_when_available] = new_resource.start_when_available
          settings
        end

        def principal_settings
          settings = {}
          settings[:run_level] = run_level
          settings[:logon_type] = logon_type
          settings
        end

        def description_needs_update?(task)
          task.description != new_resource.description unless new_resource.description.nil?
        end

        def logon_type
          # Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/aa383566(v=vs.85).aspx
          # if nothing is passed as logon_type the TASK_LOGON_SERVICE_ACCOUNT is getting set as default so using that for comparison.
          user_id = new_resource.user.to_s
          password = new_resource.password.to_s
          if Chef::ReservedNames::Win32::Security::SID.service_account_user?(user_id)
            ::Win32::TaskScheduler::TASK_LOGON_SERVICE_ACCOUNT
          elsif Chef::ReservedNames::Win32::Security::SID.group_user?(user_id)
            ::Win32::TaskScheduler::TASK_LOGON_GROUP
          elsif !user_id.empty? && !password.empty?
            if new_resource.interactive_enabled
              ::Win32::TaskScheduler::TASK_LOGON_INTERACTIVE_TOKEN
            else
              ::Win32::TaskScheduler::TASK_LOGON_PASSWORD
            end
          else
            ::Win32::TaskScheduler::TASK_LOGON_INTERACTIVE_TOKEN
          end
        end

        # This method checks if task and command properties exist since those two are mandatory properties to create a schedules task.
        def basic_validation
          validate = []
          validate << "Command" if new_resource.command.nil? || new_resource.command.empty?
          validate << "Task Name" if new_resource.task_name.nil? || new_resource.task_name.empty?
          return true if validate.empty?

          raise Chef::Exceptions::ValidationFailed.new "Value for '#{validate.join(", ")}' option cannot be empty"
        end

        # rubocop:disable Style/StringLiteralsInInterpolation
        def run_schtasks(task_action, options = {})
          cmd = "schtasks /#{task_action} /TN \"#{new_resource.task_name}\" "
          options.each_key do |option|
            unless option == "TR"
              cmd += "/#{option} "
              cmd += "\"#{options[option].to_s.gsub('"', "\\\"")}\" " unless options[option] == ""
            end
          end
          # Appending Task Run [TR] option at the end since appending causing sometimes to append other options in option["TR"] value
          if options["TR"]
            cmd += "/TR \"#{options["TR"]} \" " unless task_action == "DELETE"
          end
          logger.trace("running: ")
          logger.trace("    #{cmd}")
          shell_out!(cmd, returns: [0])
        end
        # rubocop:enable Style/StringLiteralsInInterpolation

        def get_day(date)
          Date.strptime(date, "%m/%d/%Y").strftime("%a").upcase
        end

        def do_backup
          file = "C:/Windows/System32/Tasks/#{new_resource.task_name}"
          Chef::Util::Backup.new(new_resource, file).backup!
        end
      end

      action :create, description: "Creates a scheduled task, or updates an existing task if any property has changed." do
        set_command_and_arguments if new_resource.command

        if current_resource.exists
          logger.trace "#{new_resource} task exist."
          unless (task_needs_update?(current_resource.task)) || (new_resource.force)
            logger.info "#{new_resource} task does not need updating and force is not specified - nothing to do"
            return
          end

          # if start_day and start_time is not set by user current date and time will be set while updating any property
          set_start_day_and_time unless new_resource.frequency == :none
          update_task(current_resource.task)
        else
          basic_validation
          set_start_day_and_time
          converge_by("#{new_resource} task created") do
            task = ::Win32::TaskScheduler.new
            if new_resource.frequency == :none
              task.new_work_item(new_resource.task_name, {}, { user: new_resource.user, password: new_resource.password, interactive: new_resource.interactive_enabled })
              task.activate(new_resource.task_name)
            else
              task.new_work_item(new_resource.task_name, trigger, { user: new_resource.user, password: new_resource.password, interactive: new_resource.interactive_enabled })
            end
            task.application_name = new_resource.command
            task.parameters = new_resource.command_arguments if new_resource.command_arguments
            task.working_directory = new_resource.cwd if new_resource.cwd
            task.configure_settings(config_settings)
            task.configure_principals(principal_settings)
            task.set_account_information(new_resource.user, new_resource.password, new_resource.interactive_enabled)
            task.creator = new_resource.user
            task.description = new_resource.description unless new_resource.description.nil?
            task.activate(new_resource.task_name)
          end
        end
      end

      action :run, description: "Runs a scheduled task." do
        if current_resource.exists
          logger.trace "#{new_resource} task exists"
          if current_resource.task.status == "running"
            logger.info "#{new_resource} task is currently running, skipping run"
          else
            converge_by("run scheduled task #{new_resource}") do
              current_resource.task.run
            end
          end
        else
          logger.debug "#{new_resource} task does not exist - nothing to do"
        end
      end

      action :delete, description: "Deletes a scheduled task." do
        if current_resource.exists
          logger.trace "#{new_resource} task exists"
          converge_by("delete scheduled task #{new_resource}") do
            do_backup
            ts = ::Win32::TaskScheduler.new
            ts.delete(current_resource.task_name)
          end
        else
          logger.debug "#{new_resource} task does not exist - nothing to do"
        end
      end

      action :end, description: "Ends a scheduled task." do
        if current_resource.exists
          logger.trace "#{new_resource} task exists"
          if current_resource.task.status != "running"
            logger.debug "#{new_resource} is not running - nothing to do"
          else
            converge_by("#{new_resource} task ended") do
              current_resource.task.stop
            end
          end
        else
          logger.debug "#{new_resource} task does not exist - nothing to do"
        end
      end

      action :enable, description: "Enables a scheduled task." do
        if current_resource.exists
          logger.trace "#{new_resource} task exists"
          if current_resource.task.status == "not scheduled"
            converge_by("#{new_resource} task enabled") do
              # TODO wind32-taskscheduler currently not having any method to handle this so using schtasks.exe here
              run_schtasks "CHANGE", "ENABLE" => ""
            end
          else
            logger.debug "#{new_resource} already enabled - nothing to do"
          end
        else
          logger.fatal "#{new_resource} task does not exist - nothing to do"
          raise Errno::ENOENT, "#{new_resource}: task does not exist, cannot enable"
        end
      end

      action :disable, description: "Disables a scheduled task." do
        if current_resource.exists
          logger.info "#{new_resource} task exists"
          if %w{ready running}.include?(current_resource.task.status)
            converge_by("#{new_resource} task disabled") do
              # TODO: in win32-taskscheduler there is no method which disables the task so currently calling disable with schtasks.exe
              run_schtasks "CHANGE", "DISABLE" => ""
            end
          else
            logger.warn "#{new_resource} already disabled - nothing to do"
          end
        else
          logger.warn "#{new_resource} task does not exist - nothing to do"
        end
      end

      action_class do
        alias_method :action_change, :action_create
      end
    end
  end
end
