module Invoker
  # Class is responsible for managing all the processes Invoker is supposed
  # to manage. Takes care of starting, stopping and restarting processes.
  class ProcessManager
    LABEL_COLORS = [:green, :yellow, :blue, :magenta, :cyan]
    attr_accessor :open_pipes, :workers

    def initialize
      @open_pipes = {}
      @workers = {}
      @worker_mutex = Mutex.new
      @thread_group = ThreadGroup.new
    end

    def start_process(process_info)
      m, s = PTY.open
      s.raw! # disable newline conversion.

      pid = run_command(process_info, s)

      s.close

      worker = CommandWorker.new(process_info.label, m, pid, select_color)

      add_worker(worker)
      wait_on_pid(process_info.label, pid)
    end

    # Start a process given their name
    # @param process_name [String] Command label of process specified in config file.
    def start_process_by_name(process_name)
      if process_running?(process_name)
        Invoker::Logger.puts "\nProcess '#{process_name}' is already running".colorize(:red)
        return false
      end

      process_info = Invoker.config.process(process_name)
      start_process(process_info) if process_info
    end

    # Remove a process from list of processes managed by invoker supervisor.It also
    # kills the process before removing it from the list.
    #
    # @param remove_message [Invoker::IPC::Message::Remove]
    # @return [Boolean] if process existed and was removed else false
    def stop_process(remove_message)
      worker = workers[remove_message.process_name]
      command_label = remove_message.process_name
      return false unless worker
      signal_to_use = remove_message.signal || 'INT'

      Invoker::Logger.puts("Removing #{command_label} with signal #{signal_to_use}".colorize(:red))
      kill_or_remove_process(worker.pid, signal_to_use, command_label)
    end

    # Receive a message from user to restart a Process
    # @param [Invoker::IPC::Message::Reload]
    def restart_process(reload_message)
      command_label = reload_message.process_name
      if stop_process(reload_message.remove_message)
        Invoker.commander.schedule_event(command_label, :worker_removed) do
          start_process_by_name(command_label)
        end
      else
        start_process_by_name(command_label)
      end
    end

    def run_power_server
      return unless Invoker.can_run_balancer?(false)

      powerup_id = Invoker::Power::Powerup.fork_and_start
      wait_on_pid("powerup_manager", powerup_id)
      at_exit do
        begin
          Process.kill("INT", powerup_id)
        rescue Errno::ESRCH; end
      end
    end

    # Given a file descriptor returns the worker object
    #
    # @param fd [IO] an IO object with valid file descriptor
    # @return [Invoker::CommandWorker] The worker object which is associated with this fd
    def get_worker_from_fd(fd)
      open_pipes[fd.fileno]
    end

    def load_env(directory = nil)
      directory ||= ENV['PWD']

      if !directory || directory.empty? || !Dir.exist?(directory)
        return {}
      end

      default_env = File.join(directory, '.env')
      local_env = File.join(directory, '.env.local')
      env = {}

      if File.exist?(default_env)
        env.merge!(Dotenv::Environment.new(default_env))
      end

      if File.exist?(local_env)
        env.merge!(Dotenv::Environment.new(local_env))
      end

      env
    end

    def kill_workers
      @workers.each do |key, worker|
        kill_or_remove_process(worker.pid, "INT", worker.command_label)
      end
      @workers = {}
    end

    # List currently running commands
    def process_list
      Invoker::IPC::Message::ListResponse.from_workers(workers)
    end

    private

    def wait_on_pid(command_label, pid)
      raise Invoker::Errors::ToomanyOpenConnections if @thread_group.enclosed?

      thread = Thread.new do
        Process.wait(pid)
        message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
        Invoker::Logger.puts("\n#{message}".colorize(:red))
        Invoker.notify_user(message)
        Invoker.commander.trigger(command_label, :exit)
      end
      @thread_group.add(thread)
    end

    def select_color
      selected_color = LABEL_COLORS.shift
      LABEL_COLORS.push(selected_color)
      selected_color
    end

    def process_running?(command_label)
      !!workers[command_label]
    end

    def kill_or_remove_process(pid, signal_to_use, command_label)
      process_kill(pid, signal_to_use)
      true
    rescue Errno::ESRCH
      Invoker::Logger.puts("Killing process with #{pid} and name #{command_label} failed".colorize(:red))
      remove_worker(command_label, false)
      false
    end

    def process_kill(pid, signal_to_use)
      if signal_to_use.to_i == 0
        Process.kill(signal_to_use, -Process.getpgid(pid))
      else
        Process.kill(signal_to_use.to_i, -Process.getpgid(pid))
      end
    end

    # Remove worker from all collections
    def remove_worker(command_label, trigger_event = true)
      worker = @workers[command_label]
      if worker
        @open_pipes.delete(worker.pipe_end.fileno)
        @workers.delete(command_label)
        # Move label color to front of array so it's reused first
        LABEL_COLORS.delete(worker.color)
        LABEL_COLORS.unshift(worker.color)
      end
      if trigger_event
        Invoker.commander.trigger(command_label, :worker_removed)
      end
    end

    # add worker to global collections
    def add_worker(worker)
      @open_pipes[worker.pipe_end.fileno] = worker
      @workers[worker.command_label] = worker
      Invoker.commander.watch_for_read(worker.pipe_end)
    end

    def run_command(process_info, write_pipe)
      command_label = process_info.label

      Invoker.commander.schedule_event(command_label, :exit) { remove_worker(command_label) }

      env_options = load_env(process_info.dir)

      spawn_options = {
        :chdir => process_info.dir || ENV['PWD'], :out => write_pipe, :err => write_pipe,
        :pgroup => true, :close_others => true, :in => :close
      }
      Invoker.run_without_bundler { spawn(env_options, process_info.cmd, spawn_options) }
    end
  end
end
