require 'puppet/util/tagging'
require 'puppet/util/classgen'
require 'puppet/network/format_support'

# Pass feedback to the user.  Log levels are modeled after syslog's, and it is
# expected that that will be the most common log destination.  Supports
# multiple destinations, one of which is a remote server.
class Puppet::Util::Log
  include Puppet::Util
  extend Puppet::Util::ClassGen
  include Puppet::Util::Tagging
  include Puppet::Network::FormatSupport

  @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit]
  @loglevel = 2

  @desttypes = {}

  # Create a new destination type.
  def self.newdesttype(name, options = {}, &block)

    dest = genclass(
      name,
      :parent     => Puppet::Util::Log::Destination,
      :prefix     => "Dest",
      :block      => block,
      :hash       => @desttypes,
      :attributes => options
    )
    dest.match(dest.name)

    dest
  end

  require 'puppet/util/log/destination'
  require 'puppet/util/log/destinations'

  @destinations = {}

  @queued = []

  class << self
    include Puppet::Util
    include Puppet::Util::ClassGen

    attr_reader :desttypes
  end

  # Reset log to basics.  Basically just flushes and closes files and
  # undefs other objects.
  def Log.close(destination)
    if @destinations.include?(destination)
      @destinations[destination].flush if @destinations[destination].respond_to?(:flush)
      @destinations[destination].close if @destinations[destination].respond_to?(:close)
      @destinations.delete(destination)
    end
  end

  def self.close_all
    destinations.keys.each { |dest|
      close(dest)
    }
    raise Puppet::DevError.new("Log.close_all failed to close #{@destinations.keys.inspect}") if !@destinations.empty?
  end

  # Flush any log destinations that support such operations.
  def Log.flush
    @destinations.each { |type, dest|
      dest.flush if dest.respond_to?(:flush)
    }
  end

  def Log.autoflush=(v)
    @destinations.each do |type, dest|
      dest.autoflush = v if dest.respond_to?(:autoflush=)
    end
  end

  # Create a new log message.  The primary role of this method is to
  # avoid creating log messages below the loglevel.
  def Log.create(hash)
    raise Puppet::DevError, "Logs require a level" unless hash.include?(:level)
    raise Puppet::DevError, "Invalid log level #{hash[:level]}" unless @levels.index(hash[:level])
    @levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil
  end

  def Log.destinations
    @destinations
  end

  # Yield each valid level in turn
  def Log.eachlevel
    @levels.each { |level| yield level }
  end

  # Return the current log level.
  def Log.level
    @levels[@loglevel]
  end

  # Set the current log level.
  def Log.level=(level)
    level = level.intern unless level.is_a?(Symbol)

    raise Puppet::DevError, "Invalid loglevel #{level}" unless @levels.include?(level)

    @loglevel = @levels.index(level)
  end

  def Log.levels
    @levels.dup
  end

  # Create a new log destination.
  def Log.newdestination(dest)
    # Each destination can only occur once.
    if @destinations.find { |name, obj| obj.name == dest }
      return
    end

    name, type = @desttypes.find do |name, klass|
      klass.match?(dest)
    end

    if type.respond_to?(:suitable?) and not type.suitable?(dest)
      return
    end

    raise Puppet::DevError, "Unknown destination type #{dest}" unless type

    begin
      if type.instance_method(:initialize).arity == 1
        @destinations[dest] = type.new(dest)
      else
        @destinations[dest] = type.new
      end
      flushqueue
      @destinations[dest]
    rescue => detail
      Puppet.log_exception(detail)

      # If this was our only destination, then add the console back in.
      newdestination(:console) if @destinations.empty? and (dest != :console and dest != "console")
    end
  end

  def Log.with_destination(destination, &block)
    if @destinations.include?(destination)
      yield
    else
      newdestination(destination)
      begin
        yield
      ensure
        close(destination)
      end
    end
  end

  # Route the actual message. FIXME There are lots of things this method
  # should do, like caching and a bit more.  It's worth noting that there's
  # a potential for a loop here, if the machine somehow gets the destination set as
  # itself.
  def Log.newmessage(msg)
    return if @levels.index(msg.level) < @loglevel

    queuemessage(msg) if @destinations.length == 0

    @destinations.each do |name, dest|
      dest.handle(msg)
    end
  end

  def Log.queuemessage(msg)
    @queued.push(msg)
  end

  def Log.flushqueue
    return unless @destinations.size >= 1
    @queued.each do |msg|
      Log.newmessage(msg)
    end
    @queued.clear
  end

  # Flush the logging queue.  If there are no destinations available,
  #  adds in a console logger before flushing the queue.
  # This is mainly intended to be used as a last-resort attempt
  #  to ensure that logging messages are not thrown away before
  #  the program is about to exit--most likely in a horrific
  #  error scenario.
  # @return nil
  def Log.force_flushqueue()
    if (@destinations.empty? and !(@queued.empty?))
      newdestination(:console)
    end
    flushqueue
  end

  def Log.sendlevel?(level)
    @levels.index(level) >= @loglevel
  end

  # Reopen all of our logs.
  def Log.reopen
    Puppet.notice "Reopening log files"
    types = @destinations.keys
    @destinations.each { |type, dest|
      dest.close if dest.respond_to?(:close)
    }
    @destinations.clear
    # We need to make sure we always end up with some kind of destination
    begin
      types.each { |type|
        Log.newdestination(type)
      }
    rescue => detail
      if @destinations.empty?
        Log.setup_default
        Puppet.err detail.to_s
      end
    end
  end

  def self.setup_default
    Log.newdestination(
      (Puppet.features.syslog?   ? :syslog   :
      (Puppet.features.eventlog? ? :eventlog : Puppet[:puppetdlog])))
  end

  # Is the passed level a valid log level?
  def self.validlevel?(level)
    @levels.include?(level)
  end

  def self.from_data_hash(data)
    obj = allocate
    obj.initialize_from_hash(data)
    obj
  end

  def self.from_pson(data)
    Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
    self.from_data_hash(data)
  end

  attr_accessor :time, :remote, :file, :line, :pos, :source, :issue_code, :environment, :node, :backtrace
  attr_reader :level, :message

  def initialize(args)
    self.level = args[:level]
    self.message = args[:message]
    self.source = args[:source] || "Puppet"

    @time = Time.now

    if tags = args[:tags]
      tags.each { |t| self.tag(t) }
    end

    # Don't add these unless defined (preserve 3.x API as much as possible)
    [:file, :line, :pos, :issue_code, :environment, :node, :backtrace].each do |attr|
      next unless value = args[attr]
      send(attr.to_s + '=', value)
    end

    Log.newmessage(self)
  end

  def initialize_from_hash(data)
    @level = data['level'].intern
    @message = data['message']
    @source = data['source']
    @tags = Puppet::Util::TagSet.new(data['tags'])
    @time = data['time']
    if @time.is_a? String
      @time = Time.parse(@time)
    end
    # Don't add these unless defined (preserve 3.x API as much as possible)
    %w(file line pos issue_code environment node backtrace).each do |name|
      next unless value = data[name]
      send(name + '=', value)
    end
  end

  def to_hash
    self.to_data_hash
  end

  def to_data_hash
    {
      'level' => @level,
      'message' => to_s,
      'source' => @source,
      'tags' => @tags.to_a,
      'time' => @time.iso8601(9),
      'file' => @file,
      'line' => @line,
    }
  end

  def to_structured_hash
    hash = {
      'level' => @level,
      'message' => @message,
      'source' => @source,
      'tags' => @tags.to_a,
      'time' => @time.iso8601(9),
    }
    %w(file line pos issue_code environment node backtrace).each do |name|
      attr_name = "@#{name}"
      hash[name] = instance_variable_get(attr_name) if instance_variable_defined?(attr_name)
    end
    hash
  end

  def to_pson(*args)
    to_data_hash.to_pson(*args)
  end

  def message=(msg)
    raise ArgumentError, "Puppet::Util::Log requires a message" unless msg
    @message = msg.to_s
  end

  def level=(level)
    raise ArgumentError, "Puppet::Util::Log requires a log level" unless level
    raise ArgumentError, "Puppet::Util::Log requires a symbol or string" unless level.respond_to? "to_sym"
    @level = level.to_sym
    raise ArgumentError, "Invalid log level #{@level}" unless self.class.validlevel?(@level)

    # Tag myself with my log level
    tag(level)
  end

  # If they pass a source in to us, we make sure it is a string, and
  # we retrieve any tags we can.
  def source=(source)
    if source.respond_to?(:path)
      @source = source.path
      source.tags.each { |t| tag(t) }
      self.file = source.file
      self.line = source.line
    else
      @source = source.to_s
    end
  end

  def to_report
    "#{time} #{source} (#{level}): #{to_s}"
  end

  def to_s
    msg = message

    # Issue based messages do not have details in the message. It
    # must be appended here
    unless issue_code.nil?
      msg = "Could not parse for environment #{environment}: #{msg}" unless environment.nil?
      if file && line && pos
        msg = "#{msg} at #{file}:#{line}:#{pos}"
      elsif file and line
        msg = "#{msg}  at #{file}:#{line}"
      elsif line && pos
        msg = "#{msg}  at line #{line}:#{pos}"
      elsif line
        msg = "#{msg}  at line #{line}"
      elsif file
        msg = "#{msg}  in #{file}"
      end
      msg = "#{msg} on node #{node}" unless node.nil?
      if @backtrace.is_a?(Array)
        msg += "\n"
        msg += @backtrace.join("\n")
      end
    end
    msg
  end

end

# This is for backward compatibility from when we changed the constant to Puppet::Util::Log
# because the reports include the constant name.  Apparently the alias was created in
# March 2007, should could probably be removed soon.
Puppet::Log = Puppet::Util::Log
