#include "foundation/diagnostics/Logger.h"

#include "foundation/io/FileSystemUtils.h"
#include "foundation/time/TimeFormat.h"

#include <algorithm>
#include <chrono>
#include <deque>
#include <fstream>
#include <mutex>
#include <sstream>
#include <thread>
#include <utility>

#if defined(_WIN32)
extern "C" __declspec(dllimport) unsigned long __stdcall GetCurrentThreadId();
#endif

namespace
{
    constexpr std::size_t max_history_entries = 2500;
    constexpr std::uintmax_t max_persistent_log_bytes = 2U * 1024U * 1024U;

    ta::foundation::logger::Sink* g_sink = nullptr;
    std::ofstream g_file_output;
    std::filesystem::path g_file_path;
    std::uintmax_t g_file_bytes = 0;
    std::deque<ta::foundation::logger::Entry> g_history;
    std::uint64_t g_sequence = 0;
    std::mutex g_mutex;

    std::string current_timestamp()
    {
        return ta::foundation::time::format_local_timestamp(
            std::chrono::system_clock::now(), "%H:%M:%S", '.');
    }

    std::uint64_t current_thread_id() noexcept
    {
#if defined(_WIN32)
        return static_cast<std::uint64_t>(::GetCurrentThreadId());
#else
        // Portable-test host only. Production Windows builds always use the native TID.
        return static_cast<std::uint64_t>(std::hash<std::thread::id>{}(std::this_thread::get_id()));
#endif
    }

    std::string format_entry(const ta::foundation::logger::Entry& entry)
    {
        std::ostringstream stream;
        stream << entry.timestamp
               << " [" << ta::foundation::logger::level_name(entry.level) << ']';
        if (!entry.channel.empty())
            stream << '[' << entry.channel << ']';
        stream << "[T" << entry.thread_id << "] " << entry.message;
        return stream.str();
    }

    std::filesystem::path rotated_log_path(const std::filesystem::path& path)
    {
        return path.parent_path() /
            (path.stem().string() + ".previous" + path.extension().string());
    }

    bool open_persistent_file(const std::ios::openmode mode)
    {
        g_file_output.clear();
        g_file_output.open(g_file_path, std::ios::binary | mode);
        return g_file_output.good();
    }

    bool rotate_persistent_file()
    {
        if (g_file_output.is_open())
            g_file_output.close();

        std::error_code ec;
        const std::filesystem::path backup = rotated_log_path(g_file_path);
        std::filesystem::remove(backup, ec);
        ec.clear();
        if (std::filesystem::exists(g_file_path, ec) && !ec)
        {
            std::filesystem::rename(g_file_path, backup, ec);
            if (ec)
            {
                ec.clear();
                std::filesystem::remove(g_file_path, ec);
            }
        }

        g_file_bytes = 0;
        if (!open_persistent_file(std::ios::trunc))
            return false;

        constexpr const char* continuation = "===== log rotated =====\n";
        g_file_output << continuation;
        g_file_output.flush();
        g_file_bytes = std::char_traits<char>::length(continuation);
        return true;
    }

    void write_file_text(const std::string& text, const bool flush_now = false)
    {
        if (!g_file_output.is_open() || text.empty())
            return;

        constexpr std::size_t rotation_headroom = 128;
        constexpr const char* truncation_marker = "\n...[log entry truncated]\n";
        const std::size_t max_text_bytes =
            static_cast<std::size_t>(max_persistent_log_bytes) - rotation_headroom;

        std::string bounded_text;
        const std::string* output = &text;
        if (text.size() > max_text_bytes)
        {
            const std::size_t marker_size = std::char_traits<char>::length(truncation_marker);
            bounded_text.assign(text.data(), max_text_bytes - marker_size);
            bounded_text += truncation_marker;
            output = &bounded_text;
        }

        if (g_file_bytes + output->size() > max_persistent_log_bytes && !rotate_persistent_file())
            return;

        g_file_output << *output;
        if (flush_now)
            g_file_output.flush();
        if (g_file_output.good())
            g_file_bytes += output->size();
    }

    void write_file_entry(const ta::foundation::logger::Entry& entry)
    {
        write_file_text(format_entry(entry) + '\n',
                        entry.level == ta::foundation::logger::Level::Error ||
                        entry.level == ta::foundation::logger::Level::Fatal);
    }

    void append_entry(const ta::foundation::logger::Level level,
                      std::string channel,
                      std::string message)
    {
        std::lock_guard lock(g_mutex);

        ta::foundation::logger::Entry entry{};
        entry.sequence = ++g_sequence;
        entry.level = level;
        entry.thread_id = current_thread_id();
        entry.timestamp = current_timestamp();
        entry.channel = channel.empty() ? "GENERAL" : std::move(channel);
        entry.message = std::move(message);

        if (g_history.size() >= max_history_entries)
            g_history.pop_front();
        g_history.push_back(entry);

        if (g_sink)
            g_sink->write(entry);
        write_file_entry(entry);
    }

    bool write_history_file(const std::filesystem::path& path,
                            const std::deque<ta::foundation::logger::Entry>& entries)
    {
        if (!ta::foundation::io::ensure_parent_directory(path))
            return false;

        std::ofstream output(path, std::ios::binary | std::ios::trunc);
        if (!output.good())
            return false;

        for (const ta::foundation::logger::Entry& entry : entries)
            output << format_entry(entry) << '\n';

        output.close();
        return !output.fail();
    }
}

namespace ta::foundation::logger
{
    const char* level_name(const Level level) noexcept
    {
        switch (level)
        {
        case Level::Path: return "TRACE";
        case Level::Info: return "INFO";
        case Level::Warning: return "WARN";
        case Level::Error: return "ERROR";
        case Level::Fatal: return "FATAL";
        default: return "UNKNOWN";
        }
    }

    void set_sink(Sink* sink) noexcept
    {
        std::lock_guard lock(g_mutex);
        g_sink = sink;
    }

    bool set_file_output(const std::filesystem::path& path)
    {
        std::lock_guard lock(g_mutex);

        if (g_file_output.is_open())
            g_file_output.close();

        if (!ta::foundation::io::ensure_parent_directory(path))
        {
            g_file_path.clear();
            return false;
        }

        std::error_code ec;
        g_file_path = path;
        g_file_bytes = 0;
        ec.clear();
        if (std::filesystem::exists(g_file_path, ec) && !ec)
        {
            g_file_bytes = std::filesystem::file_size(g_file_path, ec);
            if (ec)
                g_file_bytes = 0;
        }

        if (g_file_bytes >= max_persistent_log_bytes)
        {
            if (!rotate_persistent_file())
            {
                g_file_path.clear();
                return false;
            }
        }
        else if (!open_persistent_file(std::ios::app))
        {
            g_file_path.clear();
            return false;
        }

        write_file_text("\n===== session start =====\n");
        for (const Entry& entry : g_history)
            write_file_entry(entry);
        return true;
    }

    void close_file_output()
    {
        std::lock_guard lock(g_mutex);
        if (g_file_output.is_open())
            g_file_output.close();
        g_file_bytes = 0;
    }

    std::filesystem::path file_output_path()
    {
        std::lock_guard lock(g_mutex);
        return g_file_path;
    }

    void trace(std::string msg) { append_entry(Level::Path, "GENERAL", std::move(msg)); }
    void info(std::string msg) { append_entry(Level::Info, "GENERAL", std::move(msg)); }
    void warn(std::string msg) { append_entry(Level::Warning, "GENERAL", std::move(msg)); }
    void error(std::string msg) { append_entry(Level::Error, "GENERAL", std::move(msg)); }
    void fatal(std::string msg) { append_entry(Level::Fatal, "GENERAL", std::move(msg)); }
    void trace(std::string channel, std::string msg) { append_entry(Level::Path, std::move(channel), std::move(msg)); }
    void info(std::string channel, std::string msg) { append_entry(Level::Info, std::move(channel), std::move(msg)); }
    void warn(std::string channel, std::string msg) { append_entry(Level::Warning, std::move(channel), std::move(msg)); }
    void error(std::string channel, std::string msg) { append_entry(Level::Error, std::move(channel), std::move(msg)); }
    void fatal(std::string channel, std::string msg) { append_entry(Level::Fatal, std::move(channel), std::move(msg)); }

    void path(const std::uint64_t trace_id,
              std::string component,
              std::string stage,
              std::string detail)
    {
        std::ostringstream message;
        message << "trace=" << trace_id
                << " component=" << component
                << " stage=" << stage;
        if (!detail.empty())
            message << " detail=" << detail;
        append_entry(Level::Path, "PATH", message.str());
    }

    std::vector<Entry> snapshot(const std::size_t max_entries)
    {
        std::lock_guard lock(g_mutex);
        const std::size_t count = max_entries == 0
            ? g_history.size()
            : (std::min)(max_entries, g_history.size());
        return {g_history.end() - static_cast<std::ptrdiff_t>(count), g_history.end()};
    }

    void clear_history()
    {
        std::lock_guard lock(g_mutex);
        g_history.clear();
    }

    bool save_history()
    {
        std::lock_guard lock(g_mutex);
        const std::filesystem::path path = history_snapshot_path();
        return !path.empty() && write_history_file(path, g_history);
    }

    std::filesystem::path history_snapshot_path()
    {
        if (g_file_path.empty())
            return {};
        return g_file_path.parent_path() /
            (g_file_path.stem().string() + ".snapshot" + g_file_path.extension().string());
    }
}
