#ifndef _WIN32

#include "terminal_pty_protocol.h"

#include <fcntl.h>
#include <poll.h>
#if defined(__APPLE__) || defined(__FreeBSD__)
#include <util.h>
#else
#include <pty.h>
#endif
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>

#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

namespace {

constexpr int kExitUsage = 64;
constexpr int kExitUnavailable = 69;
constexpr int kExitSoftware = 70;
constexpr int kExitNoPermission = 77;

bool write_all(int fd, const void *buffer, size_t size) {
  auto *data = static_cast<const char *>(buffer);
  while (size) {
    auto written = write(fd, data, size);
    if (written > 0) {
      data += written;
      size -= written;
      continue;
    }
    if (written == -1 && errno == EINTR) {
      continue;
    }
    return false;
  }
  return true;
}

bool make_nonblocking(int fd) {
  auto flags = fcntl(fd, F_GETFL, 0);
  return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
}

void close_unneeded_descriptors(int data_fd, int control_fd) {
  long maximum = sysconf(_SC_OPEN_MAX);
  if (maximum < 0 || maximum > 65536) {
    maximum = 65536;
  }
  for (int fd = 3; fd < maximum; fd++) {
    if (fd != data_fd && fd != control_fd) {
      close(fd);
    }
  }
}

void set_limits() {
  struct rlimit limit {};
  limit.rlim_cur = limit.rlim_max = 3600;
  setrlimit(RLIMIT_CPU, &limit);
  limit.rlim_cur = limit.rlim_max = 1024ULL * 1024 * 1024;
  setrlimit(RLIMIT_AS, &limit);
  limit.rlim_cur = limit.rlim_max = 256ULL * 1024 * 1024;
  setrlimit(RLIMIT_FSIZE, &limit);
#ifdef RLIMIT_NPROC
  limit.rlim_cur = limit.rlim_max = 64;
  setrlimit(RLIMIT_NPROC, &limit);
#endif
}

void terminate_child(pid_t child) {
  if (child <= 0) {
    return;
  }
  kill(-child, SIGTERM);
  for (int i = 0; i < 20; i++) {
    int status = 0;
    auto result = waitpid(child, &status, WNOHANG);
    if (result == child || (result == -1 && errno == ECHILD)) {
      return;
    }
    usleep(50000);
  }
  kill(-child, SIGKILL);
  while (waitpid(child, nullptr, 0) == -1 && errno == EINTR) {
  }
}

int child_exit_status(int status) {
  if (WIFEXITED(status)) {
    return WEXITSTATUS(status);
  }
  if (WIFSIGNALED(status)) {
    return 128 + WTERMSIG(status);
  }
  return -1;
}

void drain_child_output(int master_fd, int data_fd) {
  char buffer[64 * 1024];
  while (true) {
    struct pollfd descriptor {master_fd, POLLIN, 0};
    if (poll(&descriptor, 1, 50) <= 0 || !(descriptor.revents & POLLIN)) {
      return;
    }
    auto count = read(master_fd, buffer, sizeof(buffer));
    if (count <= 0 || !write_all(data_fd, buffer, count)) {
      return;
    }
  }
}

}  // namespace

int main(int argc, char **argv) {
  if (argc < 6) {
    return kExitUsage;
  }
  if (geteuid() == 0) {
    return kExitNoPermission;
  }

  int data_fd = std::atoi(argv[1]);
  int control_fd = std::atoi(argv[2]);
  int width = std::atoi(argv[3]);
  int height = std::atoi(argv[4]);
  const char *executable = argv[5];
  close_unneeded_descriptors(data_fd, control_fd);
  signal(SIGPIPE, SIG_IGN);

  struct winsize size {};
  size.ws_col = width > 0 ? static_cast<unsigned short>(width) : 80;
  size.ws_row = height > 0 ? static_cast<unsigned short>(height) : 24;

  int master_fd = -1;
  pid_t child = forkpty(&master_fd, nullptr, nullptr, &size);
  if (child == -1) {
    TerminalPtyMessage message{TerminalPtyOperation::EXIT, kExitUnavailable, 0, 0};
    write_all(control_fd, &message, sizeof(message));
    return kExitUnavailable;
  }

  if (child == 0) {
    signal(SIGPIPE, SIG_DFL);
    close(data_fd);
    close(control_fd);
    setpgid(0, 0);
    if (chdir("/tmp") != 0) {
      _exit(127);
    }
    set_limits();

    std::vector<char *> arguments;
    arguments.reserve(argc - 4);
    arguments.push_back(const_cast<char *>(executable));
    for (int i = 6; i < argc; i++) {
      arguments.push_back(argv[i]);
    }
    arguments.push_back(nullptr);

    char path[] = "PATH=/usr/bin:/bin";
    char term[] = "TERM=xterm-256color";
    char lang[] = "LANG=C.UTF-8";
    char home[] = "HOME=/tmp";
    char *environment[] = {path, term, lang, home, nullptr};
    execve(executable, arguments.data(), environment);
    _exit(127);
  }

  if (!make_nonblocking(data_fd) || !make_nonblocking(master_fd)) {
    terminate_child(child);
    return kExitSoftware;
  }

  bool stopping = false;
  std::chrono::steady_clock::time_point stop_deadline;
  char buffer[64 * 1024];
  while (true) {
    int status = 0;
    auto waited = waitpid(child, &status, WNOHANG);
    if (waited == child) {
      drain_child_output(master_fd, data_fd);
      TerminalPtyMessage message{TerminalPtyOperation::EXIT, child_exit_status(status), 0, 0};
      write_all(control_fd, &message, sizeof(message));

      struct pollfd ack_fd {control_fd, POLLIN, 0};
      if (poll(&ack_fd, 1, 5000) > 0) {
        TerminalPtyMessage acknowledgement {};
        /* We only drain the ack; its contents are irrelevant. */
        ssize_t ignored = read(control_fd, &acknowledgement, sizeof(acknowledgement));
        (void)ignored;
      }
      return 0;
    }
    if (waited == -1 && errno != EINTR) {
      return kExitSoftware;
    }
    if (stopping && std::chrono::steady_clock::now() >= stop_deadline) {
      kill(-child, SIGKILL);
      stopping = false;
    }

    struct pollfd descriptors[] = {
        {data_fd, POLLIN, 0},
        {control_fd, POLLIN, 0},
        {master_fd, POLLIN, 0},
    };
    auto ready = poll(descriptors, 3, 100);
    if (ready == -1) {
      if (errno == EINTR) {
        continue;
      }
      terminate_child(child);
      return kExitSoftware;
    }

    if (descriptors[1].revents & (POLLHUP | POLLERR | POLLNVAL)) {
      terminate_child(child);
      return 0;
    }
    if (descriptors[1].revents & POLLIN) {
      TerminalPtyMessage message {};
      auto count = read(control_fd, &message, sizeof(message));
      if (count != sizeof(message)) {
        terminate_child(child);
        return 0;
      }
      switch (message.operation) {
        case TerminalPtyOperation::RESIZE: {
          struct winsize new_size {};
          new_size.ws_col = message.width;
          new_size.ws_row = message.height;
          ioctl(master_fd, TIOCSWINSZ, &new_size);
          kill(-child, SIGWINCH);
          break;
        }
        case TerminalPtyOperation::SIGNAL:
          if (message.value > 0 && message.value < NSIG) {
            kill(-child, message.value);
          }
          break;
        case TerminalPtyOperation::STOP:
          if (!stopping) {
            stopping = true;
            stop_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
            kill(-child, SIGTERM);
          }
          break;
        default:
          break;
      }
    }

    if (descriptors[0].revents & (POLLHUP | POLLERR | POLLNVAL)) {
      terminate_child(child);
      return 0;
    }
    if (descriptors[0].revents & POLLIN) {
      auto count = read(data_fd, buffer, sizeof(buffer));
      if (count <= 0 || !write_all(master_fd, buffer, count)) {
        terminate_child(child);
        return 0;
      }
    }

    if (descriptors[2].revents & POLLIN) {
      auto count = read(master_fd, buffer, sizeof(buffer));
      if (count > 0 && !write_all(data_fd, buffer, count)) {
        terminate_child(child);
        return 0;
      }
    }
  }
}

#else

int main() {
  return 1;
}

#endif
