# Command Line Interface Handler for Cryload
module Cryload
  class Cli
    def initialize
      @options = Options.new
      @show_help = false
      @show_version = false
      @parse_error = false
      prepare_op

      if @show_version
        puts "cryload #{Cryload::VERSION}"
        exit 0
      end

      if @show_help
        exit(@parse_error ? 1 : 0)
      end

      unless Validator.validate(@options) { |message| print_start_message(message) }
        exit 1
      end

      connections = @options.connections
      urls = OptionsBuilder.resolve_urls(@options)
      display_url = Cryload.display_url(urls)
      output_format = OptionsBuilder.resolve_output_format(@options)
      method = @options.method
      body = OptionsBuilder.resolve_body(@options)
      timeout_seconds = @options.timeout
      rate_limit = @options.rate
      insecure = @options.insecure?
      follow_redirects = @options.follow_redirects?
      success_status_ranges = Validator.parse_success_status_ranges(@options.success_status)
      headers = OptionsBuilder.build_headers(@options)
      ci_thresholds = OptionsBuilder.build_ci_thresholds(@options)
      warmup_seconds = @options.warmup || 0
      progress = @options.progress?
      random_path = @options.random_path?
      disable_keepalive = @options.disable_keepalive?
      proxy = OptionsBuilder.resolve_proxy(@options)

      if duration = @options.duration
        Cryload::LoadGenerator.new display_url, nil, connections, duration, output_format, method, body, headers, timeout_seconds, insecure, rate_limit, follow_redirects, success_status_ranges, ci_thresholds, urls, warmup_seconds, proxy, progress, random_path, disable_keepalive
      elsif numbers = @options.numbers
        Cryload::LoadGenerator.new display_url, numbers, connections, nil, output_format, method, body, headers, timeout_seconds, insecure, rate_limit, follow_redirects, success_status_ranges, ci_thresholds, urls, warmup_seconds, proxy, progress, random_path, disable_keepalive
      end
    end

    private def prepare_op
      begin
        OptionParser.parse(ARGV) do |opts|
          opts.banner = "Cross-platform HTTP load testing CLI: a modern ab/wrk alternative with machine-readable reports for CI/CD\n\nUsage: cryload <url> [options]"

          opts.on("-n NUMBERS", "--numbers NUMBERS", "Number of requests to make") do |v|
            @options.numbers = parse_int(v, "-n/--numbers")
          end

          opts.on("-c CONNECTIONS", "--connections CONNECTIONS", "Number of concurrent connections (default: 10)") do |v|
            @options.connections = parse_int(v, "-c/--connections")
          end

          opts.on("-d SECONDS", "--duration SECONDS", "Duration of test in seconds (e.g. -d 10 for 10 seconds)") do |v|
            @options.duration = parse_int(v, "-d/--duration")
          end

          opts.on("-m METHOD", "--method METHOD", "HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)") do |v|
            @options.method = v.upcase
          end

          opts.on("-b BODY", "--body BODY", "HTTP request body") do |v|
            @options.body = v
          end

          opts.on("--body-file PATH", "Read HTTP request body from file") do |v|
            @options.body_file = v
          end

          opts.on("--body-stdin", "Read HTTP request body from standard input") do
            @options.body_stdin = true
          end

          opts.on("-H HEADER", "--header HEADER", "HTTP header, repeatable (e.g. -H 'Authorization: Bearer token')") do |v|
            @options.headers << v
          end

          opts.on("--user-agent VALUE", "Set the User-Agent header") do |v|
            @options.user_agent = v
          end

          opts.on("--host-header VALUE", "Override the Host header") do |v|
            @options.host_header = v
          end

          opts.on("-a USERPASS", "--basic-auth USERPASS", "HTTP Basic auth in the form 'user:password'") do |v|
            @options.basic_auth = v
          end

          opts.on("--timeout SECONDS", "Client connect/read timeout in seconds") do |v|
            @options.timeout = parse_int(v, "--timeout")
          end

          opts.on("-q RATE", "--rate RATE", "Total request rate limit in requests/sec") do |v|
            @options.rate = parse_int(v, "-q/--rate")
          end

          opts.on("-L", "--follow-redirects", "Follow HTTP redirects (up to 5 hops)") do
            @options.follow_redirects = true
          end

          opts.on("--disable-keepalive", "Open a new connection for every request (sends 'Connection: close')") do
            @options.disable_keepalive = true
          end

          opts.on("--output-format FORMAT", "Output format: text, json, csv, quiet") do |v|
            @options.output_format = v.downcase
          end

          opts.on("--success-status CODES", "Successful status codes/ranges (e.g. 200-299,301,304)") do |v|
            @options.success_status = v
          end

          opts.on("--insecure", "Accept invalid TLS certificates (HTTPS only)") do
            @options.insecure = true
          end

          opts.on("--json", "Output final results as JSON") do
            @options.json = true
          end

          opts.on("--fail-on-error", "Exit with code 1 when any HTTP or transport error occurs") do
            @options.fail_on_error = true
          end

          opts.on("--fail-on-transport-error", "Exit with code 1 when any transport error occurs") do
            @options.fail_on_transport_error = true
          end

          opts.on("--max-fail-rate PERCENT", "Exit with code 1 when failure rate exceeds PERCENT") do |v|
            @options.max_fail_rate = parse_float(v, "--max-fail-rate")
          end

          opts.on("--max-p99 MS", "Exit with code 1 when p99 latency exceeds MS milliseconds") do |v|
            @options.max_p99_ms = parse_float(v, "--max-p99")
          end

          opts.on("--warmup SECONDS", "Warm up before the timed benchmark (seconds)") do |v|
            @options.warmup = parse_int(v, "--warmup")
          end

          opts.on("--proxy URL", "HTTP(S) proxy (e.g. http://127.0.0.1:8080 or http://user:pass@proxy:8080)") do |v|
            @options.proxy = v
          end

          opts.on("--no-progress", "Disable live progress on stderr") do
            @options.progress = false
          end

          opts.on("--progress", "Show live progress on stderr during the run (default)") do
            @options.progress = true
          end

          opts.on("--cookie COOKIE", "Cookie value, repeatable (name=value)") do |v|
            @options.cookies << v
          end

          opts.on("--urls-file PATH", "Load target URLs from file (one http(s) URL per line, # comments allowed)") do |v|
            @options.urls_file = v
          end

          opts.on("--random-path", "Append a random path segment to each request URL") do
            @options.random_path = true
          end

          opts.on("-h", "--help", "Print Help") do
            puts opts
            @show_help = true
          end

          opts.on("-V", "--version", "Print version") do
            @show_version = true
          end

          if ARGV.empty?
            puts opts
            @show_help = true
          end
        end
      rescue ex : OptionParser::Exception
        STDERR.puts ex.message.to_s.colorize(:red)
        STDERR.puts "Try 'cryload -h' for usage.".colorize(:red)
        @show_help = true
        @parse_error = true
      end

      if (url = ARGV[0]?) && !url.starts_with?("-")
        @options.server = url
      end
    end

    private def parse_int(value : String, flag : String) : Int32
      value.to_i? || raise OptionParser::Exception.new("Invalid value '#{value}' for #{flag}: expected an integer")
    end

    private def parse_float(value : String, flag : String) : Float64
      value.to_f? || raise OptionParser::Exception.new("Invalid value '#{value}' for #{flag}: expected a number")
    end

    private def print_start_message(message : String)
      return unless OptionsBuilder.resolve_output_format(@options) == "text"
      puts message.colorize(:green)
    end
  end
end
