# frozen_string_literal: true

require 'puppet/parameter'
require_relative '../../../puppet_x/puppetlabs/scheduled_task/task'

Puppet::Type.type(:scheduled_task).provide(:taskscheduler_api2) do
  desc "This provider manages scheduled tasks on Windows.
       This is a technical preview using the newer V2 API interface but
       still editing V1 compatbile scheduled tasks."

  defaultfor operatingsystem: :windows
  confine    operatingsystem: :windows

  has_feature :compatibility

  def self.instances
    task = PuppetX::PuppetLabs::ScheduledTask::Task
    task.tasks(task::V2_COMPATIBILITY).map do |task_name|
      new(
        provider: :taskscheduler_api2,
        name: task_name,
      )
    end
  end

  def exists?
    PuppetX::PuppetLabs::ScheduledTask::Task.exists? resource[:name]
  end

  def task
    @task ||=
      PuppetX::PuppetLabs::ScheduledTask::Task.new(resource[:name])
  end

  def enabled
    task.enabled ? :true : :false
  end

  def command
    task.application_name
  end

  def arguments
    task.parameters
  end

  def description
    task.description
  end

  def working_dir
    task.working_directory
  end

  def user
    account = task.account_information
    return 'system' if account == ''
    account
  end

  def compatibility
    task.compatibility
  end

  def trigger
    @triggers ||= task.triggers.compact # remove nils for unsupported trigger types
  end

  def user_insync?(current, should)
    return false unless current

    # Win32::TaskScheduler can return the 'SYSTEM' account as the
    # empty string.
    current = 'system' if current == ''

    # By comparing account SIDs we don't have to worry about case
    # sensitivity, or canonicalization of the account name.
    Puppet::Util::Windows::SID.name_to_sid(current) == Puppet::Util::Windows::SID.name_to_sid(should[0])
  end

  def trigger_insync?(current, should)
    should  = [should] unless should.is_a?(Array)
    current = [current] unless current.is_a?(Array)
    return false unless current.length == should.length

    current_in_sync = current.all? do |c|
      should.any? { |s| triggers_same?(c, s) }
    end

    should_in_sync = should.all? do |s|
      current.any? { |c| triggers_same?(c, s) }
    end

    current_in_sync && should_in_sync
  end

  def command=(value)
    task.application_name = value
  end

  def arguments=(value)
    task.parameters = value
  end

  def description=(value)
    task.description = value
  end

  def working_dir=(value)
    task.working_directory = value
  end

  def enabled=(value)
    task.enabled = (value == :true)
  end

  def compatibility=(value)
    task.compatibility = value
  end

  def trigger=(value)
    desired_triggers = value.is_a?(Array) ? value : [value]
    current_triggers = trigger.is_a?(Array) ? trigger : [trigger]

    extra_triggers = []
    desired_to_search = desired_triggers.dup
    current_triggers.each do |current|
      if (found = desired_to_search.find { |desired| triggers_same?(current, desired) })
        desired_to_search.delete(found)
      else
        extra_triggers << current['index']
      end
    end

    needed_triggers = []
    current_to_search = current_triggers.dup
    desired_triggers.each do |desired|
      if (found = current_to_search.find { |current| triggers_same?(current, desired) })
        current_to_search.delete(found)
      else
        needed_triggers << desired
      end
    end

    extra_triggers.reverse_each do |index|
      task.delete_trigger(index)
    end

    needed_triggers.each do |trigger_hash|
      task.append_trigger(trigger_hash)
    end
  end

  def user=(value)
    raise("Invalid user: #{value}") unless Puppet::Util::Windows::SID.name_to_sid(value)

    if !value.to_s.casecmp('system').zero?
      task.set_account_information(value, resource[:password])
    else
      # Win32::TaskScheduler treats a nil/empty username & password as
      # requesting the SYSTEM account.
      task.set_account_information(nil, nil)
    end
  end

  def create
    @triggers = nil
    @task = PuppetX::PuppetLabs::ScheduledTask::Task.new(resource[:name])
    self.command = resource[:command]

    [:arguments, :working_dir, :enabled, :trigger, :user, :compatibility, :description].each do |prop|
      send("#{prop}=", resource[prop]) if resource[prop]
    end
  end

  def destroy
    PuppetX::PuppetLabs::ScheduledTask::Task.delete(resource[:name])
  end

  def flush
    return if resource[:ensure] == :absent

    raise('Parameter command is required.') unless resource[:command]
    # HACK: even though the user may actually be insync?, for task changes to
    # fully propagate, it is necessary to explicitly set the user for the task,
    # even when it is SYSTEM (and has a nil password)
    # this is a Windows security feature with the v1 COM APIs that prevent
    # arbitrary reassignment of a task scheduler command to run as SYSTEM
    # without the authorization to do so
    self.user = resource[:user]
    task.save
    @task = nil
  end

  def triggers_same?(current_trigger, desired_trigger)
    return false if current_trigger.key?('enabled') && !current_trigger['enabled']
    # Canonicalizing the desired hash ensures it is in a matching state with what we convert from on-disk
    desired = PuppetX::PuppetLabs::ScheduledTask::Trigger::Manifest.canonicalize_and_validate(desired_trigger)
    # This method ensures that current_trigger:
    # - includes all of the key-value pairs from desired
    # - that those key value pairs are exactly matched
    # - does not preclude current_trigger from having _more_ keys than the desired trigger.
    # It will return false if any pair is missing or the values do not match.
    current_trigger.merge(desired) == current_trigger
  end

  def validate_trigger(value)
    [value].flatten.each do |t|
      ['index', 'enabled'].each do |key|
        if t.key?(key)
          raise "'#{key}' is read-only on scheduled_task triggers and should be removed ('#{key}' is usually provided in puppet resource scheduled_task)."
        end
      end
      PuppetX::PuppetLabs::ScheduledTask::Trigger::Manifest.canonicalize_and_validate(t)
    end

    true
  end

  def validate_name
    return unless @resource[:name].include?('\\') && @resource[:compatibility] < 2

    raise Puppet::ResourceError, "#{@resource[:name]} specifies a path including subfolders and a compatibility of #{@resource[:compatibility]}"\
                                 ' - tasks in subfolders are only supported on version 2 and later of the API. Specify a compatibility of 2 or higher or do not specify a subfolder path.'
  end
end
