require 'yaml'

module Flydata
  module Helper
    class ConfigError < StandardError
    end

    # This module parses a config file and return a key symbolized hash.
    # Additionally config for the helper is replaced with Config instance
    #
    # ex) helper.conf
    # ============================================
    # # For helper(serverengine)
    # workers: 2
    # # For helper
    # helper:
    #   scheduled_actions:
    #     check_remote_actions:
    #       check_interval: 10s
    # ============================================
    class Config < Hash
      DEFAULT_INTERVAL = 30.0
      DEFAULT_SCHEDULED_ACTIONS = {
        check_remote_actions: {
          check_interval: '30s',
        },
      }
      DEFAULT_HELPER = {
        scheduled_actions: DEFAULT_SCHEDULED_ACTIONS,
      }
      DEFAULT_CONFIG = {
        helper: DEFAULT_HELPER
      }

      def self.create(hash)
        self.new.merge(hash)
      end

      def self.default
        create(DEFAULT_HELPER)
      end

      def fetch_scheduled_actions_conf(action_name, name, default = nil)
        value = if self[:scheduled_actions] and
                   self[:scheduled_actions][action_name.to_sym] and
                   self[:scheduled_actions][action_name.to_sym][name.to_sym]
                  self[:scheduled_actions][action_name.to_sym][name.to_sym]
                else
                  nil
                end
        value.nil? ? default : value
      end

      class << self
        # snip from https://github.com/fluent/fluentd/blob/master/lib/fluent/config.rb#L119
        def time_value(str)
          case str.to_s
          when /([0-9]+)s/
            $~[1].to_i
          when /([0-9]+)m/
            $~[1].to_i * 60
          when /([0-9]+)h/
            $~[1].to_i * 60*60
          when /([0-9]+)d/
            $~[1].to_i * 24*60*60
          else
            str.to_f
          end
        end

        def convert_format(format, value)
          return nil if value.nil?
          case format
          when :time
            self.time_value(value)
          when :integer
            value.to_i
          when :float
            value.to_f
          when :string
            value.to_s
          when :bool
            case value
            when 'true'
              true
            when 'false'
              false
            else
              !!(value)
            end
          else
            raise "Invalid format:#{format}"
          end
        end

        def config_param(name, format, option = {})
          method_name = name.to_s
          key = option[:key] || name
          default_value = option[:default]

          case option[:type]
          when :scheduled_actions
            define_method(method_name) do |action_name|
              Config.convert_format(format, fetch_scheduled_actions_conf(
                action_name, key, default_value))
            end
          else
            define_method(method_name) do
              def_val = if default_value.respond_to?(:call)
                          default_value.call(self)
                        else
                          default_value
                        end
              Config.convert_format(format, self[key] || def_val)
            end
          end
        end
      end

      config_param :helper_retry_alert_limit, :integer, default: 3
      config_param :helper_retry_interval, :float, default: 10
      config_param :helper_retry_limit, :integer, default: 15

      # helper directories
      config_param :helper_home, :string,
        default: FLYDATA_HELPER_HOME
      config_param :helper_pid_dir, :string,
        default: lambda{|c| File.join(c.helper_home, 'pids') }
      config_param :helper_position_dir, :string,
        default: lambda{|c| File.join(c.helper_home, 'positions') }


      # helper files
      config_param :helper_action_position_path, :string,
        default: lambda{|c| File.join(c.helper_position_dir, 'helper_action.pos') }
      config_param :helper_log_path, :string,
        default: FLYDATA_LOG
      config_param :helper_pid_path, :string,
        default: lambda{|c| File.join(c.helper_pid_dir, 'helper.pid') }

      # Return deep copy
      def scheduled_actions
        actions = self[:scheduled_actions].dup
        self[:scheduled_actions].each do |k, v|
          actions[k] = v.dup
          actions[k][:name] = k
          actions[k][:check_interval] = Config.convert_format(:time, v)
        end
        actions
      end
    end

    class ConfigParser
      def self.parse_file(config_path = nil)
        config_path.nil? ? { helper: Config.default } : parse(File.open(config_path).read)
      end

      def self.parse(config_content)
        ConfigParser.new.parse(config_content)
      end

      def parse(config_content)
        conf = YAML::load(config_content)
        raise ConfigError.new("Invalid conf format.") unless conf and conf.kind_of?(Hash)
        conf = symbolize_keys(conf)
        conf[:helper] = Config.create(parse_helper(conf[:helper]))
        conf
      end

      def parse_helper(conf)
        return Config::DEFAULT_HELPER if conf.nil? or conf.empty?
        conf[:scheduled_actions] = Config::DEFAULT_SCHEDULED_ACTIONS.
          dup.merge(conf[:scheduled_actions] || {})
        conf
      end

      def symbolize_keys(value)
        case value
        when Hash
          value.inject({}){|result, (key, value)|
            new_key = case key
                      when String then key.to_sym
                      else key
                      end
            new_value = symbolize_keys(value)
            result[new_key] = new_value
            result
          }
        when Array
          value.collect {|e| symbolize_keys(e)}
        else
          value
        end
      end
    end
  end
end