#
# Author:: Seth Chisamore (<schisamo@chef.io>)
# Copyright:: Copyright 2011-2019, 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.
#

if RUBY_PLATFORM =~ /mswin|mingw32|windows/
  require "win32/service"
end
require_relative "../config"
require "mixlib/cli" unless defined?(Mixlib::CLI)
require_relative "../dist"

class Chef
  class Application
    #
    # This class is used to create and manage a windows service.
    # Service should be created using Daemon class from
    # win32/service gem.
    # For an example see: Chef::Application::WindowsService
    #
    # Outside programs are expected to use this class to manage
    # windows services.
    #
    class WindowsServiceManager
      include Mixlib::CLI

      option :action,
        short: "-a ACTION",
        long: "--action ACTION",
        default: "status",
        description: "Action to carry out on #{Chef::Dist::SHORT}-service (install, uninstall, status, start, stop, pause, or resume)."

      option :config_file,
        short: "-c CONFIG",
        long: "--config CONFIG",
        default: "#{ChefConfig::Config.c_chef_dir}/client.rb",
        description: "The configuration file to use for #{Chef::Dist::PRODUCT} runs."

      option :log_location,
        short: "-L LOGLOCATION",
        long: "--logfile LOGLOCATION",
        description: "Set the log file location for #{Chef::Dist::SHORT}-service."

      option :help,
        short: "-h",
        long: "--help",
        description: "Show this help message.",
        on: :tail,
        boolean: true,
        show_options: true,
        exit: 0

      option :version,
        short: "-v",
        long: "--version",
        description: "Show #{Chef::Dist::PRODUCT} version.",
        boolean: true,
        proc: lambda { |v| puts "#{Chef::Dist::PRODUCT}: #{::Chef::VERSION}" },
        exit: 0

      def initialize(service_options)
        # having to call super in initialize is the most annoying
        # anti-pattern :(
        super()

        raise ArgumentError, "Service definition is not provided" if service_options.nil?

        required_options = %i{service_name service_display_name service_description service_file_path}

        required_options.each do |req_option|
          unless service_options.key?(req_option)
            raise ArgumentError, "Service definition doesn't contain required option #{req_option}"
          end
        end

        @service_name = service_options[:service_name]
        @service_display_name = service_options[:service_display_name]
        @service_description = service_options[:service_description]
        @service_file_path = service_options[:service_file_path]
        @service_start_name = service_options[:run_as_user]
        @password = service_options[:run_as_password]
        @delayed_start = service_options[:delayed_start]
        @dependencies = service_options[:dependencies]
      end

      def run(params = ARGV)
        parse_options(params)

        case config[:action]
        when "install"
          if service_exists?
            puts "Service #{@service_name} already exists on the system."
          else
            ruby = File.join(RbConfig::CONFIG["bindir"], "ruby")

            opts = ""
            opts << " -c #{config[:config_file]}" if config[:config_file]
            opts << " -L #{config[:log_location]}" if config[:log_location]

            # Quote the full paths to deal with possible spaces in the path name.
            # Also ensure all forward slashes are backslashes
            cmd = "\"#{ruby}\" \"#{@service_file_path}\" #{opts}".gsub(File::SEPARATOR, File::ALT_SEPARATOR)

            ::Win32::Service.new(
              service_name: @service_name,
              display_name: @service_display_name,
              description: @service_description,
              # Prior to 0.8.5, win32-service creates interactive services by default,
              # and we don't want that, so we need to override the service type.
              service_type: ::Win32::Service::SERVICE_WIN32_OWN_PROCESS,
              start_type: ::Win32::Service::SERVICE_AUTO_START,
              binary_path_name: cmd,
              service_start_name: @service_start_name,
              password: @password,
              dependencies: @dependencies
            )
            unless @delayed_start.nil?
              ::Win32::Service.configure(
                service_name: @service_name,
                delayed_start: @delayed_start
              )
            end
            puts "Service '#{@service_name}' has successfully been installed."
          end
        when "status"
          if !service_exists?
            puts "Service #{@service_name} doesn't exist on the system."
          else
            puts "State of #{@service_name} service is: #{current_state}"
          end
        when "start"
          # TODO: allow override of startup parameters here?
          take_action("start", RUNNING)
        when "stop"
          take_action("stop", STOPPED)
        when "uninstall", "delete"
          take_action("stop", STOPPED)
          unless service_exists?
            puts "Service #{@service_name} doesn't exist on the system."
          else
            ::Win32::Service.delete(@service_name)
            puts "Service #{@service_name} deleted"
          end
        when "pause"
          take_action("pause", PAUSED)
        when "resume"
          take_action("resume", RUNNING)
        end
      end

      private

      # Just some state constants
      STOPPED = "stopped".freeze
      RUNNING = "running".freeze
      PAUSED = "paused".freeze

      def service_exists?
        ::Win32::Service.exists?(@service_name)
      end

      def take_action(action = nil, desired_state = nil)
        if service_exists?
          if current_state != desired_state
            ::Win32::Service.send(action, @service_name)
            wait_for_state(desired_state)
            puts "Service '#{@service_name}' is now '#{current_state}'."
          else
            puts "Service '#{@service_name}' is already '#{desired_state}'."
          end
        else
          puts "Cannot '#{action}' service '#{@service_name}'"
          puts "Service #{@service_name} doesn't exist on the system."
        end
      end

      def current_state
        ::Win32::Service.status(@service_name).current_state
      end

      # Helper method that waits for a status to change its state since state
      # changes aren't usually instantaneous.
      def wait_for_state(desired_state)
        while current_state != desired_state
          puts "One moment... #{current_state}"
          sleep 1
        end
      end

    end
  end
end
