#
# Author:: Adam Edwards (<adamed@chef.io>)
#
# Copyright:: Copyright 2014-2017, 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 "../util/powershell/cmdlet"
require_relative "../util/dsc/local_configuration_manager"
require_relative "../mixin/powershell_type_coercions"
require_relative "../util/dsc/resource_store"

class Chef
  class Provider
    class DscResource < Chef::Provider
      include Chef::Mixin::PowershellTypeCoercions
      provides :dsc_resource
      def initialize(new_resource, run_context)
        super
        @new_resource = new_resource
        @module_name = new_resource.module_name
        @module_version = new_resource.module_version
        @reboot_resource = nil
      end

      def action_run
        unless test_resource
          converge_by(generate_description) do
            result = set_resource
            reboot_if_required
          end
        end
      end

      def load_current_resource; end

      def define_resource_requirements
        requirements.assert(:run) do |a|
          a.assertion { supports_dsc_invoke_resource? }
          err = ["You must have PowerShell version >= 5.0.10018.0 to use dsc_resource."]
          a.failure_message Chef::Exceptions::ProviderNotFound,
            err
          a.whyrun err + ["Assuming a previous resource installs PowerShell 5.0.10018.0 or higher."]
          a.block_action!
        end
        requirements.assert(:run) do |a|
          a.assertion { supports_refresh_mode_enabled? || dsc_refresh_mode_disabled? }
          err = ["The LCM must have its RefreshMode set to Disabled for" \
                 " PowerShell versions before 5.0.10586.0."]
          a.failure_message Chef::Exceptions::ProviderNotFound, err.join(" ")
          a.whyrun err + ["Assuming a previous resource sets the RefreshMode."]
          a.block_action!
        end
        requirements.assert(:run) do |a|
          a.assertion { module_usage_valid? }
          err = ["module_name must be supplied along with module_version."]
          a.failure_message Chef::Exceptions::DSCModuleNameMissing,
            err
          a.block_action!
        end
      end

      protected

      def local_configuration_manager
        @local_configuration_manager ||= Chef::Util::DSC::LocalConfigurationManager.new(
          node,
          nil
        )
      end

      def resource_store
        Chef::Util::DSC::ResourceStore.instance
      end

      def supports_dsc_invoke_resource?
        run_context && Chef::Platform.supports_dsc_invoke_resource?(node)
      end

      def dsc_refresh_mode_disabled?
        Chef::Platform.dsc_refresh_mode_disabled?(node)
      end

      def supports_refresh_mode_enabled?
        Chef::Platform.supports_refresh_mode_enabled?(node)
      end

      def module_usage_valid?
        !(!@module_name && @module_version)
      end

      def generate_description
        @converge_description
      end

      def dsc_resource_name
        new_resource.resource.to_s
      end

      def module_name
        @module_name ||= begin
          found = resource_store.find(dsc_resource_name)
          r = case found.length
              when 0
                raise Chef::Exceptions::ResourceNotFound,
                  "Could not find #{dsc_resource_name}. Check to make "\
                  "sure that it shows up when running Get-DscResource"
              when 1
                if found[0]["Module"].nil?
                  "PSDesiredStateConfiguration" # default DSC module
                else
                  found[0]["Module"]["Name"]
                end
              else
                raise Chef::Exceptions::MultipleDscResourcesFound, found
              end
        end
      end

      def test_resource
        result = invoke_resource(:test)
        add_dsc_verbose_log(result)
        return_dsc_resource_result(result, "InDesiredState")
      end

      def set_resource
        result = invoke_resource(:set)
        add_dsc_verbose_log(result)
        create_reboot_resource if return_dsc_resource_result(result, "RebootRequired")
        result.return_value
      end

      def add_dsc_verbose_log(result)
        # We really want this information from the verbose stream,
        # however in some versions of WMF, Invoke-DscResource is not correctly
        # writing to that stream and instead just dumping to stdout
        verbose_output = result.stream(:verbose)
        verbose_output = result.stdout if verbose_output.empty?

        if @converge_description.nil? || @converge_description.empty?
          @converge_description = verbose_output
        else
          @converge_description << "\n"
          @converge_description << verbose_output
        end
      end

      def module_info_object
        @module_version.nil? ? module_name : "@{ModuleName='#{module_name}';ModuleVersion='#{@module_version}'}"
      end

      def invoke_resource(method, output_format = :object)
        properties = translate_type(new_resource.properties)
        switches = "-Method #{method} -Name #{new_resource.resource}"\
                   " -Property #{properties} -Module #{module_info_object} -Verbose"
        cmdlet = Chef::Util::Powershell::Cmdlet.new(
          node,
          "Invoke-DscResource #{switches}",
          output_format
        )
        cmdlet.run!({}, { timeout: new_resource.timeout })
      end

      def return_dsc_resource_result(result, property_name)
        if result.return_value.is_a?(Array)
          # WMF Feb 2015 Preview
          result.return_value[0][property_name]
        else
          # WMF April 2015 Preview
          result.return_value[property_name]
        end
      end

      def create_reboot_resource
        @reboot_resource = Chef::Resource::Reboot.new(
          "Reboot for #{new_resource.name}",
          run_context
        ).tap do |r|
          r.reason("Reboot for #{new_resource.resource}.")
        end
      end

      def reboot_if_required
        reboot_action = new_resource.reboot_action
        unless @reboot_resource.nil?
          case reboot_action
          when :nothing
            logger.trace("A reboot was requested by the DSC resource, but reboot_action is :nothing.")
            logger.trace("This dsc_resource will not reboot the node.")
          else
            logger.trace("Requesting node reboot with #{reboot_action}.")
            @reboot_resource.run_action(reboot_action)
          end
        end
      end
    end
  end
end
