# frozen_string_literal: true

require 'optparse'
require_relative 'state_file'
require_relative 'const'
require_relative 'detect'
require_relative 'configuration'
require 'uri'
require 'socket'

module Puma
  class ControlCLI

    COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats}

    def initialize(argv, stdout=STDOUT, stderr=STDERR)
      @state = nil
      @quiet = false
      @pidfile = nil
      @pid = nil
      @control_url = nil
      @control_auth_token = nil
      @config_file = nil
      @command = nil
      @environment = ENV['RACK_ENV']

      @argv = argv.dup
      @stdout = stdout
      @stderr = stderr
      @cli_options = {}

      opts = OptionParser.new do |o|
        o.banner = "Usage: pumactl (-p PID | -P pidfile | -S status_file | -C url -T token | -F config.rb) (#{COMMANDS.join("|")})"

        o.on "-S", "--state PATH", "Where the state file to use is" do |arg|
          @state = arg
        end

        o.on "-Q", "--quiet", "Not display messages" do |arg|
          @quiet = true
        end

        o.on "-P", "--pidfile PATH", "Pid file" do |arg|
          @pidfile = arg
        end

        o.on "-p", "--pid PID", "Pid" do |arg|
          @pid = arg.to_i
        end

        o.on "-C", "--control-url URL", "The bind url to use for the control server" do |arg|
          @control_url = arg
        end

        o.on "-T", "--control-token TOKEN", "The token to use as authentication for the control server" do |arg|
          @control_auth_token = arg
        end

        o.on "-F", "--config-file PATH", "Puma config script" do |arg|
          @config_file = arg
        end

        o.on "-e", "--environment ENVIRONMENT",
          "The environment to run the Rack app on (default development)" do |arg|
          @environment = arg
        end

        o.on_tail("-H", "--help", "Show this message") do
          @stdout.puts o
          exit
        end

        o.on_tail("-V", "--version", "Show version") do
          puts Const::PUMA_VERSION
          exit
        end
      end

      opts.order!(argv) { |a| opts.terminate a }
      opts.parse!

      @command = argv.shift

      unless @config_file == '-'
        environment = @environment || 'development'

        if @config_file.nil?
          @config_file = %W(config/puma/#{environment}.rb config/puma.rb).find do |f|
            File.exist?(f)
          end
        end

        if @config_file
          config = Puma::Configuration.new({ config_files: [@config_file] }, {})
          config.load
          @state              ||= config.options[:state]
          @control_url        ||= config.options[:control_url]
          @control_auth_token ||= config.options[:control_auth_token]
          @pidfile            ||= config.options[:pidfile]
        end
      end

      # check present of command
      unless @command
        raise "Available commands: #{COMMANDS.join(", ")}"
      end

      unless COMMANDS.include? @command
        raise "Invalid command: #{@command}"
      end

    rescue => e
      @stdout.puts e.message
      exit 1
    end

    def message(msg)
      @stdout.puts msg unless @quiet
    end

    def prepare_configuration
      if @state
        unless File.exist? @state
          raise "State file not found: #{@state}"
        end

        sf = Puma::StateFile.new
        sf.load @state

        @control_url = sf.control_url
        @control_auth_token = sf.control_auth_token
        @pid = sf.pid
      elsif @pidfile
        # get pid from pid_file
        File.open(@pidfile) { |f| @pid = f.read.to_i }
      end
    end

    def send_request
      uri = URI.parse @control_url

      # create server object by scheme
      server = case uri.scheme
                when "ssl"
                  require 'openssl'
                  OpenSSL::SSL::SSLSocket.new(
                    TCPSocket.new(uri.host, uri.port),
                    OpenSSL::SSL::SSLContext.new
                  ).tap(&:connect)
                when "tcp"
                  TCPSocket.new uri.host, uri.port
                when "unix"
                  UNIXSocket.new "#{uri.host}#{uri.path}"
                else
                  raise "Invalid scheme: #{uri.scheme}"
                end

      if @command == "status"
        message "Puma is started"
      else
        url = "/#{@command}"

        if @control_auth_token
          url = url + "?token=#{@control_auth_token}"
        end

        server << "GET #{url} HTTP/1.0\r\n\r\n"

        unless data = server.read
          raise "Server closed connection before responding"
        end

        response = data.split("\r\n")

        if response.empty?
          raise "Server sent empty response"
        end

        (@http,@code,@message) = response.first.split(" ",3)

        if @code == "403"
          raise "Unauthorized access to server (wrong auth token)"
        elsif @code == "404"
          raise "Command error: #{response.last}"
        elsif @code != "200"
          raise "Bad response from server: #{@code}"
        end

        message "Command #{@command} sent success"
        message response.last if @command == "stats" || @command == "gc-stats"
      end
    ensure
      server.close if server && !server.closed?
    end

    def send_signal
      unless @pid
        raise "Neither pid nor control url available"
      end

      begin

        case @command
        when "restart"
          Process.kill "SIGUSR2", @pid

        when "halt"
          Process.kill "QUIT", @pid

        when "stop"
          Process.kill "SIGTERM", @pid

        when "stats"
          puts "Stats not available via pid only"
          return

        when "reload-worker-directory"
          puts "reload-worker-directory not available via pid only"
          return

        when "phased-restart"
          Process.kill "SIGUSR1", @pid

        when "status"
          begin
            Process.kill 0, @pid
            puts "Puma is started"
          rescue Errno::ESRCH
            raise "Puma is not running"
          end

          return

        else
          return
        end

      rescue SystemCallError
        if @command == "restart"
          start
        else
          raise "No pid '#{@pid}' found"
        end
      end

      message "Command #{@command} sent success"
    end

    def run
      return start if @command == "start"

      prepare_configuration

      if Puma.windows?
        send_request
      else
        @control_url ? send_request : send_signal
      end

    rescue => e
      message e.message
      exit 1
    end

  private
    def start
      require 'puma/cli'

      run_args = []

      run_args += ["-S", @state]  if @state
      run_args += ["-q"] if @quiet
      run_args += ["--pidfile", @pidfile] if @pidfile
      run_args += ["--control-url", @control_url] if @control_url
      run_args += ["--control-token", @control_auth_token] if @control_auth_token
      run_args += ["-C", @config_file] if @config_file
      run_args += ["-e", @environment] if @environment

      events = Puma::Events.new @stdout, @stderr

      # replace $0 because puma use it to generate restart command
      puma_cmd = $0.gsub(/pumactl$/, 'puma')
      $0 = puma_cmd if File.exist?(puma_cmd)

      cli = Puma::CLI.new run_args, events
      cli.run
    end
  end
end
