require "backup/logger/console"
require "backup/logger/logfile"
require "backup/logger/syslog"
require "backup/logger/fog_adapter"

module Backup
  class Logger
    class Config
      class Logger < Struct.new(:class, :options)
        def enabled?
          options.enabled?
        end
      end

      class DSL < Struct.new(:ignores, :console, :logfile, :syslog)
        def ignore_warning(str_or_regexp)
          ignores << str_or_regexp
        end
      end

      attr_reader :ignores, :loggers, :dsl

      def initialize
        @ignores = []
        @loggers = [
          Logger.new(Console, Console::Options.new),
          Logger.new(Logfile, Logfile::Options.new),
          Logger.new(Syslog, Syslog::Options.new)
        ]
        @dsl = DSL.new(ignores, *loggers.map(&:options))
      end
    end

    ##
    # All messages sent to the Logger are stored in Logger.messages
    # and sent to all enabled logger's #log method as Message objects.
    class Message < Struct.new(:time, :level, :lines)
      ##
      # Returns an Array of the message lines in the following format:
      #
      #   [YYYY/MM/DD HH:MM:SS][level] message line text
      def formatted_lines
        timestamp = time.strftime("%Y/%m/%d %H:%M:%S")
        lines.map { |line| "[#{timestamp}][#{level}] #{line}" }
      end

      def matches?(ignores)
        text = lines.join("\n")
        ignores.any? do |obj|
          obj.is_a?(Regexp) ? text.match(obj) : text.include?(obj)
        end
      end
    end

    class << self
      extend Forwardable
      def_delegators :logger,
        :start!, :abort!, :info, :warn, :error,
        :messages, :has_warnings?, :has_errors?

      ##
      # Allows the Logger to be configured.
      #
      #   # shown with their default values
      #   Backup::Logger.configure do
      #     # Console options:
      #     console.quiet = false
      #
      #     # Logfile options:
      #     logfile.enabled   = true
      #     logfile.log_path  = 'log'
      #     logfile.max_bytes = 500_000
      #
      #     # Syslog options:
      #     syslog.enabled  = false
      #     syslog.ident    = 'backup'
      #     syslog.options  = Syslog::LOG_PID
      #     syslog.facility = Syslog::LOG_LOCAL0
      #     syslog.info     = Syslog::LOG_INFO
      #     syslog.warn     = Syslog::LOG_WARNING
      #     syslog.error    = Syslog::LOG_ERR
      #
      #     # Ignore Warnings:
      #     # Converts :warn level messages to level :info
      #     ignore_warning 'that contains this string'
      #     ignore_warning /that matches this regexp/
      #   end
      #
      # See each Logger's Option class for details.
      # @see Console::Options
      # @see Logfile::Options
      # @see Syslog::Options
      def configure(&block)
        config.dsl.instance_eval(&block)
      end

      ##
      # Called after each backup model/trigger has been performed.
      def clear!
        @logger = nil
        logger.start!
      end

      private

      def config
        @config ||= Config.new
      end

      def logger
        @logger ||= new(config)
      end

      def reset!
        @config = @logger = nil
      end
    end

    MUTEX = Mutex.new

    ##
    # Returns an Array of Message objects for all logged messages received.
    # These are used to attach log files to Mail notifications.
    attr_reader :messages

    def initialize(config)
      @config = config
      @messages = []
      @loggers = []
      @has_warnings = @has_errors = false
    end

    ##
    # Sends a message to the Logger using the specified log level.
    # +obj+ may be any Object that responds to #to_s (i.e. an Exception)
    [:info, :warn, :error].each do |level|
      define_method level do |obj|
        MUTEX.synchronize { log(obj, level) }
      end
    end

    ##
    # Returns true if any +:warn+ level messages have been received.
    def has_warnings?
      @has_warnings
    end

    ##
    # Returns true if any +:error+ level messages have been received.
    def has_errors?
      @has_errors
    end

    ##
    # The Logger is available as soon as Backup is loaded, and stores all
    # messages it receives. Since the Logger may be configured via the
    # command line and/or the user's +config.rb+, no messages are sent
    # until configuration can be completed. (see CLI#perform)
    #
    # Once configuration is completed, this method is called to activate
    # all enabled loggers and send them any messages that have been received
    # up to this point. From this point onward, these loggers will be sent
    # all messages as soon as they're received.
    def start!
      @config.loggers.each do |logger|
        @loggers << logger.class.new(logger.options) if logger.enabled?
      end
      messages.each do |message|
        @loggers.each { |logger| logger.log(message) }
      end
    end

    ##
    # If errors are encountered by Backup::CLI while preparing to perform
    # the backup jobs, this method is called to dump all messages to the
    # console before Backup exits.
    def abort!
      console = Console.new
      console.log(messages.shift) until messages.empty?
    end

    private

    def log(obj, level)
      message = Message.new(Time.now.utc, level, obj.to_s.split("\n"))

      if message.level == :warn && message.matches?(@config.ignores)
        message.level = :info
      end
      @has_warnings ||= message.level == :warn
      @has_errors   ||= message.level == :error

      messages << message
      @loggers.each { |logger| logger.log(message) }
    end
  end
end
