# frozen_string_literal: true

require 'sensu/extension'
require 'sensu/extension/tueor/version'

require 'tueor/mixin'
require 'tueor/patches'

require 'json'

module Sensu
  module Extension
    class TueorHandler < Handler
    end

    class TueorCheck < Check
      extend ::Tueor::Mixin

      class << self
        def prep_step(name, *sets, &block)
          store_step(name, sets, true, &block)
        end

        def step(name, *sets, &block)
          store_step(name, sets, false, &block)
        end

        def steps(only: nil)
          @steps ||= []

          case only
            when :run_once
              @steps.select { |step| step[:run_once] }
            when :run_always
              @steps.reject { |step| step[:run_once] }
            when nil
              @steps
            else
              raise "Invalid only filter `#{only}`"
          end
        end

        private

        def store_step(name, sets, run_once, &block)
          steps << {
              block: block,
              name: name,
              sets: sets,
              run_once: run_once
          }
        end
      end

      module Runner
        def initialize(namespace, check)
          @namespace = namespace
          @check = check
        end

        def set(key, value)
          @check.set(key, value, namespace: @namespace)
        end

        def get(key)
          @check.get(key, namespace: @namespace)
        end

        def has?(key)
          @check.has?(key, namespace: @namespace)
        end

        def method_missing(name, *args)
          return get(name) if has?(name) || @check.class.steps.collect { |step| step[:sets] }.flatten.include?(name)
          super
        end

        def respond_to_missing?(name, *args)
          return true if has?(name) || @check.class.steps.collect { |step| step[:sets] }.flatten.include?(name)
          super
        end
      end

      OK = 0
      WARNING = 1
      CRITICAL = 2
      UNKNOWN = 3
      GLOBAL_NAMESPACE = :tueor_extension_global_settings

      def initialize
        @data ||= {}
        @data[GLOBAL_NAMESPACE] ||= definition.dup
        super
      end

      def run_steps(filter, namespace)
        self.class.steps(only: filter).each do |step|
          EventMachine.schedule { runner(namespace).instance_eval(&step[:block]) }
        end
      end

      def post_init
        run_steps(:run_once, GLOBAL_NAMESPACE)
      end

      def run(check)
        namespace = [check[:source], check[:name]].compact.join(":")

        @data[namespace] = check.dup
        reserved = self.class.steps.collect { |step| step[:sets] }.flatten
        reserved.each do |key|
          yield("key (#{key}) is defined in the check definition but expected to be set from the check itself", UNKNOWN) if @data[namespace].include?(key)
          @data[namespace].delete(key)
        end

        # Add step to read in secrets if they exist
        path = Gem.win_platform? ? File.join('/opt/sensu/secrets', "#{check[:name]}.json") :
                                   File.join('/etc/sensu/secrets', "#{check[:name]}.json")

        begin
          ::JSON.parse(File.read(path)).each{|k,v| @data[namespace][k.to_sym] = v}
        rescue
        ensure
          @data[namespace][:arcana_secrets_have_been_set] = true
        end unless @data[namespace][:arcana_secrets_have_been_set]

        run_steps(:run_always, namespace)

        is_critical = self.class.instance_method(:critical?)
        is_warning = self.class.instance_method(:warning?)

        when_true(proc { ready?(namespace) }) do |timed_out, elapsed|
          if timed_out
            yield "Unable to reach ready before timing out in #{elapsed} seconds.", WARNING
          elsif is_critical.bind(runner(namespace)).call
            yield get(:message, namespace: namespace), CRITICAL
          elsif is_warning.bind(runner(namespace)).call
            yield get(:message, namespace: namespace), WARNING
          else
            yield get(:message, namespace: namespace), OK
          end

        end
      end

      def set(key, value, namespace: GLOBAL_NAMESPACE)
        @data[namespace] ||= {}
        @data[namespace][key] = value
      end

      def get(key, namespace: GLOBAL_NAMESPACE)
        @data[namespace] ||= {}
        @data[namespace].include?(key) ? @data[namespace][key] : @data[GLOBAL_NAMESPACE][key]
      end

      def has?(key, namespace: GLOBAL_NAMESPACE)
        @data[namespace] ||= {}
        @data[namespace].include?(key) || @data[GLOBAL_NAMESPACE].include?(key)
      end

      def ready?(namespace)
        required = self.class.steps.collect { |step| step[:sets] }.flatten
        required.all? { |key| has?(key, namespace: namespace) }
      end

      private

      def runner(namespace)
        @runners ||= {}
        unless @runners.include?(namespace)
          klass = Class.new(self.class)
          klass.extend ::Tueor::Mixin
          klass.include Runner
          @runners[namespace] = klass.new(namespace, self)
        end
        @runners[namespace]
      end
    end
  end
end
