/**
 * @page copyright
 * Copyright(c) 2020-present, Odysseas Georgoudis & quill contributors.
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
 */

#pragma once

#include "quill/core/Attributes.h"
#include "quill/core/Filesystem.h"
#include "quill/core/LogLevel.h"
#include "quill/core/QuillError.h"
#include "quill/sinks/Sink.h"

#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>

#if defined(_WIN32)
  #if !defined(WIN32_LEAN_AND_MEAN)
    #define WIN32_LEAN_AND_MEAN
  #endif

  #if !defined(NOMINMAX)
    // Mingw already defines this, so no need to redefine
    #define NOMINMAX
  #endif

  #include <io.h>
  #include <windows.h>
#endif

QUILL_BEGIN_NAMESPACE

#if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
  #pragma warning(push)
  #pragma warning(disable : 4996)
#endif

QUILL_BEGIN_EXPORT

/** Forward Declaration **/
class MacroMetadata;

#if defined(_WIN32)
using FileEventNotifierHandle = HANDLE;
#else
using FileEventNotifierHandle = FILE*;
#endif

/**
 * @brief Notifies on file events by calling the appropriate callback.
 *
 * @note `before_write` executes as part of the normal log write path.
 *       `before_open`, `after_open`, `before_close`, and `after_close` execute on the thread
 *       performing the file open/close operation. Different callbacks, and different invocations
 *       of the same callback, may therefore run on different threads over the sink lifetime.
 *       Callbacks must be thread-safe and must not assume a single calling thread.
 *
 * @warning File-event callbacks must not call `Frontend::create_sink()` or
 *          `Frontend::create_or_get_sink()` with the name of the sink currently being constructed.
 *          That sink name remains reserved until construction completes, so recursively requesting
 *          it waits indefinitely. Creating a sink with a different name is supported.
 *
 * @note When `before_write` changes the message size, size-based rotation
 *       (`rotation_max_file_size`) evaluates its threshold using the pre-transform size of the
 *       current message, so a rotation can trigger up to one transformed message later or
 *       earlier than the exact limit. The tracked file size itself stays accurate.
 */
struct FileEventNotifier
{
  std::function<void(fs::path const& file_path)> before_open;
  std::function<void(fs::path const& file_path, FileEventNotifierHandle f)> after_open;
  std::function<void(fs::path const& file_path, FileEventNotifierHandle f)> before_close;
  std::function<void(fs::path const& file_path)> after_close;
  std::function<std::string(std::string_view message)> before_write;
};

/**
 * @brief StreamSink class for handling log messages
 */
class StreamSink : public Sink
{
public:
  /**
   * @brief Constructor for StreamSink
   * @param stream The stream type (stdout, stderr, or file)
   * @param override_pattern_formatter_options override the logger pattern formatter
   * @param file File pointer for file-based stream
   * @param file_event_notifier Notifies on file events
   * @throws QuillError if an invalid parameter is provided
   */
  explicit StreamSink(fs::path stream, FILE* file = nullptr,
                      std::optional<PatternFormatterOptions> const& override_pattern_formatter_options = std::nullopt,
                      FileEventNotifier file_event_notifier = FileEventNotifier{})
    : Sink(override_pattern_formatter_options),
      _filename(std::move(stream)),
      _file(file),
      _file_event_notifier(std::move(file_event_notifier))
  {
    // reserve stdout and stderr as filenames
    if (_filename == std::string{"stdout"})
    {
      _file = stdout;
    }
    else if (_filename == std::string{"stderr"})
    {
      _file = stderr;
    }
    else if (_filename == std::string{"/dev/null"})
    {
      _is_null = true;
    }
    else
    {
      _filename = detail::normalize_file_sink_path(_filename);
    }
  }

  ~StreamSink() override = default;

  /**
   * @brief Writes a formatted log message to the stream
   */
  QUILL_ATTRIBUTE_HOT void write_log(MacroMetadata const* /* log_metadata */,
                                     uint64_t /* log_timestamp */, std::string_view /* thread_id */,
                                     std::string_view /* thread_name */, std::string const& /* process_id */,
                                     std::string_view /* logger_name */, LogLevel /* log_level */,
                                     std::string_view /* log_level_description */,
                                     std::string_view /* log_level_short_code */,
                                     std::vector<std::pair<std::string, std::string>> const* /* named_args */,
                                     std::string_view /* log_message */, std::string_view log_statement) override
  {
    if (QUILL_UNLIKELY(!_file))
    {
      // FileSink::flush() tries to re-open a deleted file and if it fails _file can be null
      return;
    }

    if (_file_event_notifier.before_write)
    {
      std::string const user_log_statement = _file_event_notifier.before_write(log_statement);
      _write_statement(user_log_statement);
    }
    else
    {
      _write_statement(log_statement);
    }
  }

  /**
   * Flushes the stream
   */
  QUILL_ATTRIBUTE_HOT void flush_sink() override
  {
    if (!_write_occurred || !_file)
    {
      return;
    }

    flush();
  }

  /**
   * @brief Returns the name of the file
   * @return The name of the file
   */
  QUILL_NODISCARD virtual fs::path const& get_filename() const noexcept { return _filename; }

  /**
   * @brief Checks if the stream is null
   * @return True if the stream is null, false otherwise
   */
  QUILL_NODISCARD bool is_null() const noexcept { return _is_null; }

  /**
   * @brief Writes data safely to the stream
   * @param ptr Pointer to the data to be written
   * @param size Size of each element to be written
   * @param count Number of elements to write
   * @param stream The stream to write to
   */
  QUILL_ATTRIBUTE_HOT static void safe_fwrite(void const* ptr, size_t size, size_t count, FILE* stream)
  {
    size_t const total_bytes = size * count;
    size_t bytes_written = 0;

    while (bytes_written < total_bytes)
    {
      auto const* current_ptr = static_cast<char const*>(ptr) + bytes_written;
      size_t const remaining = total_bytes - bytes_written;

#if defined(_WIN32)
      // On Windows, using fwrite to non-binary stream (stdout/stderr) results in \r\r\n
      // Instead, use WriteFile for console streams
      if ((stream == stdout) || (stream == stderr))
      {
        HANDLE const handle = reinterpret_cast<HANDLE>(::_get_osfhandle(::_fileno(stream)));

        if (QUILL_LIKELY(handle != INVALID_HANDLE_VALUE))
        {
          constexpr size_t max_dword = static_cast<size_t>(~DWORD{0});
          auto const total_bytes_remaining = static_cast<DWORD>(std::min<size_t>(remaining, max_dword));
          DWORD bytes_written_this_call = 0;

          if (QUILL_UNLIKELY(::WriteFile(handle, current_ptr, total_bytes_remaining,
                                         &bytes_written_this_call, nullptr) == 0))
          {
            QUILL_THROW(QuillError{std::string{"WriteFile failed. GetLastError: "} +
                                   std::to_string(::GetLastError())});
          }

          if (QUILL_UNLIKELY(bytes_written_this_call == 0))
          {
            QUILL_THROW(QuillError{"WriteFile returned 0 bytes written without error"});
          }

          bytes_written += bytes_written_this_call;
          continue;
        }

        // Fall through to fwrite if the handle is invalid (e.g., no console attached)
      }
#endif

      size_t const written = std::fwrite(current_ptr, 1, remaining, stream);

      if (QUILL_UNLIKELY(written < remaining))
      {
        // Partial write or error
        if (ferror(stream))
        {
          int const saved_errno = errno;
          std::clearerr(stream); // Reset error state
          QUILL_THROW(QuillError{std::string{"fwrite failed errno: "} + std::to_string(saved_errno) +
                                 " error: " + std::strerror(saved_errno)});
        }

        if (written == 0)
        {
          // Zero bytes written without error is unusual - treat as fatal to avoid infinite loop
          QUILL_THROW(
            QuillError{std::string{"fwrite returned 0 bytes written without error - stream may be "
                                   "at EOF or in invalid state"}});
        }

        // Partial write succeeded - continue with remaining bytes
      }

      bytes_written += written;
    }
  }

protected:
  QUILL_NODISCARD virtual size_t estimate_write_size(
    MacroMetadata const* /* log_metadata */, uint64_t /* log_timestamp */,
    std::string_view /* thread_id */, std::string_view /* thread_name */,
    std::string const& /* process_id */, std::string_view /* logger_name */, LogLevel /* log_level */,
    std::string_view /* log_level_description */, std::string_view /* log_level_short_code */,
    std::vector<std::pair<std::string, std::string>> const* /* named_args */,
    std::string_view /* log_message */, std::string_view log_statement)
  {
    return log_statement.size();
  }

  /**
   * Discards any state estimate_write_size() may have cached for the write that follows it.
   * Called when that write is abandoned (e.g. an exception during file rotation) so a later,
   * unrelated record cannot consume the stale cached state.
   */
  void discard_write_estimate() noexcept {}

  /**
   * Writes an already transformed statement without invoking before_write again.
   */
  QUILL_ATTRIBUTE_HOT void _write_statement(std::string_view statement)
  {
    safe_fwrite(statement.data(), sizeof(char), statement.size(), _file);
    _file_size += statement.size();
    _write_occurred = true;
  }

  /**
   * Flushes the stream
   */
  QUILL_ATTRIBUTE_HOT void flush()
  {
    // Retry on EINTR — a signal delivered during fflush() causes it to fail transiently.
    int result{0};
    do
    {
      result = std::fflush(_file);
    } while (result != 0 && errno == EINTR);

    if (QUILL_LIKELY(result == 0))
    {
      _write_occurred = false;
    }
    else
    {
      int const saved_errno = errno;
      std::clearerr(_file); // Reset error state
      QUILL_THROW(QuillError{std::string{"fflush failed errno: "} + std::to_string(saved_errno) +
                             " error: " + std::strerror(saved_errno)});
    }
  }

protected:
  fs::path _filename;
  FILE* _file{nullptr};
  size_t _file_size{0}; /**< The current file size */
  FileEventNotifier _file_event_notifier;
  bool _is_null{false};
  bool _write_occurred{false};
};

QUILL_END_EXPORT

#if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
  #pragma warning(pop)
#endif

QUILL_END_NAMESPACE
