# frozen_string_literal: true

class ProblemCheck
  class Collection
    include Enumerable

    def initialize(checks)
      @checks = checks
    end

    def each(...)
      checks.each(...)
    end

    def run_all
      select(&:enabled?).each(&:run)
    end

    private

    attr_reader :checks
  end

  include ActiveSupport::Configurable

  config_accessor :enabled, default: true, instance_writer: false
  config_accessor :priority, default: "low", instance_writer: false

  # Determines if the check should be performed at a regular interval, and if
  # so how often. If left blank, the check will be performed every time the
  # admin dashboard is loaded, or the data is otherwise requested.
  #
  config_accessor :perform_every, default: nil, instance_writer: false

  # How many times the check should retry before registering a problem. Only
  # works for scheduled checks.
  #
  config_accessor :max_retries, default: 2, instance_writer: false

  # The retry delay after a failed check. Only works for scheduled checks with
  # more than one retry configured.
  #
  config_accessor :retry_after, default: 30.seconds, instance_writer: false

  # How many consecutive times the check can fail without notifying admins.
  # This can be used to give some leeway for transient problems. Note that
  # retries are not counted. So a check that ultimately fails after e.g. two
  # retries is counted as one "blip".
  #
  config_accessor :max_blips, default: 0, instance_writer: false

  # Indicates that the problem check is an "inline" check. This provides a
  # low level construct for registering problems ad-hoc within application
  # code, without having to extract the checking logic into a dedicated
  # problem check.
  #
  config_accessor :inline, default: false, instance_writer: false

  # Problem check classes need to be registered here in order to be enabled.
  #
  # Note: This list must come after the `config_accessor` declarations.
  #
  CORE_PROBLEM_CHECKS = [
    ProblemCheck::BadFaviconUrl,
    ProblemCheck::EmailPollingErroredRecently,
    ProblemCheck::FacebookConfig,
    ProblemCheck::FailingEmails,
    ProblemCheck::ForceHttps,
    ProblemCheck::GithubConfig,
    ProblemCheck::GoogleAnalyticsVersion,
    ProblemCheck::GoogleOauth2Config,
    ProblemCheck::GroupEmailCredentials,
    ProblemCheck::HostNames,
    ProblemCheck::ImageMagick,
    ProblemCheck::MissingMailgunApiKey,
    ProblemCheck::OutOfDateThemes,
    ProblemCheck::PollPop3Timeout,
    ProblemCheck::PollPop3AuthError,
    ProblemCheck::RailsEnv,
    ProblemCheck::Ram,
    ProblemCheck::S3BackupConfig,
    ProblemCheck::S3Cdn,
    ProblemCheck::S3UploadConfig,
    ProblemCheck::SidekiqCheck,
    ProblemCheck::SubfolderEndsInSlash,
    ProblemCheck::TranslationOverrides,
    ProblemCheck::TwitterConfig,
    ProblemCheck::TwitterLogin,
    ProblemCheck::UnreachableThemes,
    ProblemCheck::WatchedWords,
  ].freeze

  # To enforce the unique constraint in Postgres <15 we need a dummy
  # value, since the index considers NULLs to be distinct.
  NO_TARGET = "__NULL__"

  def self.[](key)
    key = key.to_sym

    checks.find { |c| c.identifier == key }
  end

  def self.checks
    Collection.new(DiscoursePluginRegistry.problem_checks.concat(CORE_PROBLEM_CHECKS))
  end

  def self.scheduled
    Collection.new(checks.select(&:scheduled?))
  end

  def self.realtime
    Collection.new(checks.select(&:realtime?))
  end

  def self.identifier
    name.demodulize.underscore.to_sym
  end
  delegate :identifier, to: :class

  def self.enabled?
    enabled
  end
  delegate :enabled?, to: :class

  def self.scheduled?
    perform_every.present?
  end
  delegate :scheduled?, to: :class

  def self.realtime?
    !scheduled? && !inline?
  end
  delegate :realtime?, to: :class

  def self.inline?
    inline
  end
  delegate :inline?, to: :class

  def self.call(data = {})
    new(data).call
  end

  def self.run(data = {}, &)
    new(data).run(&)
  end

  def initialize(data = {})
    @data = OpenStruct.new(data)
  end

  attr_reader :data

  def call
    raise NotImplementedError
  end

  def run
    problems = call

    yield(problems) if block_given?

    next_run_at = perform_every&.from_now

    if problems.empty?
      targets.each { |t| tracker(t).no_problem!(next_run_at:) }
    else
      problems
        .uniq(&:target)
        .each do |problem|
          tracker(problem.target).problem!(
            next_run_at:,
            details: translation_data.merge(problem.details).merge(base_path: Discourse.base_path),
          )
        end
    end

    problems
  end

  private

  def tracker(target = NO_TARGET)
    ProblemCheckTracker[identifier, target]
  end

  def targets
    [NO_TARGET]
  end

  def problem(override_key: nil, override_data: {})
    [
      Problem.new(
        I18n.t(
          override_key || translation_key,
          base_path: Discourse.base_path,
          **override_data.merge(translation_data).symbolize_keys,
        ),
        priority: self.config.priority,
        identifier:,
      ),
    ]
  end

  def no_problem
    []
  end

  def translation_key
    "dashboard.problem.#{identifier}"
  end

  def translation_data
    {}
  end
end