# frozen_string_literal: true
module GoodJob
  #
  # +GoodJob::Configuration+ provides normalized configuration information to
  # the rest of GoodJob. It combines environment information with explicitly
  # set options to get the final values for each option.
  #
  class Configuration
    # Valid execution modes.
    EXECUTION_MODES = [:async, :async_all, :async_server, :external, :inline].freeze
    # Default number of threads to use per {Scheduler}
    DEFAULT_MAX_THREADS = 5
    # Default number of seconds between polls for jobs
    DEFAULT_POLL_INTERVAL = 10
    # Default poll interval for async in development environment
    DEFAULT_DEVELOPMENT_ASYNC_POLL_INTERVAL = -1
    # Default number of threads to use per {Scheduler}
    DEFAULT_MAX_CACHE = 10000
    # Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs} and {GoodJob.cleanup_preserved_jobs}
    DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
    # Default number of jobs to execute between preserved job cleanup runs
    DEFAULT_CLEANUP_INTERVAL_JOBS = nil
    # Default number of seconds to wait between preserved job cleanup runs
    DEFAULT_CLEANUP_INTERVAL_SECONDS = nil
    # Default to always wait for jobs to finish for {Adapter#shutdown}
    DEFAULT_SHUTDOWN_TIMEOUT = -1
    # Default to not running cron
    DEFAULT_ENABLE_CRON = false

    # The options that were explicitly set when initializing +Configuration+.
    # @return [Hash]
    attr_reader :options

    # The environment from which to read GoodJob's environment variables. By
    # default, this is the current process's environment, but it can be set
    # to something else in {#initialize}.
    # @return [Hash]
    attr_reader :env

    # @param options [Hash] Any explicitly specified configuration options to
    #   use. Keys are symbols that match the various methods on this class.
    # @param env [Hash] A +Hash+ from which to read environment variables that
    #   might specify additional configuration values.
    def initialize(options, env: ENV)
      @options = options
      @env = env
    end

    def validate!
      raise ArgumentError, "GoodJob execution mode must be one of #{EXECUTION_MODES.join(', ')}. It was '#{execution_mode}' which is not valid." unless execution_mode.in?(EXECUTION_MODES)
    end

    # Specifies how and where jobs should be executed. See {Adapter#initialize}
    # for more details on possible values.
    # @return [Symbol]
    def execution_mode
      mode = if GoodJob::CLI.within_exe?
               :external
             else
               options[:execution_mode] ||
                 rails_config[:execution_mode] ||
                 env['GOOD_JOB_EXECUTION_MODE']
             end

      if mode
        mode.to_sym
      elsif Rails.env.development?
        :async
      elsif Rails.env.test?
        :inline
      else
        :external
      end
    end

    # Indicates the number of threads to use per {Scheduler}. Note that
    # {#queue_string} may provide more specific thread counts to use with
    # individual schedulers.
    # @return [Integer]
    def max_threads
      (
        options[:max_threads] ||
          rails_config[:max_threads] ||
          env['GOOD_JOB_MAX_THREADS'] ||
          env['RAILS_MAX_THREADS'] ||
          DEFAULT_MAX_THREADS
      ).to_i
    end

    # Describes which queues to execute jobs from and how those queues should
    # be grouped into {Scheduler} instances. See
    # {file:README.md#optimize-queues-threads-and-processes} for more details
    # on the format of this string.
    # @return [String]
    def queue_string
      options[:queues].presence ||
        rails_config[:queues].presence ||
        env['GOOD_JOB_QUEUES'].presence ||
        '*'
    end

    # The number of seconds between polls for jobs. GoodJob will execute jobs
    # on queues continuously until a queue is empty, at which point it will
    # poll (using this interval) for new queued jobs to execute.
    # @return [Integer]
    def poll_interval
      interval = (
        options[:poll_interval] ||
          rails_config[:poll_interval] ||
          env['GOOD_JOB_POLL_INTERVAL']
      )

      if interval
        interval.to_i
      elsif Rails.env.development? && execution_mode.in?([:async, :async_all, :async_server])
        DEFAULT_DEVELOPMENT_ASYNC_POLL_INTERVAL
      else
        DEFAULT_POLL_INTERVAL
      end
    end

    # The maximum number of future-scheduled jobs to store in memory.
    # Storing future-scheduled jobs in memory reduces execution latency
    # at the cost of increased memory usage. 10,000 stored jobs = ~20MB.
    # @return [Integer]
    def max_cache
      (
        options[:max_cache] ||
          rails_config[:max_cache] ||
          env['GOOD_JOB_MAX_CACHE'] ||
          DEFAULT_MAX_CACHE
      ).to_i
    end

    # The number of seconds to wait for jobs to finish when shutting down
    # before stopping the thread. +-1+ is forever.
    # @return [Numeric]
    def shutdown_timeout
      (
        options[:shutdown_timeout] ||
          rails_config[:shutdown_timeout] ||
          env['GOOD_JOB_SHUTDOWN_TIMEOUT'] ||
          DEFAULT_SHUTDOWN_TIMEOUT
      ).to_f
    end

    # Whether to run cron
    # @return [Boolean]
    def enable_cron
      value = ActiveModel::Type::Boolean.new.cast(
        options[:enable_cron] ||
          rails_config[:enable_cron] ||
          env['GOOD_JOB_ENABLE_CRON'] ||
          false
      )
      value && cron.size.positive?
    end

    alias enable_cron? enable_cron

    def cron
      env_cron = JSON.parse(ENV['GOOD_JOB_CRON'], symbolize_names: true) if ENV['GOOD_JOB_CRON'].present?

      options[:cron] ||
        rails_config[:cron] ||
        env_cron ||
        {}
    end

    def cron_entries
      cron.map { |cron_key, params| GoodJob::CronEntry.new(params.merge(key: cron_key)) }
    end

    # Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
    # This configuration is only used when {GoodJob.preserve_job_records} is +true+.
    # @return [Integer]
    def cleanup_preserved_jobs_before_seconds_ago
      (
        options[:before_seconds_ago] ||
          rails_config[:cleanup_preserved_jobs_before_seconds_ago] ||
          env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
          DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
      ).to_i
    end

    # Number of jobs a {Scheduler} will execute before cleaning up preserved jobs.
    # @return [Integer, nil]
    def cleanup_interval_jobs
      value = (
        rails_config[:cleanup_interval_jobs] ||
          env['GOOD_JOB_CLEANUP_INTERVAL_JOBS'] ||
          DEFAULT_CLEANUP_INTERVAL_JOBS
      )
      value.present? ? value.to_i : nil
    end

    # Number of seconds a {Scheduler} will wait before cleaning up preserved jobs.
    # @return [Integer, nil]
    def cleanup_interval_seconds
      value = (
        rails_config[:cleanup_interval_seconds] ||
          env['GOOD_JOB_CLEANUP_INTERVAL_SECONDS'] ||
          DEFAULT_CLEANUP_INTERVAL_SECONDS
      )
      value.present? ? value.to_i : nil
    end

    # Tests whether to daemonize the process.
    # @return [Boolean]
    def daemonize?
      options[:daemonize] || false
    end

    # Path of the pidfile to create when running as a daemon.
    # @return [Pathname,String]
    def pidfile
      options[:pidfile] ||
        env['GOOD_JOB_PIDFILE'] ||
        Rails.application.root.join('tmp', 'pids', 'good_job.pid')
    end

    # Port of the probe server
    # @return [nil,Integer]
    def probe_port
      options[:probe_port] ||
        env['GOOD_JOB_PROBE_PORT']
    end

    private

    def rails_config
      Rails.application.config.good_job
    end
  end
end