# Copyright 2020 Google LLC
#
# 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
#
#     https://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 "logger"
require "optparse"

require "functions_framework"

module FunctionsFramework
  ##
  # Implementation of the functions-framework-ruby executable.
  #
  class CLI
    ##
    # The default logging level, if not given in the environment variable.
    # @return [Integer]
    #
    DEFAULT_LOGGING_LEVEL = ::Logger::Severity::INFO

    ##
    # Create a new CLI, setting arguments to their defaults.
    #
    def initialize
      @target = ::ENV["FUNCTION_TARGET"] || ::FunctionsFramework::DEFAULT_TARGET
      @source = ::ENV["FUNCTION_SOURCE"] || ::FunctionsFramework::DEFAULT_SOURCE
      @env = nil
      @port = nil
      @pidfile = nil
      @bind = nil
      @min_threads = nil
      @max_threads = nil
      @detailed_errors = nil
      @signature_type = ::ENV["FUNCTION_SIGNATURE_TYPE"]
      @logging_level = init_logging_level
      @what_to_do = nil
      @error_message = nil
      @exit_code = 0
    end

    ##
    # Determine if an error has occurred
    #
    # @return [boolean]
    #
    def error?
      !@error_message.nil?
    end

    ##
    # @return [Integer] The current exit status.
    #
    attr_reader :exit_code

    ##
    # @return [String] The current error message.
    # @return [nil] if no error has occurred.
    #
    attr_reader :error_message

    ##
    # @return [String] The pidfile.
    # @return [nil] if not running.
    #
    attr_reader :pidfile

    ##
    # Parse the given command line arguments.
    # Exits if argument parsing failed.
    #
    # @param argv [Array<String>]
    # @return [self]
    #
    def parse_args argv # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
      @option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
        op.on "-t", "--target TARGET",
              "Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
          @target = val
        end
        op.on "-s", "--source SOURCE",
              "Set the source file to load (defaults to #{DEFAULT_SOURCE})" do |val|
          @source = val
        end
        op.on "--signature-type TYPE",
              "Asserts that the function has the given signature type. " \
              "Supported values are 'http' and 'cloudevent'." do |val|
          @signature_type = val
        end
        op.on "-P", "--pidfile PIDFILE", "Set the pidfile for the server (defaults to puma.pid)" do |val|
          @pidfile = val
        end
        op.on "-p", "--port PORT", "Set the port to listen to (defaults to 8080)" do |val|
          @port = val.to_i
        end
        op.on "-b", "--bind BIND", "Set the address to bind to (defaults to 0.0.0.0)" do |val|
          @bind = val
        end
        op.on "-e", "--environment ENV", "Set the Rack environment" do |val|
          @env = val
        end
        op.on "--min-threads NUM", "Set the minimum thread pool size" do |val|
          @min_threads = val
        end
        op.on "--max-threads NUM", "Set the maximum thread pool size" do |val|
          @max_threads = val
        end
        op.on "--[no-]detailed-errors", "Set whether to show error details" do |val|
          @detailed_errors = val
        end
        op.on "--verify", "Verify the app only, but do not run the server." do
          @what_to_do ||= :verify
        end
        op.on "-v", "--verbose", "Increase log verbosity" do
          @logging_level -= 1
        end
        op.on "-q", "--quiet", "Decrease log verbosity" do
          @logging_level += 1
        end
        op.on "--version", "Display the framework version" do
          @what_to_do ||= :version
        end
        op.on "--help", "Display help" do
          @what_to_do ||= :help
        end
      end
      begin
        @option_parser.parse! argv
        error! "Unrecognized arguments: #{argv}\n#{@option_parser}", 2 unless argv.empty?
      rescue ::OptionParser::ParseError => e
        error! "#{e.message}\n#{@option_parser}", 2
      end
      self
    end

    ##
    # Perform the requested function.
    #
    #  *  If the `--version` flag was given, display the version.
    #  *  If the `--help` flag was given, display online help.
    #  *  If the `--verify` flag was given, load and verify the function,
    #     displaying any errors, then exit without starting a server.
    #  *  Otherwise, start the configured server and block until it stops.
    #
    # @return [self]
    #
    def run
      return self if error?
      case @what_to_do
      when :version
        puts ::FunctionsFramework::VERSION
      when :help
        puts @option_parser
      when :verify
        begin
          load_function
          puts "OK"
        rescue ::StandardError => e
          error! e.message
        end
      else
        begin
          start_server.wait_until_stopped
        rescue ::StandardError => e
          error! e.message
        end
      end
      self
    end

    ##
    # Finish the CLI, displaying any error status and exiting with the current
    # exit code. Never returns.
    #
    def complete
      warn @error_message if @error_message
      exit @exit_code
    end

    ##
    # Load the source and get and verify the requested function.
    # If a validation error occurs, raise an exception.
    #
    # @return [FunctionsFramework::Function]
    #
    # @private
    #
    def load_function
      ::FunctionsFramework.logger.level = @logging_level
      ::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION}"
      ::ENV["FUNCTION_TARGET"] = @target
      ::ENV["FUNCTION_SOURCE"] = @source
      ::ENV["FUNCTION_SIGNATURE_TYPE"] = @signature_type
      ::FunctionsFramework.logger.info "FunctionsFramework: Loading functions from #{@source.inspect}..."
      load @source
      ::FunctionsFramework.logger.info "FunctionsFramework: Looking for function name #{@target.inspect}..."
      function = ::FunctionsFramework.global_registry[@target]
      raise "Undefined function: #{@target.inspect}" if function.nil?
      unless @signature_type.nil? ||
             (@signature_type == "http" && function.type == :http) ||
             (@signature_type == "http" && function.type == :typed) ||
             (["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event)
        raise "Function #{@target.inspect} does not match type #{@signature_type}"
      end
      function
    end

    ##
    # Start the configured server and return the running server object.
    # If a validation error occurs, raise an exception.
    #
    # @return [FunctionsFramework::Server]
    #
    # @private
    #
    def start_server
      function = load_function
      ::FunctionsFramework.logger.info "FunctionsFramework: Starting server..."
      ::FunctionsFramework.start function do |config|
        config.rack_env = @env
        config.port = @port
        config.pidfile = @pidfile
        config.bind_addr = @bind
        config.show_error_details = @detailed_errors
        config.min_threads = @min_threads
        config.max_threads = @max_threads
      end
    end

    private

    def init_logging_level
      level_name = ::ENV["FUNCTION_LOGGING_LEVEL"].to_s.upcase.to_sym
      ::Logger::Severity.const_get level_name
    rescue ::NameError
      DEFAULT_LOGGING_LEVEL
    end

    ##
    # Set the error status.
    # @param message [String] Error message.
    # @param code [Integer] Exit code, defaults to 1.
    #
    def error! message, code = 1
      @error_message = message
      @exit_code = code
    end
  end
end
