require 'setback/util'
require 'erb'
require 'yaml'
require 'core_ext/hash/recursive_merge'

module Setback
  SettingsLoadError = Class.new(StandardError)

  class YamlLoader
    class << self
      DEFAULT_YAML_CASCADE = [
        'config/settings/default.yml',
        "config/settings/environments/#{ENV['RACK_ENV'] || ENV['RAILS_ENV']}.yml"
      ].freeze

      def autoload!(options = {})
        all_instances = load_all_from_yml_cascade(
          options.fetch(:yaml_cascade, DEFAULT_YAML_CASCADE),
          options.fetch(:subclasses),
          options
        )

        if options[:assert_loaded!]
          all_instances.each do |key,instance|
            raise SettingsLoadError.new(key) if instance.keys.empty?
          end
        end

        all_instances
      end

      def dump_all(subclasses)
        {}.tap do |to_dump|
          subclasses.each do |requirement,subclass|
            key = requirement.split(%r{[\\/]}).last
            to_dump[key] = lookup_subclass(subclass).lookup.all
          end
        end
      end

      def load_all_from_yml_cascade(yml_files, subclasses, options = {})
        require_all_subclasses(subclasses.keys)
        top_mtime = top_file_mtime(yml_files)
        load_options = options.merge(
          :default_last_load => options.fetch(:last_load, default_last_load_time),
          :top_mtime => top_mtime
        )
        {}.tap do |instances|
          settings_hash = hash_from_yml_cascade(yml_files)
          subclasses.each do |requirement,subclass|
            key = requirement.split(%r{[\\/]}).last
            subhash = {}
            [key, key.to_sym].each do |potential_key|
              if settings_hash.has_key?(potential_key)
                key = potential_key
                subhash = settings_hash[key]
              end
            end
            subclass_settings = load_settings_for_subclass(
              subclass, subhash, load_options
            )
            instances[key] = subclass_settings
          end
        end
      end

      private
      def running_in_rails_console?
        if defined?(Rails::Console)
          announce_not_loading_redis_backed_settings
          return true
        end
        false
      end

      def announce_not_loading_redis_backed_settings
        if !@announced_not_loading_redis_backed_settings
          STDERR.puts <<-EOF.gsub(/^\s{10}/, "#{self.name} ---> ")
          Rails console detected, so not loading redis-backed settings.
          Bustedness may happen unless redis-backed settings have been
          loaded via another process.  If you want to load them now, do:
              #{self.name}.autoload!(force: true)
          EOF
          @announced_not_loading_redis_backed_settings = true
        end
      end

      def require_all_subclasses(subclasses)
        subclasses.each do |subclass|
          require subclass.to_s
        end
      end

      def hash_from_yml_cascade(yml_files)
        merged = {}
        yml_files.each do |file|
          merged.recursive_merge!(
            YAML.load(ERB.new(IO.read(file)).result) || {}
          )
        end
        merged
      end

      def top_file_mtime(yml_files)
        yml_files.map { |f| File.mtime(f) }.max
      end

      def load_settings_for_subclass(subclass, subhash, load_options)
        subclass_type = lookup_subclass(subclass)
        instance = subclass_type.lookup || subclass_type.new(:cache? => true)
        return send(:"load_settings_for_#{instance.backed_by}_backed_instance",
                    instance, subhash, load_options)
      end

      def load_settings_for_memory_backed_instance(instance, subhash, load_options)
        instance.merge!(subhash)
        return instance
      end

      def load_settings_for_redis_backed_instance(instance, subhash, load_options)
        if running_in_rails_console? && !load_options.fetch(:force, false)
          return instance
        end

        if redis_backed_instance_stale?(instance, load_options)
          instance.merge!(subhash)
          instance[:__last_load] = Time.now
        end

        instance
      end

      def redis_backed_instance_stale?(instance, load_options)
        return true if !instance.keys.include?(:__last_load)
        last_load = instance[:__last_load] || load_options.fetch(:default_last_load)
        load_options.fetch(:top_mtime) > last_load
      end

      def lookup_subclass(subclass_name)
        Setback::Util.constantize(subclass_name)
      end

      def default_last_load_time
        Time.now - (60 * 60 * 24)
      end
    end
  end
end
