require 'yaml'
require 'open-uri'
require 'cgi'
require "socket"

def eval_and_fetch_constants(x)
  old = Module.constants.map{|c| c.to_s}
  begin
    eval(x)
  rescue Exception => e
    raise e unless Deputy.config['silent_on_errors'] 
  end
  new = (Module.constants.map{|c| c.to_s} - old)
  new = new.select{|c| c.to_s =~ /^TEMP/ } # do not fetch required libs, just user-defined
  new.map{|c| const_get(c) }
end

class Scout
  class Plugin
    OPTIONS = {}.to_yaml

    def self.clean_class_name
      parts = to_s.split('::')
      parts.size == 1 ? parts.first : parts[1..-1].join('::')
    end

    protected

    def report(metrics)
      metrics.each do |key, value|
        Deputy.send_report "#{self.class.clean_class_name}.#{key}", value
      end
    end

    def error(*args)
      report :error => args.map{|a| a.inspect}.join(', ')
    end

    def alert(*args)
      report :alert => args.map{|a| a.inspect}.join(', ')
    end

    def memory(key)
      complete_memory[key]
    end

    def remember(*args)
      data = (args.size == 1 ? args[0] : {args[0] => args[1]})
      all = complete_memory.merge(data)
      File.open(memory_file, 'w'){|f| f.write all.to_yaml }
    end

    def self.needs(*libs)
      libs.each{|lib| require lib }
    end

    private

    def complete_memory
      return {} unless File.exist?(memory_file)
      YAML.load(File.read(memory_file)) || {}
    end

    def memory_file
      "/tmp/deputy.memory.#{self.class.clean_class_name}.yml"
    end

    # stub options for now...
    def option(key)
      (YAML.load(self.class::OPTIONS)[key.to_s]||{})['default']
    end
  end

  def self.plugins(code)
    eval_and_fetch_constants(code).map do |container|
      interval = container.interval
      unless plugin = plugin_in_container(container)
        Deputy.send_report "Deputies.Plugin not found", code
        next
      end
      [interval, plugin]
    end.compact
  end

  def self.plugin_in_container(container)
    constants = container.constants.map{|constant_name|container.const_get(constant_name)}
    constants.detect{|c| c.instance_methods.map{|m| m.to_s}.include?('build_report') }
  end
end

module Deputy
  VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
  DEFAULT_VALUE = 'OK'

  def self.install_cron
    executable = `which deputy`.strip
    unless (`crontab -l`).include?(executable)
      `crontab -l | { cat; echo "* * * * * #{executable} --run-plugins >> /tmp/deputy.log 2>&1"; } | crontab -`
      if executable !~ %r{^/usr/}
        puts "make deputy globally available! or e.g. calls from inside cronjobs do not know deputy"
        puts "sudo ln -s #{executable} /usr/bin/deputy"
      end
    end
  end

  def self.run_plugins(options={})
    return if config['disabled']
    start_time = Time.now.to_i
    sleep_random_interval unless options[:no_wait]

    content = get("/plugins.rb")

    exceptions = []
    Scout.plugins(content).each do |interval, plugin|
      wait = minutes_to_wait(start_time, interval)
      if wait == 0
        puts "#{plugin.clean_class_name}: running"
        begin
          plugin.new.build_report
        rescue Object => e # catch and report plugin-specific errors
          e.message[0..0] = plugin.clean_class_name
          puts e
          exceptions << e
        end
      else
        puts "#{plugin.clean_class_name}: waiting another #{wait} minutes"
      end
    end
    send_report 'Deputies.finished', (exceptions.empty? ? DEFAULT_VALUE : 'Error')

    raise exceptions.first unless exceptions.empty?
  rescue Object => e # catch and report neta errors
    send_report "Deputies.Error", e.message
    raise e
  end

  def self.send_report(group, value, options = {})
    return if config['disabled']
    raise "separate #{group} with a ." unless group.split('.',2).size == 2
    get "/notify?group=#{CGI.escape group}&value=#{CGI.escape value.to_s}", options
  end

  def self.get(path, options = {})
    return if config['disabled']
    url = "#{sheriff_url}#{path}"
    url = "http://#{url}" unless url =~ %r{://}
    options[:http_basic_authentication] = extract_auth_from_url!(url)
    url = add_host_to_url(url, options.delete(:host))

    Timeout.timeout(config['timeout']||10) do
      open(url, options).read
    end
  rescue Exception => e
    e.message << url.to_s
    unless config['silent_on_errors'] 
      raise e
    end
  end

  def self.sheriff_url
    config['sheriff_url'].sub(%r{/$},'')
  end

  def self.config
    home = File.expand_path('~')
    ["#{home}/.deputy.yml", '/etc/deputy.yml'].each do |file|
      return YAML.load(File.read(file)) if File.exist?(file)
    end
    raise "No deputy.yml found in /etc or #{home}"
  end

  def self.minutes_to_wait(start_time, interval)
    start_minute = start_time / 60
    run_every_n_minutes = interval / 60
    start_minute % run_every_n_minutes
  end

  def self.sleep_random_interval
    if max = config['max_random_start_delay']
      constant_number = Socket.gethostname.sum{|x| x[0]}
      sleep seeded_random(max, constant_number)
    end
  end

  def self.seeded_random(max_rand, seed)
    old = srand(seed)
    result = rand(max_rand)
    srand(old)
    result
  end

  # stolen from klarlack -- http://github.com/schoefmax/klarlack
  # to get an reliable timeout that wont fail on other platforms
  # or if sytem_timer is missing
  Timeout = begin
    # Try to use the SystemTimer gem instead of Ruby's timeout library
    # when running on something that looks like Ruby 1.8.x. See:
    # http://ph7spot.com/articles/system_timer
    # We don't want to bother trying to load SystemTimer on jruby and
    # ruby 1.9+.
    if RUBY_VERSION =~ /^1\.8\./ and RUBY_PLATFORM !~ /java/
      require 'system_timer'
      SystemTimer
    else
      require 'timeout'
      Timeout
    end
  rescue LoadError => e
    $stderr.puts "Could not load SystemTimer gem, falling back to Ruby's slower/unsafe timeout library: #{e.message}"
    require 'timeout'
    Timeout
  end

  def self.extract_auth_from_url!(url)
    url.sub!(%r{//(.*?):(.*?)@}, '//')
    auth = [$1, $2].compact
    auth.empty? ? nil : auth
  end

  def self.add_host_to_url(url, host=nil)
    query = "hostname=#{host || Socket.gethostname}#{'&forced_host=true' if host}"
    separator = (url.include?('?') ? "&" : "?")
    url + separator + query
  end
end