# frozen_string_literal: true

require_relative '../../../puppet/ffi/windows'

module Puppet::Util::Windows
  # The Daemon class, based on the chef/win32-service implementation
  class Daemon
    include Puppet::FFI::Windows::Constants
    extend Puppet::FFI::Windows::Constants

    include Puppet::FFI::Windows::Structs
    extend Puppet::FFI::Windows::Structs

    include Puppet::FFI::Windows::Functions
    extend Puppet::FFI::Windows::Functions

    # Service is not running
    STOPPED = SERVICE_STOPPED

    # Service has received a start signal but is not yet running
    START_PENDING = SERVICE_START_PENDING

    # Service has received a stop signal but is not yet stopped
    STOP_PENDING  = SERVICE_STOP_PENDING

    # Service is running
    RUNNING = SERVICE_RUNNING

    # Service has received a signal to resume but is not yet running
    CONTINUE_PENDING = SERVICE_CONTINUE_PENDING

    # Service has received a signal to pause but is not yet paused
    PAUSE_PENDING = SERVICE_PAUSE_PENDING

    # Service is paused
    PAUSED = SERVICE_PAUSED

    # Service controls

    # Notifies service that it should stop
    CONTROL_STOP = SERVICE_CONTROL_STOP

    # Notifies service that it should pause
    CONTROL_PAUSE = SERVICE_CONTROL_PAUSE

    # Notifies service that it should resume
    CONTROL_CONTINUE = SERVICE_CONTROL_CONTINUE

    # Notifies service that it should return its current status information
    CONTROL_INTERROGATE = SERVICE_CONTROL_INTERROGATE

    # Notifies a service that its parameters have changed
    CONTROL_PARAMCHANGE = SERVICE_CONTROL_PARAMCHANGE

    # Notifies a service that there is a new component for binding
    CONTROL_NETBINDADD = SERVICE_CONTROL_NETBINDADD

    # Notifies a service that a component for binding has been removed
    CONTROL_NETBINDREMOVE = SERVICE_CONTROL_NETBINDREMOVE

    # Notifies a service that a component for binding has been enabled
    CONTROL_NETBINDENABLE = SERVICE_CONTROL_NETBINDENABLE

    # Notifies a service that a component for binding has been disabled
    CONTROL_NETBINDDISABLE = SERVICE_CONTROL_NETBINDDISABLE

    IDLE = 0

    # Misc
    IDLE_CONTROL_CODE = 0
    WAIT_OBJECT_0 = 0
    WAIT_TIMEOUT = 0x00000102
    WAIT_FAILED = 0xFFFFFFFF
    NO_ERROR = 0

    # Wraps SetServiceStatus.
    SetTheServiceStatus = Proc.new do |dwCurrentState, dwWin32ExitCode, dwCheckPoint, dwWaitHint|
      ss = SERVICE_STATUS.new # Current status of the service.

      # Disable control requests until the service is started.
      if dwCurrentState == SERVICE_START_PENDING
        ss[:dwControlsAccepted] = 0
      else
        ss[:dwControlsAccepted] =
          SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN |
          SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_PARAMCHANGE
      end

      # Initialize ss structure.
      ss[:dwServiceType]             = SERVICE_WIN32_OWN_PROCESS
      ss[:dwServiceSpecificExitCode] = 0
      ss[:dwCurrentState]            = dwCurrentState
      ss[:dwWin32ExitCode]           = dwWin32ExitCode
      ss[:dwCheckPoint]              = dwCheckPoint
      ss[:dwWaitHint]                = dwWaitHint

      @@dwServiceState = dwCurrentState

      # Send status of the service to the Service Controller.
      if !SetServiceStatus(@@ssh, ss)
        SetEvent(@@hStopEvent)
      end
    end

    ERROR_CALL_NOT_IMPLEMENTED = 0x78

    # Handles control signals from the service control manager.
    Service_Ctrl_ex = Proc.new do |dwCtrlCode, _dwEventType, _lpEventData, _lpContext|
      @@waiting_control_code = dwCtrlCode;
      return_value = NO_ERROR

      begin
        dwState = SERVICE_RUNNING

        case dwCtrlCode
        when SERVICE_CONTROL_STOP
          dwState = SERVICE_STOP_PENDING
        when SERVICE_CONTROL_SHUTDOWN
          dwState = SERVICE_STOP_PENDING
        when SERVICE_CONTROL_PAUSE
          dwState = SERVICE_PAUSED
        when SERVICE_CONTROL_CONTINUE
          dwState = SERVICE_RUNNING
          # else
          # TODO: Handle other control codes? Retain the current state?
        end

        # Set the status of the service except on interrogation.
        unless dwCtrlCode == SERVICE_CONTROL_INTERROGATE
          SetTheServiceStatus.call(dwState, NO_ERROR, 0, 0)
        end

        # Tell service_main thread to stop.
        if dwCtrlCode == SERVICE_CONTROL_STOP || dwCtrlCode == SERVICE_CONTROL_SHUTDOWN
          if SetEvent(@@hStopEvent) == 0
            SetTheServiceStatus.call(SERVICE_STOPPED, FFI.errno, 0, 0)
          end
        end
      rescue
        return_value = ERROR_CALL_NOT_IMPLEMENTED
      end

      return_value
    end

    # Called by the service control manager after the call to StartServiceCtrlDispatcher.
    Service_Main = FFI::Function.new(:void, [:ulong, :pointer], :blocking => false) do |dwArgc, lpszArgv|
      begin
        # Obtain the name of the service.
        if lpszArgv.address != 0
          argv = lpszArgv.get_array_of_string(0, dwArgc)
          lpszServiceName = argv[0]
        else
          lpszServiceName = ''
        end

        # Args passed to Service.start
        if (dwArgc > 1)
          @@Argv = argv[1..]
        else
          @@Argv = nil
        end

        # Register the service ctrl handler.
        @@ssh = RegisterServiceCtrlHandlerExW(
          lpszServiceName,
          Service_Ctrl_ex,
          nil
        )

        # No service to stop, no service handle to notify, nothing to do but exit.
        break if @@ssh == 0

        # The service has started.
        SetTheServiceStatus.call(SERVICE_RUNNING, NO_ERROR, 0, 0)

        SetEvent(@@hStartEvent)

        # Main loop for the service.
        while (WaitForSingleObject(@@hStopEvent, 1000) != WAIT_OBJECT_0) do
        end

        # Main loop for the service.
        while (WaitForSingleObject(@@hStopCompletedEvent, 1000) != WAIT_OBJECT_0) do
        end
      ensure
        # Stop the service.
        SetTheServiceStatus.call(SERVICE_STOPPED, NO_ERROR, 0, 0)
      end
    end

    ThreadProc = FFI::Function.new(:ulong, [:pointer]) do |lpParameter|
      ste = FFI::MemoryPointer.new(SERVICE_TABLE_ENTRYW, 2)

      s = SERVICE_TABLE_ENTRYW.new(ste[0])
      s[:lpServiceName] = FFI::MemoryPointer.from_string('')
      s[:lpServiceProc] = lpParameter

      s = SERVICE_TABLE_ENTRYW.new(ste[1])
      s[:lpServiceName] = nil
      s[:lpServiceProc] = nil

      # No service to step, no service handle, no ruby exceptions, just terminate the thread..
      if !StartServiceCtrlDispatcherW(ste)
        return 1
      end

      return 0
    end

    # This is a shortcut for Daemon.new + Daemon#mainloop.
    #
    def self.mainloop
      self.new.mainloop
    end

    # This is the method that actually puts your code into a loop and allows it
    # to run as a service.  The code that is actually run while in the mainloop
    # is what you defined in your own Daemon#service_main method.
    #
    def mainloop
      @@waiting_control_code = IDLE_CONTROL_CODE
      @@dwServiceState = 0

      # Redirect STDIN, STDOUT and STDERR to the NUL device if they're still
      # associated with a tty. This helps newbs avoid Errno::EBADF errors.
      STDIN.reopen('NUL') if STDIN.isatty
      STDOUT.reopen('NUL') if STDOUT.isatty
      STDERR.reopen('NUL') if STDERR.isatty

      # Calling init here so that init failures never even tries to start the
      # service. Of course that means that init methods must be very quick
      # because the SCM will be receiving no START_PENDING messages while
      # init's running.
      #
      # TODO: Fix?
      service_init() if respond_to?('service_init')

      # Create the event to signal the service to start.
      @@hStartEvent = CreateEventW(nil, 1, 0, nil)

      if @@hStartEvent == 0
        raise SystemCallError.new('CreateEvent', FFI.errno)
      end

      # Create the event to signal the service to stop.
      @@hStopEvent = CreateEventW(nil, 1, 0, nil)

      if @@hStopEvent == 0
        raise SystemCallError.new('CreateEvent', FFI.errno)
      end

      # Create the event to signal the service that stop has completed
      @@hStopCompletedEvent = CreateEventW(nil, 1, 0, nil)

      if @@hStopCompletedEvent == 0
        raise SystemCallError.new('CreateEvent', FFI.errno)
      end

      hThread = CreateThread(nil, 0, ThreadProc, Service_Main, 0, nil)

      if hThread == 0
        raise SystemCallError.new('CreateThread', FFI.errno)
      end

      events = FFI::MemoryPointer.new(:pointer, 2)
      events.put_pointer(0, FFI::Pointer.new(hThread))
      events.put_pointer(FFI::Pointer.size, FFI::Pointer.new(@@hStartEvent))

      while ((index = WaitForMultipleObjects(2, events, 0, 1000)) == WAIT_TIMEOUT) do
      end

      if index == WAIT_FAILED
        raise SystemCallError.new('WaitForMultipleObjects', FFI.errno)
      end

      # The thread exited, so the show is off.
      if index == WAIT_OBJECT_0
        raise "Service_Main thread exited abnormally"
      end

      thr = Thread.new do
        begin
          while (WaitForSingleObject(@@hStopEvent, 1000) == WAIT_TIMEOUT)
            # Check to see if anything interesting has been signaled
            case @@waiting_control_code
            when SERVICE_CONTROL_PAUSE
              service_pause() if respond_to?('service_pause')
            when SERVICE_CONTROL_CONTINUE
              service_resume() if respond_to?('service_resume')
            when SERVICE_CONTROL_INTERROGATE
              service_interrogate() if respond_to?('service_interrogate')
            when SERVICE_CONTROL_SHUTDOWN
              service_shutdown() if respond_to?('service_shutdown')
            when SERVICE_CONTROL_PARAMCHANGE
              service_paramchange() if respond_to?('service_paramchange')
            when SERVICE_CONTROL_NETBINDADD
              service_netbindadd() if respond_to?('service_netbindadd')
            when SERVICE_CONTROL_NETBINDREMOVE
              service_netbindremove() if respond_to?('service_netbindremove')
            when SERVICE_CONTROL_NETBINDENABLE
              service_netbindenable() if respond_to?('service_netbindenable')
            when SERVICE_CONTROL_NETBINDDISABLE
              service_netbinddisable() if respond_to?('service_netbinddisable')
            end
            @@waiting_control_code = IDLE_CONTROL_CODE
          end

          service_stop() if respond_to?('service_stop')
        ensure
          SetEvent(@@hStopCompletedEvent)
        end
      end

      if respond_to?('service_main')
        service_main(*@@Argv)
      end

      thr.join
    end

    # Returns the state of the service (as an constant integer) which can be any
    # of the service status constants, e.g. RUNNING, PAUSED, etc.
    #
    # This method is typically used within your service_main method to setup the
    # loop. For example:
    #
    #   class MyDaemon < Daemon
    #     def service_main
    #       while state == RUNNING || state == PAUSED || state == IDLE
    #         # Your main loop here
    #       end
    #     end
    #   end
    #
    # See the Daemon#running? method for an abstraction of the above code.
    #
    def state
      @@dwServiceState
    end

    #
    # Returns whether or not the service is in a running state, i.e. the service
    # status is either RUNNING, PAUSED or IDLE.
    #
    # This is typically used within your service_main method to setup the main
    # loop. For example:
    #
    #    class MyDaemon < Daemon
    #       def service_main
    #          while running?
    #             # Your main loop here
    #          end
    #       end
    #    end
    #
    def running?
      [SERVICE_RUNNING, SERVICE_PAUSED, 0].include?(@@dwServiceState)
    end
  end
end
