/* -------------------------------------------------------------------------
 * BKG NTRIP Client
 * -------------------------------------------------------------------------
 *
 * Class:      t_logRecord
 *
 * Purpose:    Record of the structured logfile (JSON Lines or logfmt)
 *
 * Created:    04-Nov-2026
 *
 * Changes:
 *
 * -----------------------------------------------------------------------*/

#include <cmath>

#include <QMutex>
#include <QSet>
#include <QString>

#include "bnclog.h"
#include "bncutils.h"
#include "bnctime.h"

namespace {

QMutex          streamMutex;
QSet<QByteArray> streams;

// text as UTF-8 (BNC's messages are Latin-1)
QByteArray utf8(const QByteArray& latin1) {
  return QString::fromLatin1(latin1).toUtf8();
}

QByteArray jsonString(const QByteArray& latin1) {
  QByteArray in = utf8(latin1);
  QByteArray out = "\"";
  for (char c : in) {
    unsigned char u = static_cast<unsigned char>(c);
    switch (c) {
      case '"':  out += "\\\""; break;
      case '\\': out += "\\\\"; break;
      case '\n': out += "\\n";  break;
      case '\r': out += "\\r";  break;
      case '\t': out += "\\t";  break;
      default:
        if (u < 0x20) {
          out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0');
        }
        else {
          out += c;
        }
    }
  }
  return out + "\"";
}

QByteArray logfmtValue(const QByteArray& latin1) {
  QByteArray in = utf8(latin1);
  bool quote = in.isEmpty();
  for (char c : in) {
    unsigned char u = static_cast<unsigned char>(c);
    if (u <= 0x20 || c == '=' || c == '"' || c == '\\') {
      quote = true;
      break;
    }
  }
  if (!quote) {
    return in;
  }
  QByteArray out = "\"";
  for (char c : in) {
    unsigned char u = static_cast<unsigned char>(c);
    switch (c) {
      case '"':  out += "\\\""; break;
      case '\\': out += "\\\\"; break;
      case '\n': out += "\\n";  break;
      case '\r': out += "\\r";  break;
      case '\t': out += "\\t";  break;
      default:
        if (u < 0x20) {
          out += QByteArray("\\u00") + QByteArray::number(u, 16).rightJustified(2, '0');
        }
        else {
          out += c;
        }
    }
  }
  return out + "\"";
}

bool containsWord(const QByteArray& lower, const char* word) {
  return lower.contains(word);
}

}

// Envelope of a plain text message
////////////////////////////////////////////////////////////////////////////
t_logRecord t_logRecord::fromMessage(const QByteArray& msg, const QByteArray& component) {
  t_logRecord rec(info, component, "message", "", msg);

  // stream: first token of the text, if it is a known stream or PPP station
  int end = 0;
  while (end < msg.size() && msg[end] != ':' && msg[end] != ' ' && msg[end] != '(') {
    ++end;
  }
  if (end > 0) {
    QByteArray token = msg.left(end);
    QMutexLocker locker(&streamMutex);
    if (streams.contains(token)) {
      rec._stream = token;
    }
  }

  // level from keywords
  QByteArray lower = msg.toLower();
  static const char* errorWords[] = {"error", "fail", "cannot", "can't", "could not",
                                     "unable", "exception", 0};
  static const char* warningWords[] = {"warning", "outage", "corrupt", "timeout",
                                       "wrong", "invalid", "ignored", "refused",
                                       "unauthorized", "not found", "missing",
                                       "unknown", 0};
  for (int ii = 0; errorWords[ii]; ii++) {
    if (containsWord(lower, errorWords[ii])) {
      rec._level = error;
      return rec;
    }
  }
  for (int ii = 0; warningWords[ii]; ii++) {
    if (containsWord(lower, warningWords[ii])) {
      rec._level = warning;
      return rec;
    }
  }
  return rec;
}

// Plain text message with an explicit level
////////////////////////////////////////////////////////////////////////////
t_logRecord t_logRecord::message(e_level level, const QByteArray& msg,
                                 const QByteArray& component) {
  t_logRecord rec = fromMessage(msg, component);
  rec._level = level;
  return rec;
}

// Message of a BNC_DEBUG_* build
////////////////////////////////////////////////////////////////////////////
t_logRecord t_logRecord::debugMessage(const QByteArray& component, const QByteArray& msg) {
  t_logRecord rec = fromMessage(msg, component);
  rec._level = debug;
  return rec;
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord& t_logRecord::add(const char* key, double value, int precision) {
  t_field ff;
  ff._key      = key;
  ff._isString = false;
  if (std::isfinite(value)) {
    ff._value = QByteArray::number(value, 'f', precision);   // C locale
  }
  else {
    ff._value = "null";
  }
  _fields.append(ff);
  return *this;
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord& t_logRecord::add(const char* key, int value) {
  t_field ff;
  ff._key      = key;
  ff._value    = QByteArray::number(value);
  ff._isString = false;
  _fields.append(ff);
  return *this;
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord& t_logRecord::add(const char* key, bool value) {
  t_field ff;
  ff._key      = key;
  ff._value    = value ? "true" : "false";
  ff._isString = false;
  _fields.append(ff);
  return *this;
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord& t_logRecord::add(const char* key, const QByteArray& value) {
  t_field ff;
  ff._key      = key;
  ff._value    = value;
  ff._isString = true;
  _fields.append(ff);
  return *this;
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord& t_logRecord::addTime(const char* key, const QDateTime& utc) {
  QDateTime tt = utc.toUTC();
  return add(key, tt.toString("yyyy-MM-ddThh:mm:ss.zzzZ").toLatin1());
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord& t_logRecord::setEpoch(const bncTime& epoch) {
  if (epoch.valid()) {
    _epoch = QByteArray((epoch.datestr() + 'T' + epoch.timestr(3)).c_str());
  }
  else {
    _epoch.clear();
  }
  return *this;
}

//
////////////////////////////////////////////////////////////////////////////
const char* t_logRecord::levelString(e_level level) {
  switch (level) {
    case debug:   return "debug";
    case warning: return "warning";
    case error:   return "error";
    default:      return "info";
  }
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord::e_format t_logRecord::formatFromString(const QString& str) {
  return (str.trimmed().toLower() == "logfmt") ? logfmt : json;
}

//
////////////////////////////////////////////////////////////////////////////
t_logRecord::e_level t_logRecord::levelFromString(const QString& str) {
  QString ss = str.trimmed().toLower();
  if (ss == "info")    return info;
  if (ss == "warning") return warning;
  if (ss == "error")   return error;
  return debug;
}

//
////////////////////////////////////////////////////////////////////////////
void t_logRecord::registerStream(const QByteArray& stream) {
  if (stream.isEmpty()) {
    return;
  }
  QMutexLocker locker(&streamMutex);
  streams.insert(stream);
}

// One line of the structured logfile
////////////////////////////////////////////////////////////////////////////
QByteArray t_logRecord::encode(e_format format, const QDateTime& gpst) const {

  // UTC = GPS time - leap seconds
  QDate dd = gpst.date();
  QDateTime utc = gpst.addSecs(-gnumleap(dd.year(), dd.month(), dd.day()));

  QByteArray msg = _msgStructured.isEmpty() ? _msg : _msgStructured;
  while (msg.startsWith('\n')) {
    msg.remove(0, 1);
  }
  // the mountpoint is a key of its own
  if (!_stream.isEmpty() && msg.startsWith(_stream + ": ")) {
    msg.remove(0, _stream.size() + 2);
  }
  // no trailing blanks (e.g. RTCM scan: "Receiver Serial Number: 1831551 ")
  while (msg.endsWith(' ')) {
    msg.chop(1);
  }

  QVector<t_field> all;
  auto put = [&all](const char* key, const QByteArray& value, bool isString) {
    t_field ff;
    ff._key = key; ff._value = value; ff._isString = isString;
    all.append(ff);
  };
  put("time",      utc.toString("yyyy-MM-ddThh:mm:ss.zzz").toLatin1() + "Z", true);
  if (!_epoch.isEmpty()) {
    put("epoch",   _epoch, true);
  }
  put("level",     levelString(_level), true);
  put("component", _component, true);
  put("event",     _event, true);
  if (!_stream.isEmpty()) {
    put("mp", _stream, true);
  }
  all += _fields;
  put("msg", msg, true);
  put("v",   QByteArray::number(version), false);

  QByteArray line;
  if (format == logfmt) {
    for (int ii = 0; ii < all.size(); ii++) {
      if (ii) {
        line += ' ';
      }
      line += all[ii]._key + '=';
      line += all[ii]._isString ? logfmtValue(all[ii]._value) : all[ii]._value;
    }
  }
  else {
    line = "{";
    for (int ii = 0; ii < all.size(); ii++) {
      if (ii) {
        line += ',';
      }
      line += '"' + all[ii]._key + "\":";
      line += all[ii]._isString ? jsonString(all[ii]._value) : all[ii]._value;
    }
    line += '}';
  }
  return line;
}
