/* -------------------------------------------------------------------------
 * BKG NTRIP Client
 * -------------------------------------------------------------------------
 *
 * Class:      bncCredentials
 *
 * Purpose:    Passwords from credentials files (netrc or BNC format)
 *             instead of the configuration file
 *
 * Created:    31-Oct-2026
 *
 * Changes:
 *
 * -----------------------------------------------------------------------*/

#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMap>
#include <QMutex>
#include <QSet>

#include "bnccredentials.h"
#include "bnccore.h"
#include "bncsettings.h"
#include "bncutils.h"

namespace {

struct t_entry {
  QString host;
  int     port;     // 0: any port
  bool    hasUser;
  QString user;
  QString password;
};

struct t_file {
  t_file() : exists(false), size(-1), refused(false) {}
  bool           exists;
  QDateTime      modified;
  qint64         size;
  bool           refused;   // readable for others
  QList<t_entry> entries;
};

QMutex                 _mutex;
QMap<QString, t_file>  _files;
QSet<QString>          _reported;

// Log a message once per key
void message(const QString& key, const QString& msg) {
  if (!_reported.contains(key)) {
    _reported.insert(key);
    BNC_CORE->slotMessage(msg.toLatin1(), true);
  }
}

// Split a line into tokens: white space separated, "..." quoted (with \"
// and \\), '#' outside quotes starts a comment
QStringList tokenize(const QString& line) {
  QStringList tokens;
  int ii = 0;
  while (ii < line.size()) {
    if (line[ii].isSpace()) {
      ++ii;
      continue;
    }
    if (line[ii] == '#') {
      break;
    }
    QString token;
    if (line[ii] == '"') {
      for (++ii; ii < line.size() && line[ii] != '"'; ++ii) {
        if (line[ii] == '\\' && ii + 1 < line.size()) {
          ++ii;
        }
        token += line[ii];
      }
      ++ii; // closing quote
    }
    else {
      for (; ii < line.size() && !line[ii].isSpace(); ++ii) {
        token += line[ii];
      }
    }
    tokens << token;
  }
  return tokens;
}

// "host" or "host:port"
void hostAndPort(const QString& name, t_entry& entry) {
  entry.host = name;
  entry.port = 0;
  int ic = name.lastIndexOf(':');
  if (ic > 0) {
    bool ok;
    int port = name.mid(ic + 1).toInt(&ok);
    if (ok && port > 0) {
      entry.host = name.left(ic);
      entry.port = port;
    }
  }
}

void parseNetrc(const QList<QStringList>& lines, t_file& file) {
  t_entry* entry = 0;
  bool     isDefault = false;
  for (int iLine = 0; iLine < lines.size(); iLine++) {
    const QStringList& tokens = lines[iLine];
    for (int ii = 0; ii < tokens.size(); ii++) {
      const QString& key = tokens[ii];
      QString value = (ii + 1 < tokens.size()) ? tokens[ii + 1] : QString();
      if (key == "machine") {
        file.entries.append(t_entry());
        entry = &file.entries.last();
        entry->hasUser = false;
        hostAndPort(value, *entry);
        isDefault = false;
        ++ii;
      }
      else if (key == "default") { // for all hosts: never used by BNC
        entry = 0;
        isDefault = true;
      }
      else if (key == "login" || key == "password" || key == "account") {
        if (entry && !isDefault) {
          if (key == "login") {
            entry->hasUser = true;
            entry->user = value;
          }
          else if (key == "password") {
            entry->password = value;
          }
        }
        ++ii;
      }
      else if (key == "macdef") { // macro definition up to an empty line
        while (iLine + 1 < lines.size() && !lines[iLine + 1].isEmpty()) {
          ++iLine;
        }
        break;
      }
    }
  }
}

void parseBnc(const QString& fileName, const QList<QStringList>& lines,
              const QList<int>& lineNumbers, t_file& file) {
  for (int iLine = 0; iLine < lines.size(); iLine++) {
    const QStringList& tokens = lines[iLine];
    if (tokens.isEmpty()) {
      continue;
    }
    // <host> <port|*> <user|-> <password>, or <host> <user> <password> for any port
    QStringList hlp = tokens;
    if (hlp.size() == 3) {
      hlp.insert(1, "*");
    }
    bool ok = hlp.size() == 4;
    t_entry entry;
    entry.host = hlp.value(0);
    if (hlp.value(1) == "*") {
      entry.port = 0;
    }
    else {
      bool okPort;
      entry.port = hlp.value(1).toInt(&okPort);
      ok = ok && okPort && entry.port > 0;
    }
    entry.hasUser = hlp.value(2) != "-";
    entry.user = entry.hasUser ? hlp.value(2) : QString();
    entry.password = hlp.value(3);
    if (ok) {
      file.entries.append(entry);
    }
    else {
      message(fileName + QString(":%1").arg(lineNumbers[iLine]),
              QString("Credentials file %1, line %2: not <host> [<port>] <user> <password>, ignored")
              .arg(fileName).arg(lineNumbers[iLine]));
    }
  }
}

// Entries of a file, read again if it changed
const t_file& readFile(const QString& fileName) {
  t_file& file = _files[fileName];
  QFileInfo info(fileName);
  if (info.exists() == file.exists && (!file.exists ||
      (info.lastModified() == file.modified && info.size() == file.size))) {
    return file;
  }
  file = t_file();
  file.exists = info.exists();
  if (!file.exists) {
    return file;
  }
  file.modified = info.lastModified();
  file.size     = info.size();

#ifndef Q_OS_WIN
  if (info.permissions() & (QFile::ReadGroup | QFile::ReadOther)) {
    file.refused = true;
    message("perm:" + fileName,
            QString("Credentials file %1 is readable for other users, not used (chmod 600)").arg(fileName));
    return file;
  }
#endif

  QFile inp(fileName);
  if (!inp.open(QIODevice::ReadOnly | QIODevice::Text)) {
    message("open:" + fileName, QString("Cannot read credentials file %1").arg(fileName));
    return file;
  }
  QList<QStringList> lines;
  QList<int>         lineNumbers;
  int                lineNumber = 0;
  while (!inp.atEnd()) {
    QString line = QString::fromUtf8(inp.readLine());
    ++lineNumber;
    QStringList tokens = tokenize(line);
    if (tokens.isEmpty() && line.trimmed().startsWith('#')) {
      continue; // comment line, not the end of a netrc macro
    }
    lines << tokens;
    lineNumbers << lineNumber;
  }

  // netrc, if the first token is a netrc keyword
  QString first;
  for (int ii = 0; ii < lines.size() && first.isEmpty(); ii++) {
    first = lines[ii].value(0);
  }
  if (first == "machine" || first == "default" || first == "macdef") {
    parseNetrc(lines, file);
  }
  else {
    parseBnc(fileName, lines, lineNumbers, file);
  }
  return file;
}

bool sameUser(const t_entry& entry, const QString& user) {
  return user.isEmpty() ? (!entry.hasUser || entry.user.isEmpty())
                        : (entry.hasUser && entry.user == user);
}

}  // namespace

// Credentials files: option 'credentialsFile' (comma separated), default
// $NETRC or ~/.netrc (Windows: %HOME%\_netrc)
////////////////////////////////////////////////////////////////////////////
QStringList bncCredentials::fileNames() {
  bncSettings settings;
  QStringList names;
  QStringList hlp = settings.value("credentialsFile").toString().split(',', Qt::SkipEmptyParts);
  for (int ii = 0; ii < hlp.size(); ii++) {
    QString name = hlp[ii].trimmed();
    expandEnvVar(name);
    if (name.startsWith("~/")) {
      name = QDir::homePath() + name.mid(1);
    }
    if (!name.isEmpty()) {
      names << name;
    }
  }
  if (names.isEmpty()) {
    QString netrc = QString::fromLocal8Bit(qgetenv("NETRC"));
    if (netrc.isEmpty()) {
#ifdef Q_OS_WIN
      netrc = QDir::homePath() + "/_netrc";
#else
      netrc = QDir::homePath() + "/.netrc";
#endif
    }
    names << netrc;
  }
  return names;
}

// Password of user at host:port
////////////////////////////////////////////////////////////////////////////
bool bncCredentials::password(const QString& host, int port, const QString& user,
                              QString& password) {
  QMutexLocker locker(&_mutex);
  QStringList names = fileNames();
  for (int iFile = 0; iFile < names.size(); iFile++) {
    const t_file& file = readFile(names[iFile]);
    if (file.refused) {
      continue;
    }
    // exact port first, then entries for any port
    for (int pass = 0; pass < 2; pass++) {
      for (int ii = 0; ii < file.entries.size(); ii++) {
        const t_entry& entry = file.entries[ii];
        if (entry.host.compare(host, Qt::CaseInsensitive) == 0 &&
            (pass == 0 ? (port > 0 && entry.port == port) : entry.port == 0) &&
            sameUser(entry, user)) {
          password = entry.password;
          return true;
        }
      }
    }
  }
  return false;
}

// Stream URL
////////////////////////////////////////////////////////////////////////////
void bncCredentials::complete(QUrl& url) {
  if (url.userName().isEmpty()) {
    return; // anonymous
  }
  QString user = QUrl::fromPercentEncoding(url.userName().toLatin1());
  QString pass = url.password();
  complete(url.host(), url.port(0), user, pass, false, true);
  if (url.password().isEmpty() && !pass.isEmpty()) {
    url.setPassword(pass, QUrl::DecodedMode);
  }
}

// Upload or sourcetable
////////////////////////////////////////////////////////////////////////////
void bncCredentials::complete(const QString& host, int port, const QString& user,
                              QString& password, bool anyUser, bool hint) {
  QString account = (user.isEmpty() ? QString() : user + "@") + host + QString(":%1").arg(port);
  if (!password.isEmpty()) {
    if (hint) {
      QMutexLocker locker(&_mutex);
      message("config:" + account,
              QString("%1: password in the configuration file, it can be moved to a credentials file "
                      "(option 'credentialsFile', default ~/.netrc)").arg(account));
    }
    return;
  }
  if (user.isEmpty() && !anyUser) {
    return;
  }
  if (!bncCredentials::password(host, port, user, password)) {
    QMutexLocker locker(&_mutex);
    message("none:" + account, QString("%1: no password in the configuration file nor in %2")
            .arg(account).arg(fileNames().join(", ")));
  }
}
