// sysutil.cpp
//
// Copyright (c) 2024-2025 Kristofer Berggren
// All rights reserved.
//
// nchat is distributed under the MIT license, see LICENSE for details.

#include "sysutil.h"

#include <cerrno>
#include <sstream>

#include <signal.h>
#include <unistd.h>

#include <sys/wait.h>

#include "fileutil.h"
#include "log.h"
#include "strutil.h"

std::string SysUtil::GetCompiler()
{
#if defined(__VERSION__)
#if !defined(__clang__) && defined(__GNUC__)
  std::string compiler = "gcc " __VERSION__;
#else
  std::string compiler = __VERSION__;
#endif
#else
  std::string compiler = "unknown compiler";
#endif

#if defined(__linux__)
#if defined(__GLIBC__) && defined(__GLIBC_MINOR__)
  std::stringstream sslibc;
  sslibc << "glibc " << __GLIBC__ << "." << __GLIBC_MINOR__;
  std::string libc = sslibc.str();
#else
  std::string libc = "non-glibc";
#endif
#else
  std::string libc;
#endif

  return StrUtil::ToLower(compiler + (!libc.empty() ? " " + libc : ""));
}

std::string SysUtil::GetGo(const std::string& p_GoVersion)
{
  return StrUtil::ToLower("go " + (p_GoVersion.empty() ? "n/a" : p_GoVersion));
}

std::string SysUtil::GetOsArch()
{
  static const std::string os = []()
  {
#if defined(__linux__)
    std::string str = FileUtil::ReadFile("/etc/os-release");
    std::string prettyName = StrUtil::ExtractString(str, "PRETTY_NAME=\"", "\"");
    return prettyName.empty() ? "Linux" : prettyName;
#elif defined(__APPLE__)
    std::string str = FileUtil::ReadFile("/System/Library/CoreServices/SystemVersion.plist");
    std::string name = StrUtil::ExtractString(str, "<key>ProductName</key>\n\t<string>", "</string>");
    std::string ver = StrUtil::ExtractString(str, "<key>ProductVersion</key>\n\t<string>", "</string>");
    return name + " " + ver;
#else
    return "Unknown";
#endif
  }();

  static const std::string arch = []()
  {
#if defined(__arm__)
    return "arm";
#elif defined(__aarch64__)
    return "arm64";
#elif defined(__x86_64__) || defined(__amd64__)
    return "x86_64";
#elif defined(__i386__)
    return "i386";
#else
    return std::to_string(sizeof(void*) * 8) + "-bit";
#endif
  }();

  static const std::string osArch = os + " " + arch;
  return StrUtil::ToLower(osArch);
}

bool SysUtil::IsSupportedLibc()
{
#if defined(__APPLE__) || defined(__GLIBC__)
  return true;
#else
  return false;
#endif
}

bool SysUtil::RunCommand(const std::string& p_Cmd, std::string* p_StdOut /*= nullptr*/)
{
  const bool logStdErr = true;

  std::string stdoutPath = "/dev/null";
  if (p_StdOut != nullptr)
  {
    stdoutPath = FileUtil::GetTempDir() + "/stdout.txt";
  }

  std::string stderrPath = "/dev/null";
  if (logStdErr)
  {
    stderrPath = FileUtil::GetTempDir() + "/stderr.txt";
  }

  const std::string cmdPrefix = "{ ";
  const std::string cmdSuffix = " ; } >'" + stdoutPath + "' 2>'" + stderrPath + "'";
  const std::string cmd = cmdPrefix + p_Cmd + cmdSuffix;

  // run command
  LOG_TRACE("cmd \"%s\" start", cmd.c_str());
  const int rv = SysUtil::System(cmd);
  if (rv != 0)
  {
    LOG_WARNING("cmd \"%s\" failed (%d)", cmd.c_str(), rv);
  }

  // stdout
  if ((p_StdOut != nullptr) && FileUtil::Exists(stdoutPath))
  {
    std::string str = FileUtil::ReadFile(stdoutPath);
    FileUtil::RmFile(stdoutPath);

    // trim trailing linebreak
    if (!str.empty() && str.back() == '\n')
    {
      str = str.substr(0, str.length() - 1);
    }

    *p_StdOut = str;
  }

  // stderr
  if (logStdErr && FileUtil::Exists(stderrPath))
  {
    const std::string stderrStr = FileUtil::ReadFile(stderrPath);
    FileUtil::RmFile(stderrPath);
    if (!stderrStr.empty())
    {
      LOG_WARNING("cmd \"%s\" stderr:", cmd.c_str());
      Log::Dump(stderrStr.c_str());
    }
  }

  return (rv == 0);
}

int SysUtil::System(const std::string& p_Cmd)
{
#if defined(HAVE_TERMUX)
  static const std::string shPath = "/data/data/com.termux/files/usr/bin/sh";
#else
  static const std::string shPath = "/bin/sh";
#endif

  // Block SIGCHLD and ignore SIGINT/SIGQUIT in parent per POSIX system() spec
  struct sigaction saIgnore;
  struct sigaction saOrigInt;
  struct sigaction saOrigQuit;
  saIgnore.sa_handler = SIG_IGN;
  sigemptyset(&saIgnore.sa_mask);
  saIgnore.sa_flags = 0;
  sigaction(SIGINT, &saIgnore, &saOrigInt);
  sigaction(SIGQUIT, &saIgnore, &saOrigQuit);

  sigset_t blockChld;
  sigset_t origMask;
  sigemptyset(&blockChld);
  sigaddset(&blockChld, SIGCHLD);
  sigprocmask(SIG_BLOCK, &blockChld, &origMask);

  pid_t pid = fork();
  if (pid == 0)
  {
    // Child: restore original signal dispositions and mask
    sigaction(SIGINT, &saOrigInt, nullptr);
    sigaction(SIGQUIT, &saOrigQuit, nullptr);
    sigprocmask(SIG_SETMASK, &origMask, nullptr);

    execl(shPath.c_str(), "sh", "-c", p_Cmd.c_str(), (char*)nullptr);
    _exit(127);
  }

  int status = -1;
  if (pid > 0)
  {
    // Retry waitpid on EINTR (e.g. from SIGWINCH during terminal resize)
    while (waitpid(pid, &status, 0) < 0)
    {
      if (errno != EINTR)
      {
        status = -1;
        break;
      }
    }
  }

  // Restore original signal dispositions and mask
  sigaction(SIGINT, &saOrigInt, nullptr);
  sigaction(SIGQUIT, &saOrigQuit, nullptr);
  sigprocmask(SIG_SETMASK, &origMask, nullptr);

  return status;
}
