/* -------------------------------------------------------------------------
 * 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"

namespace {

QMutex          stationMutex;
QSet<QByteArray> stations;

// 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) {
  t_logRecord rec(info, "bnc", "message", "", msg);

  // station: first token of the text, if it is a known station name
  int end = 0;
  while (end < msg.size() && msg[end] != ':' && msg[end] != ' ' && msg[end] != '(') {
    ++end;
  }
  if (end > 0) {
    QByteArray token = msg.left(end);
    QMutexLocker locker(&stationMutex);
    if (stations.contains(token)) {
      rec._station = 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;
}

//
////////////////////////////////////////////////////////////////////////////
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());
}

//
////////////////////////////////////////////////////////////////////////////
const char* t_logRecord::levelString(e_level level) {
  switch (level) {
    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;
}

//
////////////////////////////////////////////////////////////////////////////
void t_logRecord::registerStation(const QByteArray& station) {
  if (station.isEmpty()) {
    return;
  }
  QMutexLocker locker(&stationMutex);
  stations.insert(station);
}

// 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 = _msg;
  while (msg.startsWith('\n')) {
    msg.remove(0, 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);
  put("gpst",      gpst.toString("yyyy-MM-ddThh:mm:ss.zzz").toLatin1(), true);
  put("level",     levelString(_level), true);
  put("component", _component, true);
  put("event",     _event, true);
  if (!_station.isEmpty()) {
    put("station", _station, 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;
}
