/*
  ISC License

  Copyright (c) 2026, Antonio SJ Musumeci <trapexit@spawn.link>

  Permission to use, copy, modify, and/or distribute this software for any
  purpose with or without fee is hereby granted, provided that the above
  copyright notice and this permission notice appear in all copies.

  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include "config.hpp"
#include "errno.hpp"
#include "fmt/core.h"
#include "fs_path.hpp"
#include "nonstd/string.hpp"
#include "str.hpp"
#include "version.hpp"

#include <fstream>
#include <string>

#include <string.h>


constexpr const char CACHE_FILES_PROCESS_NAMES_DEFAULT[] =
  "rtorrent|"
  "qbittorrent-nox";

Config cfg;

thread_local int Config::CfgConfigFile::_depth = 0;

Config::CfgConfigFile::CfgConfigFile()
{
}

int
Config::CfgConfigFile::from_string(const std::string_view s_)
{
  int rv;
  fs::path cfg_file;

  if(_depth > 5)
    return -ELOOP;
  _depth++;
  struct DepthGuard { int &d; ~DepthGuard() { --d; } } guard{_depth};

  cfg_file = (s_.empty() ? _cfg_file : s_);

  rv = cfg.from_file(cfg_file);
  if(rv == 0)
    _cfg_file = cfg_file;

  return rv;
}

std::string
Config::CfgConfigFile::to_string() const
{
  return _cfg_file;
}


Config::Config()
  :
  allow_idmap(true),
  async_read(true),
  branches(),
  branches_mount_timeout(0),
  branches_mount_timeout_fail(false),
  cache_attr(1),
  cache_entry(1),
  cache_files(CacheFiles::ENUM::OFF),
  cache_files_process_names(CACHE_FILES_PROCESS_NAMES_DEFAULT),
  cache_negative_entry(0),
  cache_readdir(false),
  cache_statfs(0),
  cache_symlinks(false),
  cache_writeback(false),
  category(func),
  config_file(),
  direct_io_allow_mmap(true),
  dropcacheonclose(false),
  export_support(true),
  flushonclose(FlushOnClose::ENUM::OPENED_FOR_WRITE),
  follow_symlinks(FollowSymlinks::ENUM::NEVER),
  fsname(),
  func(),
  fuse_msg_size("1M"),
  handle_killpriv(true),
  handle_killpriv_v2(true),
  ignorepponrename(false),
  inodecalc("hybrid-hash"),
  kernel_permissions_check(true),
  lazy_umount_mountpoint(false),
  link_cow(false),
  link_exdev(LinkEXDEV::ENUM::PASSTHROUGH),
  log_file({}),
  minfreespace(branches.minfreespace),
  mountpoint(),
  moveonenospc(true),
  nfsopenhack(NFSOpenHack::ENUM::OFF),
  nullrw(false),
  parallel_direct_writes(true),
  passthrough_io(PassthroughIO::ENUM::OFF),
  passthrough_max_stack_depth(fuse_cfg.passthrough_max_stack_depth),
  pin_threads(fuse_cfg.pin_threads),
  posix_acl(false),
  process_thread_count(fuse_cfg.process_thread_count),
  process_thread_queue_depth(fuse_cfg.process_thread_queue_depth),
  proxy_ioprio(false),
  read_thread_count(fuse_cfg.read_thread_count),
  readahead(0),
  readdir("seq"),
  rename_exdev(RenameEXDEV::ENUM::PASSTHROUGH),
  scheduling_priority(-10),
  security_capability(true),
  statfs(StatFS::ENUM::BASE),
  statfs_ignore(StatFSIgnore::ENUM::NONE),
  symlinkify(false),
  symlinkify_timeout(3600),
  xattr(XAttr::ENUM::PASSTHROUGH),

  _congestion_threshold(fuse_cfg.congestion_threshold),
  _debug(false),
  _gid(fuse_cfg.gid),
  _max_background(fuse_cfg.max_background),
  _mount(mountpoint),
  _mountpoint(mountpoint),
  _never_forget_nodes(),
  _noforget(),
  _remember(fuse_cfg.remember_nodes),
  _remember_nodes(fuse_cfg.remember_nodes),
  _srcmounts(branches),
  _threads(fuse_cfg.read_thread_count),
  _uid(fuse_cfg.uid),
  _umask(fuse_cfg.umask),
  _version(MERGERFS_VERSION),

  _initialized(false)
{
  allow_idmap.ro =
    async_read.ro =
    branches_mount_timeout.ro =
    branches_mount_timeout_fail.ro =
    cache_symlinks.ro =
    cache_writeback.ro =
    _congestion_threshold.ro =
    direct_io_allow_mmap.ro =
    export_support.ro =
    fsname.ro =
    fuse_msg_size.ro =
    handle_killpriv.ro =
    handle_killpriv_v2.ro =
    kernel_permissions_check.ro =
    _max_background.ro =
    _mount.ro =
    _mountpoint.ro =
    nullrw.ro =
    pid.ro =
    pin_threads.ro =
    posix_acl.ro =
    process_thread_count.ro =
    process_thread_queue_depth.ro =
    read_thread_count.ro =
    scheduling_priority.ro =
    _srcmounts.ro =
    _version.ro =
    true;
  _congestion_threshold.display =
    _gid.display =
    _max_background.display =
    _mount.display =
    _noforget.display =
    _remember.display =
    _srcmounts.display =
    _threads.display =
    _uid.display =
    _umask.display =
    false;

  _map["allow-idmap"]                 = &allow_idmap;
  _map["async-read"]                  = &async_read;
  _map["atomic-o-trunc"]              = &_dummy;
  _map["auto-cache"]                  = &_dummy;
  _map["big-writes"]                  = &_dummy;
  _map["branches"]                    = &branches;
  _map["branches-mount-timeout"]      = &branches_mount_timeout;
  _map["branches-mount-timeout-fail"] = &branches_mount_timeout_fail;
  _map["cache.attr"]                  = &cache_attr;
  _map["cache.entry"]                 = &cache_entry;
  _map["cache.files"]                 = &cache_files;
  _map["cache.files.process-names"]   = &cache_files_process_names;
  _map["cache.negative-entry"]        = &cache_negative_entry;
  _map["cache.open"]                  = &_dummy;
  _map["cache.readdir"]               = &cache_readdir;
  _map["cache.statfs"]                = &cache_statfs;
  _map["cache.symlinks"]              = &cache_symlinks;
  _map["cache.writeback"]             = &cache_writeback;
  _map["category.action"]             = &category.action;
  _map["category.create"]             = &category.create;
  _map["category.search"]             = &category.search;
  _map["config"]                      = &config_file;
  _map["debug"]                       = &_debug;
  _map["defaults"]                    = &_dummy;
  _map["direct-io"]                   = &_dummy;
  _map["direct-io-allow-mmap"]        = &direct_io_allow_mmap;
  _map["dropcacheonclose"]            = &dropcacheonclose;
  _map["export-support"]              = &export_support;
  _map["flush-on-close"]              = &flushonclose;
  _map["follow-symlinks"]             = &follow_symlinks;
  _map["fsname"]                      = &fsname;
  _map["func.access"]                 = &func.access;
  _map["func.chmod"]                  = &func.chmod;
  _map["func.chown"]                  = &func.chown;
  _map["func.create"]                 = &func.create;
  _map["func.getattr"]                = &func.getattr;
  _map["func.getxattr"]               = &func.getxattr;
  _map["func.link"]                   = &func.link;
  _map["func.listxattr"]              = &func.listxattr;
  _map["func.mkdir"]                  = &func.mkdir;
  _map["func.mknod"]                  = &func.mknod;
  _map["func.open"]                   = &func.open;
  _map["func.readdir"]                = &readdir;
  _map["func.readlink"]               = &func.readlink;
  _map["func.removexattr"]            = &func.removexattr;
  _map["func.rename"]                 = &func.rename;
  _map["func.rmdir"]                  = &func.rmdir;
  _map["func.setxattr"]               = &func.setxattr;
  _map["func.symlink"]                = &func.symlink;
  _map["func.truncate"]               = &func.truncate;
  _map["func.unlink"]                 = &func.unlink;
  _map["func.utimens"]                = &func.utimens;
  _map["fuse-msg-size"]               = &fuse_msg_size;
  _map["gid"]                         = &_gid;
  _map["gid-cache.expire-timeout"]    = &_dummy;
  _map["gid-cache.remove-timeout"]    = &_dummy;
  _map["handle-killpriv"]             = &handle_killpriv;
  _map["handle-killpriv-v2"]          = &handle_killpriv_v2;
  _map["hard-remove"]                 = &_dummy;
  _map["ignorepponrename"]            = &ignorepponrename;
  _map["inodecalc"]                   = &inodecalc;
  _map["kernel-cache"]                = &_dummy;
  _map["kernel-permissions-check"]    = &kernel_permissions_check;
  _map["lazy-umount-mountpoint"]      = &lazy_umount_mountpoint;
  _map["link-cow"]                    = &link_cow;
  _map["link-exdev"]                  = &link_exdev;
  _map["log.file"]                    = &log_file;
  _map["minfreespace"]                = &minfreespace;
  _map["mount"]                       = &_mount;
  _map["mountpoint"]                  = &_mountpoint;
  _map["moveonenospc"]                = &moveonenospc;
  _map["negative-entry"]              = &_dummy;
  _map["never-forget-nodes"]          = &_never_forget_nodes;
  _map["nfsopenhack"]                 = &nfsopenhack;
  _map["no-splice-move"]              = &_dummy;
  _map["no-splice-read"]              = &_dummy;
  _map["no-splice-write"]             = &_dummy;
  _map["noforget"]                    = &_noforget;
  _map["nonempty"]                    = &_dummy;
  _map["nullrw"]                      = &nullrw;
  _map["parallel-direct-writes"]      = &parallel_direct_writes;
  _map["passthrough.io"]              = &passthrough_io;
  _map["passthrough.max-stack-depth"] = &passthrough_max_stack_depth;
  _map["pid"]                         = &pid;
  _map["pin-threads"]                 = &pin_threads;
  _map["posix-acl"]                   = &posix_acl;
  _map["process-thread-count"]        = &process_thread_count;
  _map["process-thread-queue-depth"]  = &process_thread_queue_depth;
  _map["proxy-ioprio"]                = &proxy_ioprio;
  _map["read-thread-count"]           = &read_thread_count;
  _map["readahead"]                   = &readahead;
  _map["remember"]                    = &_remember;
  _map["remember-nodes"]              = &_remember_nodes;
  _map["rename-exdev"]                = &rename_exdev;
  _map["scheduling-priority"]         = &scheduling_priority;
  _map["security-capability"]         = &security_capability;
  _map["splice-move"]                 = &_dummy;
  _map["splice-read"]                 = &_dummy;
  _map["splice-write"]                = &_dummy;
  _map["srcmounts"]                   = &_srcmounts;
  _map["statfs"]                      = &statfs;
  _map["statfs-ignore"]               = &statfs_ignore;
  _map["symlinkify"]                  = &symlinkify;
  _map["symlinkify-timeout"]          = &symlinkify_timeout;
  _map["threads"]                     = &_threads;
  _map["uid"]                         = &_uid;
  _map["umask"]                       = &_umask;
  _map["use-ino"]                     = &_dummy;
  _map["version"]                     = &_version;
  _map["xattr"]                       = &xattr;
}

bool
Config::has_key(const std::string &key_) const
{
  return _map.count(key_);
}

ssize_t
Config::keys_listxattr_size() const
{
  ssize_t rv;

  rv = 0;
  for(const auto &[key,val] : _map)
    {
      if(val->display == false)
        continue;

      rv += sizeof("user.mergerfs.");
      rv += key.size();
    }

  return rv;
}

ssize_t
Config::keys_listxattr(char   *list_,
                       size_t  size_) const
{
  char    *list = list_;
  ssize_t  size = size_;

  if(size_ == 0)
    return keys_listxattr_size();

  for(const auto &[key,val] : _map)
    {
      if(val->display == false)
        continue;

      static constexpr std::string_view prefix = "user.mergerfs.";
      ssize_t entry_size = (ssize_t)(prefix.size() + key.size() + 1);
      if(entry_size > size)
        return -ERANGE;
      memcpy(list, prefix.data(), prefix.size());
      list += prefix.size();
      memcpy(list, key.data(), key.size());
      list += key.size();
      *list++ = '\0';
      size -= entry_size;
    }

  return (list - list_);
}

int
Config::get(const std::string &key_,
            std::string       *val_) const
{
  std::string key;
  Str2TFStrMap::const_iterator i;

  key = str::replace(key_,'_','-');


  i = _map.find(key);
  if(i == _map.end())
    return -ENOATTR;

  *val_ = i->second->to_string();

  return 0;
}

int
Config::set(const std::string &key_,
            const std::string &value_)
{
  std::string key;
  Str2TFStrMap::iterator i;

  key = str::replace(key_,'_','-');

  i = _map.find(key);
  if(i == _map.end())
    return -ENOATTR;
  if(_initialized && i->second->ro)
    return -EROFS;

  return i->second->from_string(value_);
}

int
Config::set(const std::string &kv_)
{
  auto [key, val] = str::splitkv(kv_,'=');
  key = str::trim(key);
  val = str::trim(val);

  return set(key,val);
}

int
Config::from_stream(std::istream &istrm_)
{
  std::string line;
  Config::ErrVec new_errs;

  while(std::getline(istrm_,line,'\n'))
    {
      line = str::trim(line);
      if(line.empty() || (line[0] == '#'))
        continue;

      int rv = set(line);
      if(rv < 0)
        new_errs.push_back({-rv,line});
    }

  errs.insert(errs.end(),
              new_errs.begin(),
              new_errs.end());

  return (new_errs.empty() ? 0 : -EINVAL);
}

int
Config::from_file(const std::string &filepath_)
{
  std::ifstream ifstrm;

  errno = 0;
  ifstrm.open(filepath_);
  if(!ifstrm.good())
    {
      errno = errno ? errno : EIO;
      errs.push_back({errno,filepath_});
      return -errno;
    }

  return from_stream(ifstrm);
}

void
Config::finish_initializing()
{
  _initialized = true;
}

bool
Config::is_rootdir(const fs::path &fusepath_)
{
  return fusepath_.empty();
}

bool
Config::is_ctrl_file(const fs::path &fusepath_)
{
  return (fusepath_ == ".mergerfs");
}

bool
Config::is_mergerfs_xattr(const char *attrname_)
{
  return str::startswith(attrname_,"user.mergerfs.");
}

bool
Config::is_cmd_xattr(const std::string_view &attrname_)
{
  return nonstd::starts_with(attrname_,"user.mergerfs.cmd.");
}

std::string_view
Config::prune_ctrl_xattr(const std::string_view s_)
{
  constexpr size_t offset = (sizeof("user.mergerfs.") - 1);

  if(offset < s_.size())
    return s_.substr(offset);
  return {};
}

std::string_view
Config::prune_cmd_xattr(const std::string_view &s_)
{
  constexpr size_t offset = (sizeof("user.mergerfs.cmd.") - 1);

  if(offset < s_.size())
    return s_.substr(offset);
  return {};
}

std::string
Config::Err::to_string() const
{
  std::string s;

  switch(err)
    {
    case 0:
      break;
    case EINVAL:
      s = "invalid value";
      break;
    case ENOATTR:
      s = "unknown option";
      break;
    case EROFS:
      s = "read-only option";
      break;
    case ELOOP:
      s = "too many levels of config";
      break;
    case ENOENT:
      s = "no value set";
      break;
    default:
      s = strerror(err);
      break;
    }

  return fmt::format("{} - {}",s,str);
}
