class Log
  # Creates a `Log` for the given source.
  # If *level* is given, it will override the configuration.
  def self.for(source : String, level : Severity? = nil) : Log
    log = builder.for(source)
    log.level = level if level
    log
  end

  # Creates a `Log` for the given nested source.
  # If *level* is given, it will override the configuration.
  def for(child_source : String, level : Severity? = nil) : Log
    return ::Log.for(child_source) if source.blank?
    return ::Log.for(source) if child_source.blank?
    ::Log.for("#{source}.#{child_source}", level)
  end

  # Creates a `Log` for the given type.
  # A type `Foo::Bar(Baz)` corresponds to the source `foo.bar`.
  # If *level* is given, it will override the configuration.
  def self.for(type : Class, level : Severity? = nil) : Log
    source = type.name.underscore.gsub("::", ".")

    # remove generic arguments
    paren = source.index('(')
    source = source[0...paren] if paren

    ::Log.for(source, level)
  end

  # :ditto:
  def for(type : Class, level : Severity? = nil) : Log
    ::Log.for(type, level)
  end

  private Top = Log.for("")

  {% for method in %i(trace debug info notice warn error fatal) %}
    # See `Log#{{method.id}}`.
    def self.{{method.id}}(*, exception : Exception? = nil)
      Top.{{method.id}}(exception: exception) do |dsl|
        yield dsl
      end
    end
  {% end %}

  @@builder = Builder.new

  at_exit { @@builder.close }

  # Returns the default `Log::Builder` used for `Log.for` calls.
  def self.builder : Log::Builder
    @@builder
  end

  # Returns the current fiber logging context.
  def self.context : Log::Context
    Log::Context.new(Fiber.current.logging_context)
  end

  # Sets the current fiber logging context.
  def self.context=(value : Log::Metadata)
    Fiber.current.logging_context = value
  end

  # :ditto:
  def self.context=(value : Log::Context)
    # NOTE: There is a need for `Metadata` and `Context` setters in
    # because `Log.context` returns a `Log::Context` for allowing DSL like `Log.context.set(a: 1)`
    # but if the metadata is built manually the construct `Log.context = metadata` will be used.
    Log.context = value.metadata
  end

  # Returns the current fiber logging context.
  def context : Log::Context
    Log.context
  end

  # Sets the current fiber logging context.
  def context=(value : Log::Metadata | Log::Context)
    Log.context = value
  end

  # Method to save and restore the current logging context.
  # Temporary context for the duration of the block can be set via arguments.
  #
  # ```
  # Log.context.set a: 1
  # Log.info { %(message with {"a" => 1} context) }
  # Log.with_context(b: 2) do
  #   Log.context.set c: 3
  #   Log.info { %(message with {"a" => 1, "b" => 2, "c" => 3} context) }
  # end
  # Log.info { %(message with {"a" => 1} context) }
  # ```
  def self.with_context(**kwargs)
    previous = Log.context
    Log.context.set(**kwargs) unless kwargs.empty?
    begin
      yield
    ensure
      Log.context = previous
    end
  end

  # :ditto:
  def self.with_context(values)
    previous = Log.context
    Log.context.set(values) unless values.empty?
    begin
      yield
    ensure
      Log.context = previous
    end
  end

  # :ditto:
  def with_context(**kwargs)
    self.class.with_context(**kwargs) do
      yield
    end
  end

  # :ditto:
  def with_context(values)
    self.class.with_context(values) do
      yield
    end
  end

  struct Context
    getter metadata : Metadata

    def initialize(@metadata : Metadata)
    end

    # Clears the current `Fiber` logging context.
    #
    # ```
    # Log.context.clear
    # Log.info { "message with empty context" }
    # ```
    def clear : Nil
      Fiber.current.logging_context = @metadata = Log::Metadata.empty
    end

    # Extends the current `Fiber` logging context.
    #
    # ```
    # Log.context.set a: 1
    # Log.context.set b: 2
    # Log.info { %q(message with a: 1, b: 2 context") }
    # h = {:c => "3"}
    # Log.context.set extra: h
    # Log.info { %q(message with a: 1, b: 2, extra: {"c" => "3"} context) }
    # h = {"c" => 3}
    # Log.context.set extra: h
    # Log.info { %q(message with a: 1, b: 2, extra: {"c" => 3} context) }
    # ```
    def set(**kwargs)
      extend_fiber_context(Fiber.current, kwargs)
    end

    # :ditto:
    def set(values) : Nil
      extend_fiber_context(Fiber.current, values)
    end

    private def extend_fiber_context(fiber : Fiber, values)
      context = fiber.logging_context
      fiber.logging_context = @metadata = context.extend(values)
    end
  end

  # Helper DSL module for emitting log entries with data.
  struct Emitter
    # :nodoc:
    def initialize(@source : String, @severity : Severity, @exception : Exception?)
    end

    # Emits a logs entry with a message, and data attached to
    #
    # ```
    # Log.info &.emit("Program started")                         # No data, same as Log.info { "Program started" }
    # Log.info &.emit("User logged in", user_id: 42)             # With entry data
    # Log.info &.emit(action: "Logged in", user_id: 42)          # Empty string message, only data
    # Log.error exception: ex, &.emit("Oops", account: {id: 42}) # With data and exception
    # ```
    def emit(message : String) : Entry
      emit(message, Metadata.empty)
    end

    def emit(message : String, **kwargs) : Entry
      emit(message, kwargs)
    end

    def emit(message : String, data : Metadata | Hash | NamedTuple) : Entry
      Entry.new(@source, @severity, message, Metadata.build(data), @exception)
    end

    def emit(**kwargs) : Entry
      emit(kwargs)
    end

    def emit(data : Metadata | Hash | NamedTuple) : Entry
      emit("", Metadata.build(data))
    end
  end
end
