#include "fgc/Logger.h"

#include <atomic>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <iostream>
#include <mutex>

namespace fgc {

namespace {

std::atomic<LogLevel> g_level{LogLevel::Info};
std::atomic<unsigned> g_cats{0};   // every wire-trace category OFF by default
std::mutex            g_mutex;
Logger::LogSink       g_sink;      // empty => default stdout/stderr writer (below)

// Default writer: the stdout/stderr split this logger has always used.
void writeDefault(LogLevel level, const std::string& line) {
    std::ostream& out = (level >= LogLevel::Warn) ? std::cerr : std::cout;
    out << line << '\n';
    out.flush();
}

const char* tag(LogLevel l) {
    switch (l) {
        case LogLevel::Trace: return "TRACE";
        case LogLevel::Debug: return "DEBUG";
        case LogLevel::Info:  return "INFO ";
        case LogLevel::Warn:  return "WARN ";
        case LogLevel::Error: return "ERROR";
        case LogLevel::Off:   return "OFF  ";
    }
    return "?????";
}

const char* catTag(LogCat c) {
    switch (c) {
        case LogCat::Serial:  return "SERIAL";
        case LogCat::Mqtt:    return "MQTT";
        case LogCat::Camera:  return "CAMERA";
        case LogCat::Control: return "CONTROL";
        default:              return "TRACE";
    }
}

std::string lower(const std::string& s) {
    std::string v;
    for (char c : s) v += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
    return v;
}

// Trim leading/trailing ASCII whitespace.
std::string trim(const std::string& s) {
    size_t b = s.find_first_not_of(" \t\r\n");
    if (b == std::string::npos) return "";
    size_t e = s.find_last_not_of(" \t\r\n");
    return s.substr(b, e - b + 1);
}

std::string timestamp() {
    using namespace std::chrono;
    auto now = system_clock::now();
    auto t   = system_clock::to_time_t(now);
    auto ms  = duration_cast<milliseconds>(now.time_since_epoch()) % 1000;

    std::tm tm{};
    localtime_r(&t, &tm);

    char buf[32];
    std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d.%03d", tm.tm_hour, tm.tm_min,
                  tm.tm_sec, static_cast<int>(ms.count()));
    return buf;
}

}  // namespace

void Logger::setSink(LogSink sink) {
    std::lock_guard<std::mutex> lock(g_mutex);
    g_sink = std::move(sink);
}

void     Logger::setLevel(LogLevel level) { g_level.store(level); }
LogLevel Logger::level() { return g_level.load(); }
bool     Logger::enabled(LogLevel level) { return level >= g_level.load(); }

bool Logger::setLevelFromString(const std::string& s) {
    std::string v = lower(s);
    if (v == "trace") { setLevel(LogLevel::Trace); return true; }
    if (v == "debug") { setLevel(LogLevel::Debug); return true; }
    if (v == "info")  { setLevel(LogLevel::Info);  return true; }
    if (v == "warn" || v == "warning") { setLevel(LogLevel::Warn); return true; }
    if (v == "error") { setLevel(LogLevel::Error); return true; }
    if (v == "off" || v == "none") { setLevel(LogLevel::Off); return true; }
    return false;
}

void     Logger::setCategories(unsigned mask) { g_cats.store(mask); }
unsigned Logger::categories() { return g_cats.load(); }
void Logger::enableCategory(LogCat cat)  { g_cats.fetch_or(static_cast<unsigned>(cat)); }
void Logger::disableCategory(LogCat cat) { g_cats.fetch_and(~static_cast<unsigned>(cat)); }
bool Logger::categoryEnabled(LogCat cat) {
    return (g_cats.load() & static_cast<unsigned>(cat)) != 0;
}

LogCat Logger::catFromString(const std::string& name, bool* ok) {
    std::string v = lower(trim(name));
    if (ok) *ok = true;
    if (v == "serial")  return LogCat::Serial;
    if (v == "mqtt")    return LogCat::Mqtt;
    if (v == "camera")  return LogCat::Camera;
    if (v == "control") return LogCat::Control;
    if (v == "all")     return LogCat::All;
    if (v == "none" || v.empty()) return LogCat::None;
    if (ok) *ok = false;
    return LogCat::None;
}

unsigned Logger::parseCategories(const std::string& csv, bool* ok) {
    if (ok) *ok = true;
    unsigned mask = 0;
    size_t start = 0;
    while (start <= csv.size()) {
        size_t comma = csv.find(',', start);
        std::string token = csv.substr(start, comma == std::string::npos ? std::string::npos
                                                                          : comma - start);
        std::string t = trim(token);
        if (!t.empty()) {
            bool tok_ok = true;
            mask |= static_cast<unsigned>(catFromString(t, &tok_ok));
            if (!tok_ok && ok) *ok = false;
        }
        if (comma == std::string::npos) break;
        start = comma + 1;
    }
    return mask;
}

LogStream::LogStream(LogLevel level) : level_(level), enabled_(Logger::enabled(level)) {}

LogStream::LogStream(LogLevel level, LogCat cat)
    : level_(level), cat_(cat),
      enabled_(Logger::categoryEnabled(cat) && Logger::level() != LogLevel::Off) {}

void LogStream::commit() {
    done_ = true;
    if (!enabled_) return;
    const char* label = (cat_ != LogCat::None) ? catTag(cat_) : tag(level_);
    std::string line = timestamp() + " [" + label + "] " + buffer_.str();
    std::lock_guard<std::mutex> lock(g_mutex);
    if (g_sink) g_sink(level_, line);
    else        writeDefault(level_, line);
}

}  // namespace fgc
