#include "base/std.h"

#include "terminal.h"

#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/bufferevent_ssl.h>
#include <event2/event.h>
#include <event2/util.h>
#include "thirdparty/libtelnet/libtelnet.h"

#ifndef _WIN32
#include <limits.h>
#include <signal.h>
#include <spawn.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
#endif

#include <algorithm>
#include <atomic>
#include <cerrno>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>

#include "base/internal/tracing.h"
#include "comm.h"
#include "interactive.h"
#include "terminal_pty_protocol.h"
#include "user.h"

#ifndef _WIN32
extern char **environ;
#endif

namespace {

constexpr size_t kMaximumQueuedBytes = 64 * 1024 * 1024;
constexpr size_t kMaximumQueuedMessages = 65536;
constexpr size_t kMaximumSessionOutputBytes = 16 * 1024 * 1024;
constexpr size_t kMaximumMainEventsPerDrain = 256;
constexpr size_t kMaximumMainBytesPerDrain = 1024 * 1024;

enum class CommandType {
  ATTACH,
  WRITE,
  FLUSH,
  CLOSE,
  ABORT,
  SET_EDITOR,
  SET_ECHO,
  BEGIN_COMPRESS,
  START_PTY,
  RESIZE_PTY,
  SIGNAL_PTY,
  STOP_PTY,
};

struct TerminalCommand {
  CommandType type;
  uint64_t session_id;
  int connection_type{0};
  evutil_socket_t fd{-1};
  SSL *ssl{nullptr};
  std::string data;
  std::vector<std::string> arguments;
  bool enabled{false};
  unsigned short width{0};
  unsigned short height{0};
  int signal_number{0};
};

enum class EventType {
  READY,
  DATA,
  DISCONNECTED,
  PTY_EXIT,
};

struct TerminalEvent {
  EventType type;
  uint64_t session_id;
  std::string data;
  int status{0};
};

struct TerminalSession {
  uint64_t id;
  int connection_type;
  evutil_socket_t fd;
  SSL *ssl;
  struct bufferevent *buffer{nullptr};
  struct telnet_t *decoder{nullptr};
  bool editor_enabled{false};
  bool echo_enabled{false};
  bool compress_started{false};
  bool previous_was_cr{false};
  size_t cursor{0};
  std::string line;
  std::string escape;
#ifndef _WIN32
  struct bufferevent *pty_buffer{nullptr};
  struct event *pty_control_event{nullptr};
  evutil_socket_t pty_control_fd{-1};
  pid_t pty_helper_pid{-1};
#endif
  bool pty_active{false};
  bool pty_stopping{false};
  bool output_overflow{false};
};

static ev_ssize_t socketpair_read(evutil_socket_t fd, void *buffer, size_t size) {
#ifdef _WIN32
  return recv(fd, static_cast<char *>(buffer), static_cast<int>(size), 0);
#else
  return read(fd, buffer, size);
#endif
}

static ev_ssize_t socketpair_write(evutil_socket_t fd, const void *buffer, size_t size) {
#ifdef _WIN32
  return send(fd, static_cast<const char *>(buffer), static_cast<int>(size), 0);
#else
  return write(fd, buffer, size);
#endif
}

static bool socket_error_interrupted(int error) {
#ifdef _WIN32
  return error == WSAEINTR;
#else
  return error == EINTR;
#endif
}

static bool socket_error_would_block(int error) {
#ifdef _WIN32
  return error == WSAEWOULDBLOCK;
#else
  return error == EAGAIN || error == EWOULDBLOCK;
#endif
}

class TerminalManager {
 public:
  static TerminalManager &instance() {
    static TerminalManager manager;
    return manager;
  }

  ~TerminalManager() {
    shutdown();
  }

  bool init(struct event_base *main_base) {
    std::unique_lock<std::mutex> lock(state_mutex_);
    if (running_) {
      return true;
    }

#ifdef _WIN32
    constexpr int socket_family = AF_INET;
#else
    constexpr int socket_family = AF_UNIX;
#endif
    if (evutil_socketpair(socket_family, SOCK_STREAM, 0, main_notify_) == -1 ||
        evutil_socketpair(socket_family, SOCK_STREAM, 0, worker_notify_) == -1) {
      close_notify_sockets();
      return false;
    }

    for (auto fd : main_notify_) {
      if (evutil_make_socket_nonblocking(fd) == -1 ||
          evutil_make_socket_closeonexec(fd) == -1) {
        close_notify_sockets();
        return false;
      }
    }
    for (auto fd : worker_notify_) {
      if (evutil_make_socket_nonblocking(fd) == -1 ||
          evutil_make_socket_closeonexec(fd) == -1) {
        close_notify_sockets();
        return false;
      }
    }

    main_event_ =
        event_new(main_base, main_notify_[0], EV_READ | EV_PERSIST, on_main_notify, this);
    if (!main_event_ || event_add(main_event_, nullptr) == -1) {
      if (main_event_) {
        event_free(main_event_);
        main_event_ = nullptr;
      }
      close_notify_sockets();
      return false;
    }

    starting_ = true;
    worker_ = std::thread([this]() { worker_main(); });
    state_changed_.wait(lock, [this]() { return !starting_; });
    if (!running_) {
      lock.unlock();
      if (worker_.joinable()) {
        worker_.join();
      }
      if (main_event_) {
        event_del(main_event_);
        event_free(main_event_);
        main_event_ = nullptr;
      }
      close_notify_sockets();
      return false;
    }
    return running_;
  }

  void shutdown() {
    {
      std::lock_guard<std::mutex> lock(state_mutex_);
      if (!running_) {
        return;
      }
    }

    event_base_loopbreak(worker_base_);
    if (worker_.joinable()) {
      worker_.join();
    }

    if (main_event_) {
      event_del(main_event_);
      event_free(main_event_);
      main_event_ = nullptr;
    }
    close_notify_sockets();

    std::lock_guard<std::mutex> lock(state_mutex_);
    running_ = false;
  }

  bool attach(uint64_t session_id, int connection_type, evutil_socket_t fd, SSL *ssl) {
    TerminalCommand command{CommandType::ATTACH, session_id};
    command.connection_type = connection_type;
    command.fd = fd;
    command.ssl = ssl;
    return enqueue_command(std::move(command));
  }

  bool write(uint64_t session_id, std::string_view data) {
    TerminalCommand command{CommandType::WRITE, session_id};
    command.data.assign(data.data(), data.size());
    return enqueue_command(std::move(command));
  }

  void flush(uint64_t session_id) {
    enqueue_command(TerminalCommand{CommandType::FLUSH, session_id});
  }

  void close(uint64_t session_id) {
    enqueue_command(TerminalCommand{CommandType::CLOSE, session_id});
  }

  void abort(uint64_t session_id) {
    enqueue_command(TerminalCommand{CommandType::ABORT, session_id});
  }

  void set_editor(uint64_t session_id, bool enabled) {
    TerminalCommand command{CommandType::SET_EDITOR, session_id};
    command.enabled = enabled;
    enqueue_command(std::move(command));
  }

  void set_echo(uint64_t session_id, bool enabled) {
    TerminalCommand command{CommandType::SET_ECHO, session_id};
    command.enabled = enabled;
    enqueue_command(std::move(command));
  }

  void begin_compress(uint64_t session_id) {
    enqueue_command(TerminalCommand{CommandType::BEGIN_COMPRESS, session_id});
  }

  bool start_pty(uint64_t session_id, std::string executable,
                 std::vector<std::string> arguments, unsigned short width,
                 unsigned short height) {
#ifdef _WIN32
    (void)session_id;
    (void)executable;
    (void)arguments;
    (void)width;
    (void)height;
    return false;
#else
    TerminalCommand command{CommandType::START_PTY, session_id};
    command.data = std::move(executable);
    command.arguments = std::move(arguments);
    command.width = width;
    command.height = height;
    return enqueue_command(std::move(command));
#endif
  }

  void resize_pty(uint64_t session_id, unsigned short width, unsigned short height) {
    TerminalCommand command{CommandType::RESIZE_PTY, session_id};
    command.width = width;
    command.height = height;
    enqueue_command(std::move(command));
  }

  void signal_pty(uint64_t session_id, int signal_number) {
    TerminalCommand command{CommandType::SIGNAL_PTY, session_id};
    command.signal_number = signal_number;
    enqueue_command(std::move(command));
  }

  void stop_pty(uint64_t session_id) {
    enqueue_command(TerminalCommand{CommandType::STOP_PTY, session_id});
  }

 private:
  static void on_main_notify(evutil_socket_t, short, void *arg) {
    static_cast<TerminalManager *>(arg)->drain_main_events();
  }

  static void on_worker_notify(evutil_socket_t, short, void *arg) {
    static_cast<TerminalManager *>(arg)->drain_worker_commands();
  }

  static void on_read(struct bufferevent *buffer, void *arg) {
    auto *session = static_cast<TerminalSession *>(arg);
    char data[64 * 1024];

    while (true) {
      auto count = bufferevent_read(buffer, data, sizeof(data));
      if (count == 0) {
        break;
      }
      if (session->decoder) {
        telnet_recv(session->decoder, data, count);
      }
      if (session->output_overflow) {
        auto session_id = session->id;
        instance().enqueue_event(TerminalEvent{EventType::DISCONNECTED, session_id});
        instance().destroy_session(session_id);
        return;
      }
      if (!instance().enqueue_event(
              TerminalEvent{EventType::DATA, session->id, std::string(data, count)})) {
        auto session_id = session->id;
        instance().enqueue_event(TerminalEvent{EventType::DISCONNECTED, session_id});
        instance().destroy_session(session_id);
        return;
      }
    }
  }

  static void on_telnet_decode(struct telnet_t *, telnet_event_t *event, void *arg) {
    auto *session = static_cast<TerminalSession *>(arg);
    switch (event->type) {
      case TELNET_EV_DATA:
        if (session->pty_active) {
#ifndef _WIN32
          if (session->pty_buffer) {
            auto *output = bufferevent_get_output(session->pty_buffer);
            auto output_size = evbuffer_get_length(output);
            if (output_size >= kMaximumSessionOutputBytes ||
                event->data.size > kMaximumSessionOutputBytes - output_size) {
              session->output_overflow = true;
            } else {
              bufferevent_write(session->pty_buffer, event->data.buffer, event->data.size);
            }
          }
#endif
        } else {
          instance().handle_editor_data(session, event->data.buffer, event->data.size);
        }
        break;
      case TELNET_EV_SEND:
        if (session->buffer) {
          auto *output = bufferevent_get_output(session->buffer);
          auto output_size = evbuffer_get_length(output);
          if (output_size >= kMaximumSessionOutputBytes ||
              event->data.size > kMaximumSessionOutputBytes - output_size) {
            session->output_overflow = true;
          } else {
            bufferevent_write(session->buffer, event->data.buffer, event->data.size);
          }
        }
        break;
      default:
        break;
    }
  }

  static void on_write(struct bufferevent *, void *) {}

#ifndef _WIN32
  static void on_pty_read(struct bufferevent *buffer, void *arg) {
    auto *session = static_cast<TerminalSession *>(arg);
    char data[64 * 1024];
    while (true) {
      auto count = bufferevent_read(buffer, data, sizeof(data));
      if (!count) {
        break;
      }
      if (session->decoder) {
        telnet_send(session->decoder, data, count);
      }
      if (session->output_overflow) {
        auto session_id = session->id;
        instance().enqueue_event(TerminalEvent{EventType::DISCONNECTED, session_id});
        instance().destroy_session(session_id);
        return;
      }
    }
  }

  static void on_pty_buffer_event(struct bufferevent *, short events, void *arg) {
    if (!(events & (BEV_EVENT_ERROR | BEV_EVENT_EOF))) {
      return;
    }
    auto *session = static_cast<TerminalSession *>(arg);
    if (!session->pty_active) {
      return;
    }
    auto session_id = session->id;
    instance().cleanup_pty(session, false);
    instance().enqueue_event(TerminalEvent{EventType::PTY_EXIT, session_id, {}, -1});
  }

  static void on_pty_control(evutil_socket_t fd, short, void *arg) {
    auto *session = static_cast<TerminalSession *>(arg);
    TerminalPtyMessage message {};
    auto count = socketpair_read(fd, &message, sizeof(message));
    if (count != sizeof(message) || message.operation != TerminalPtyOperation::EXIT) {
      return;
    }

    TerminalPtyMessage acknowledgement{TerminalPtyOperation::ACK, 0, 0, 0};
    socketpair_write(fd, &acknowledgement, sizeof(acknowledgement));
    auto session_id = session->id;
    auto status = message.value;
    instance().cleanup_pty(session, false);
    instance().enqueue_event(TerminalEvent{EventType::PTY_EXIT, session_id, {}, status});
  }
#endif

  static void on_buffer_event(struct bufferevent *, short events, void *arg) {
    if (!(events & (BEV_EVENT_ERROR | BEV_EVENT_EOF))) {
      return;
    }

    auto *session = static_cast<TerminalSession *>(arg);
    auto session_id = session->id;
    instance().enqueue_event(TerminalEvent{EventType::DISCONNECTED, session_id});
    instance().destroy_session(session_id);
  }

  bool enqueue_command(TerminalCommand command) {
    size_t data_size = command.data.size();
    for (const auto &argument : command.arguments) {
      if (argument.size() > kMaximumQueuedBytes - std::min(data_size, kMaximumQueuedBytes)) {
        data_size = kMaximumQueuedBytes + 1;
        break;
      }
      data_size += argument.size();
    }
    {
      std::lock_guard<std::mutex> lock(command_mutex_);
      bool control = command.type == CommandType::CLOSE || command.type == CommandType::ABORT;
      if (!control && (commands_.size() >= kMaximumQueuedMessages ||
                       data_size > kMaximumQueuedBytes - queued_command_bytes_)) {
        return false;
      }
      queued_command_bytes_ += data_size;
      commands_.push_back(std::move(command));
    }
    notify(worker_notify_[1]);
    return true;
  }

  bool enqueue_event(TerminalEvent event) {
    size_t data_size = event.data.size();
    {
      std::lock_guard<std::mutex> lock(event_mutex_);
      if (event.type == EventType::DATA &&
          (events_.size() >= kMaximumQueuedMessages ||
           data_size > kMaximumQueuedBytes - queued_event_bytes_)) {
        return false;
      }
      queued_event_bytes_ += data_size;
      events_.push_back(std::move(event));
    }
    notify(main_notify_[1]);
    return true;
  }

  static void notify(evutil_socket_t fd) {
    const char byte = 1;
    while (socketpair_write(fd, &byte, sizeof(byte)) == -1) {
      auto error = EVUTIL_SOCKET_ERROR();
      if (socket_error_interrupted(error)) {
        continue;
      }
      if (socket_error_would_block(error)) {
        return;
      }
      return;
    }
  }

  static void drain_notify_socket(evutil_socket_t fd) {
    char bytes[256];
    while (socketpair_read(fd, bytes, sizeof(bytes)) > 0) {
    }
  }

  void drain_main_events() {
    drain_notify_socket(main_notify_[0]);

    std::deque<TerminalEvent> events;
    bool events_remain = false;
    {
      std::lock_guard<std::mutex> lock(event_mutex_);
      size_t drained_bytes = 0;
      while (!events_.empty() && events.size() < kMaximumMainEventsPerDrain &&
             (events.empty() || drained_bytes < kMaximumMainBytesPerDrain)) {
        auto data_size = events_.front().data.size();
        drained_bytes += data_size;
        queued_event_bytes_ -= data_size;
        events.push_back(std::move(events_.front()));
        events_.pop_front();
      }
      events_remain = !events_.empty();
    }
    if (events_remain) {
      notify(main_notify_[1]);
    }

    for (auto &event : events) {
      auto *user = user_find_session(event.session_id);
      if (!user || !user->ob || (user->iflags & CLOSING)) {
        continue;
      }

      switch (event.type) {
        case EventType::READY:
          on_user_logon(user);
          break;
        case EventType::DATA:
          on_terminal_data(user, event.data.data(), event.data.size());
          break;
        case EventType::DISCONNECTED:
          if (user->ob->interactive != user) {
            discard_pending_user(user);
          } else {
            user->iflags |= NET_DEAD;
            remove_interactive(user->ob, 0);
          }
          break;
        case EventType::PTY_EXIT:
          on_terminal_pty_exit(user, event.status);
          break;
      }
    }
  }

  void worker_main() {
    Tracer::setThreadName("FluffOS Terminal");

    worker_base_ = event_base_new();
    if (worker_base_) {
      worker_event_ = event_new(worker_base_, worker_notify_[0], EV_READ | EV_PERSIST,
                                on_worker_notify, this);
    }

    {
      std::lock_guard<std::mutex> lock(state_mutex_);
      running_ = worker_base_ && worker_event_ && event_add(worker_event_, nullptr) == 0;
      starting_ = false;
    }
    state_changed_.notify_all();

    if (!running_) {
      cleanup_worker();
      return;
    }

    event_base_loop(worker_base_, 0);
    cleanup_worker();
  }

  void drain_worker_commands() {
    drain_notify_socket(worker_notify_[0]);

    std::deque<TerminalCommand> commands;
    {
      std::lock_guard<std::mutex> lock(command_mutex_);
      commands.swap(commands_);
      queued_command_bytes_ = 0;
    }

    for (auto &command : commands) {
      switch (command.type) {
        case CommandType::ATTACH:
          attach_on_worker(command);
          break;
        case CommandType::WRITE:
          write_on_worker(command.session_id, command.data);
          break;
        case CommandType::FLUSH:
          flush_on_worker(command.session_id);
          break;
        case CommandType::CLOSE:
          destroy_session(command.session_id);
          break;
        case CommandType::ABORT:
          enqueue_event(TerminalEvent{EventType::DISCONNECTED, command.session_id});
          destroy_session(command.session_id);
          break;
        case CommandType::SET_EDITOR:
          set_editor_on_worker(command.session_id, command.enabled);
          break;
        case CommandType::SET_ECHO:
          set_echo_on_worker(command.session_id, command.enabled);
          break;
        case CommandType::BEGIN_COMPRESS:
          begin_compress_on_worker(command.session_id);
          break;
        case CommandType::START_PTY:
          start_pty_on_worker(command);
          break;
        case CommandType::RESIZE_PTY:
          resize_pty_on_worker(command.session_id, command.width, command.height);
          break;
        case CommandType::SIGNAL_PTY:
          signal_pty_on_worker(command.session_id, command.signal_number);
          break;
        case CommandType::STOP_PTY:
          stop_pty_on_worker(command.session_id);
          break;
      }
    }
  }

  void attach_on_worker(TerminalCommand &command) {
    auto session = std::make_unique<TerminalSession>();
    session->id = command.session_id;
    session->connection_type = command.connection_type;
    session->fd = command.fd;
    session->ssl = command.ssl;

    auto options = BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS;
    session->buffer =
        session->ssl
            ? bufferevent_openssl_socket_new(worker_base_, session->fd, session->ssl,
                                             BUFFEREVENT_SSL_ACCEPTING, options)
            : bufferevent_socket_new(worker_base_, session->fd, options);
    if (!session->buffer) {
      // The OpenSSL bufferevent constructor consumes SSL on failure when
      // BEV_OPT_CLOSE_ON_FREE is set, but it does not close this descriptor.
      session->ssl = nullptr;
      if (session->fd != -1) {
        evutil_closesocket(session->fd);
        session->fd = -1;
      }
      enqueue_event(TerminalEvent{EventType::DISCONNECTED, session->id});
      return;
    }

    bufferevent_setcb(session->buffer, on_read, on_write, on_buffer_event, session.get());
    bufferevent_enable(session->buffer, EV_READ | EV_WRITE);
    bufferevent_set_timeouts(session->buffer, nullptr, nullptr);
    session->decoder = telnet_init(nullptr, on_telnet_decode, TELNET_FLAG_PROXY, session.get());
    if (!session->decoder) {
      bufferevent_free(session->buffer);
      session->buffer = nullptr;
      session->ssl = nullptr;
      session->fd = -1;
      enqueue_event(TerminalEvent{EventType::DISCONNECTED, session->id});
      return;
    }

    auto session_id = session->id;
    sessions_[session_id] = std::move(session);
    enqueue_event(TerminalEvent{EventType::READY, session_id});
  }

  void write_on_worker(uint64_t session_id, const std::string &data) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end()) {
      return;
    }
    auto *session = iter->second.get();
    auto *output = bufferevent_get_output(session->buffer);
    auto output_size = evbuffer_get_length(output);
    if (output_size >= kMaximumSessionOutputBytes ||
        data.size() > kMaximumSessionOutputBytes - output_size) {
      enqueue_event(TerminalEvent{EventType::DISCONNECTED, session_id});
      destroy_session(session_id);
      return;
    }
    // The data is an already-encoded telnet stream; relay it through the
    // session decoder's send path so it shares the COMPRESS2 zlib stream
    // with worker-generated output (editor echo, PTY data).
    telnet_send_raw(session->decoder, data.data(), data.size());
    if (session->output_overflow) {
      enqueue_event(TerminalEvent{EventType::DISCONNECTED, session_id});
      destroy_session(session_id);
    }
  }

  void flush_on_worker(uint64_t session_id) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end()) {
      return;
    }
    bufferevent_flush(iter->second->buffer, EV_WRITE, BEV_FLUSH);
  }

  void set_editor_on_worker(uint64_t session_id, bool enabled) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end()) {
      return;
    }
    auto *session = iter->second.get();
    session->editor_enabled = enabled;
    clear_editor_line(session);
    session->escape.clear();
    session->cursor = 0;
    session->previous_was_cr = false;
  }

  void set_echo_on_worker(uint64_t session_id, bool enabled) {
    auto iter = sessions_.find(session_id);
    if (iter != sessions_.end()) {
      iter->second->echo_enabled = enabled;
    }
  }

  void begin_compress_on_worker(uint64_t session_id) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end() || iter->second->compress_started) {
      return;
    }
    iter->second->compress_started = true;
    // Emits IAC SB COMPRESS2 IAC SE uncompressed, then deflates everything
    // subsequently sent through the decoder (relayed writes, echo, PTY).
    telnet_begin_compress2(iter->second->decoder);
  }

  static void clear_editor_line(TerminalSession *session) {
    std::fill(session->line.begin(), session->line.end(), '\0');
    session->line.clear();
  }

#ifndef _WIN32
  static std::string pty_helper_path() {
    char path[PATH_MAX + 1];
#ifdef __APPLE__
    uint32_t size = sizeof(path);
    if (_NSGetExecutablePath(path, &size) != 0) {
      return {};
    }
#else
    auto count = readlink("/proc/self/exe", path, PATH_MAX);
    if (count <= 0) {
      return {};
    }
    path[count] = '\0';
#endif
    std::string result(path);
    auto separator = result.find_last_of('/');
    if (separator == std::string::npos) {
      return {};
    }
    result.resize(separator + 1);
    result += "terminal_pty_helper";
    return result;
  }

  static bool send_pty_control(TerminalSession *session, TerminalPtyMessage message) {
    if (session->pty_control_fd == -1) {
      return false;
    }
    return socketpair_write(session->pty_control_fd, &message, sizeof(message)) == sizeof(message);
  }

  void start_pty_on_worker(const TerminalCommand &command) {
    auto iter = sessions_.find(command.session_id);
    if (iter == sessions_.end() || iter->second->pty_active) {
      enqueue_event(TerminalEvent{EventType::PTY_EXIT, command.session_id, {}, -1});
      return;
    }
    auto helper = pty_helper_path();
    if (helper.empty()) {
      enqueue_event(TerminalEvent{EventType::PTY_EXIT, command.session_id, {}, -1});
      return;
    }

    evutil_socket_t data_fds[2] = {-1, -1};
    evutil_socket_t control_fds[2] = {-1, -1};
    if (evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, data_fds) == -1 ||
        evutil_socketpair(AF_UNIX, SOCK_DGRAM, 0, control_fds) == -1) {
      for (auto fd : data_fds) {
        if (fd != -1) {
          evutil_closesocket(fd);
        }
      }
      for (auto fd : control_fds) {
        if (fd != -1) {
          evutil_closesocket(fd);
        }
      }
      enqueue_event(TerminalEvent{EventType::PTY_EXIT, command.session_id, {}, -1});
      return;
    }

    if (evutil_make_socket_nonblocking(data_fds[0]) == -1 ||
        evutil_make_socket_nonblocking(control_fds[0]) == -1 ||
        evutil_make_socket_closeonexec(data_fds[0]) == -1 ||
        evutil_make_socket_closeonexec(control_fds[0]) == -1) {
      for (auto fd : data_fds) {
        evutil_closesocket(fd);
      }
      for (auto fd : control_fds) {
        evutil_closesocket(fd);
      }
      enqueue_event(TerminalEvent{EventType::PTY_EXIT, command.session_id, {}, -1});
      return;
    }

    std::vector<std::string> argument_storage = {
        helper,
        std::to_string(data_fds[1]),
        std::to_string(control_fds[1]),
        std::to_string(command.width ? command.width : 80),
        std::to_string(command.height ? command.height : 24),
        command.data,
    };
    argument_storage.insert(argument_storage.end(), command.arguments.begin(),
                            command.arguments.end());
    std::vector<char *> arguments;
    arguments.reserve(argument_storage.size() + 1);
    for (auto &argument : argument_storage) {
      arguments.push_back(argument.data());
    }
    arguments.push_back(nullptr);

    pid_t helper_pid = -1;
    auto spawn_result =
        posix_spawn(&helper_pid, helper.c_str(), nullptr, nullptr, arguments.data(), ::environ);
    evutil_closesocket(data_fds[1]);
    evutil_closesocket(control_fds[1]);
    if (spawn_result != 0) {
      evutil_closesocket(data_fds[0]);
      evutil_closesocket(control_fds[0]);
      enqueue_event(TerminalEvent{EventType::PTY_EXIT, command.session_id, {}, -1});
      return;
    }

    std::thread([helper_pid]() {
      int status = 0;
      while (waitpid(helper_pid, &status, 0) == -1 && errno == EINTR) {
      }
    }).detach();

    auto *session = iter->second.get();
    session->pty_buffer =
        bufferevent_socket_new(worker_base_, data_fds[0], BEV_OPT_CLOSE_ON_FREE);
    session->pty_control_event =
        event_new(worker_base_, control_fds[0], EV_READ | EV_PERSIST, on_pty_control, session);
    if (!session->pty_buffer || !session->pty_control_event ||
        event_add(session->pty_control_event, nullptr) == -1) {
      if (session->pty_buffer) {
        bufferevent_free(session->pty_buffer);
        session->pty_buffer = nullptr;
      } else {
        evutil_closesocket(data_fds[0]);
      }
      if (session->pty_control_event) {
        event_free(session->pty_control_event);
        session->pty_control_event = nullptr;
      }
      evutil_closesocket(control_fds[0]);
      enqueue_event(TerminalEvent{EventType::PTY_EXIT, command.session_id, {}, -1});
      return;
    }

    session->pty_control_fd = control_fds[0];
    session->pty_helper_pid = helper_pid;
    session->pty_active = true;
    session->pty_stopping = false;
    session->editor_enabled = false;
    clear_editor_line(session);
    session->escape.clear();
    session->cursor = 0;
    bufferevent_setcb(session->pty_buffer, on_pty_read, nullptr, on_pty_buffer_event, session);
    bufferevent_enable(session->pty_buffer, EV_READ | EV_WRITE);
  }

  void resize_pty_on_worker(uint64_t session_id, unsigned short width, unsigned short height) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end() || !iter->second->pty_active) {
      return;
    }
    send_pty_control(
        iter->second.get(),
        TerminalPtyMessage{TerminalPtyOperation::RESIZE, 0, width, height});
  }

  void signal_pty_on_worker(uint64_t session_id, int signal_number) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end() || !iter->second->pty_active) {
      return;
    }
    send_pty_control(iter->second.get(),
                     TerminalPtyMessage{TerminalPtyOperation::SIGNAL, signal_number, 0, 0});
  }

  void stop_pty_on_worker(uint64_t session_id) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end() || !iter->second->pty_active ||
        iter->second->pty_stopping) {
      return;
    }
    iter->second->pty_stopping = true;
    send_pty_control(iter->second.get(), TerminalPtyMessage{TerminalPtyOperation::STOP, 0, 0, 0});
  }

  void cleanup_pty(TerminalSession *session, bool request_stop) {
    if (!session->pty_active && !session->pty_buffer && session->pty_control_fd == -1) {
      return;
    }
    if (request_stop) {
      send_pty_control(session, TerminalPtyMessage{TerminalPtyOperation::STOP, 0, 0, 0});
    }
    session->pty_active = false;
    session->pty_stopping = false;
    if (session->pty_buffer) {
      bufferevent_free(session->pty_buffer);
      session->pty_buffer = nullptr;
    }
    if (session->pty_control_event) {
      event_del(session->pty_control_event);
      event_free(session->pty_control_event);
      session->pty_control_event = nullptr;
    }
    if (session->pty_control_fd != -1) {
      evutil_closesocket(session->pty_control_fd);
      session->pty_control_fd = -1;
    }
    session->pty_helper_pid = -1;
  }
#else
  void start_pty_on_worker(const TerminalCommand &command) {
    enqueue_event(TerminalEvent{EventType::PTY_EXIT, command.session_id, {}, -1});
  }
  void resize_pty_on_worker(uint64_t, unsigned short, unsigned short) {}
  void signal_pty_on_worker(uint64_t, int) {}
  void stop_pty_on_worker(uint64_t) {}
#endif

  static size_t previous_character(const std::string &text, size_t cursor) {
    if (cursor == 0) {
      return 0;
    }
    size_t pos = cursor - 1;
    while (pos > 0 && (static_cast<unsigned char>(text[pos]) & 0xc0) == 0x80) {
      pos--;
    }
    return pos;
  }

  static size_t next_character(const std::string &text, size_t cursor) {
    if (cursor >= text.size()) {
      return text.size();
    }
    size_t pos = cursor + 1;
    while (pos < text.size() && (static_cast<unsigned char>(text[pos]) & 0xc0) == 0x80) {
      pos++;
    }
    return pos;
  }

  static void send_editor_text(TerminalSession *session, std::string_view text) {
    if (!session->echo_enabled || !session->decoder) {
      return;
    }
    telnet_send_text(session->decoder, text.data(), text.size());
  }

  static void send_editor_newline(TerminalSession *session) {
    send_editor_text(session, "\n");
  }

  static size_t character_count(std::string_view text) {
    size_t count = 0;
    for (auto ch : text) {
      if ((static_cast<unsigned char>(ch) & 0xc0) != 0x80) {
        count++;
      }
    }
    return count;
  }

  static void move_editor_left(TerminalSession *session, size_t count) {
    for (size_t i = 0; i < count; i++) {
      send_editor_text(session, "\x1b[D");
    }
  }

  void handle_escape(TerminalSession *session) {
    auto &escape = session->escape;
    if (escape == "\x1b[D" || escape == "\x1bOD") {
      if (session->cursor > 0) {
        session->cursor = previous_character(session->line, session->cursor);
        send_editor_text(session, escape);
      }
    } else if (escape == "\x1b[C" || escape == "\x1bOC") {
      if (session->cursor < session->line.size()) {
        session->cursor = next_character(session->line, session->cursor);
        send_editor_text(session, escape);
      }
    } else if (escape == "\x1b[H" || escape == "\x1b[1~" || escape == "\x1bOH") {
      while (session->cursor > 0) {
        session->cursor = previous_character(session->line, session->cursor);
        send_editor_text(session, "\x1b[D");
      }
    } else if (escape == "\x1b[F" || escape == "\x1b[4~" || escape == "\x1bOF") {
      while (session->cursor < session->line.size()) {
        session->cursor = next_character(session->line, session->cursor);
        send_editor_text(session, "\x1b[C");
      }
    } else if (escape == "\x1b[3~" && session->cursor < session->line.size()) {
      auto next = next_character(session->line, session->cursor);
      session->line.erase(session->cursor, next - session->cursor);
      auto tail = std::string_view(session->line).substr(session->cursor);
      send_editor_text(session, tail);
      send_editor_text(session, " ");
      move_editor_left(session, character_count(tail) + 1);
    }
    escape.clear();
  }

  void handle_editor_data(TerminalSession *session, const char *data, size_t size) {
    for (size_t i = 0; i < size; i++) {
      auto ch = static_cast<unsigned char>(data[i]);
      if (session->previous_was_cr) {
        session->previous_was_cr = false;
        if (ch == '\n' || ch == '\0') {
          continue;
        }
      }

      if (!session->editor_enabled) {
        if (ch == '\r' || ch == '\n') {
          session->previous_was_cr = ch == '\r';
          send_editor_newline(session);
        } else if (ch != '\0') {
          char output = static_cast<char>(ch);
          send_editor_text(session, std::string_view(&output, 1));
        }
        continue;
      }

      if (!session->escape.empty()) {
        session->escape.push_back(static_cast<char>(ch));
        bool complete =
            (session->escape.size() == 2 && ch != '[' && ch != 'O') ||
            (session->escape.size() >= 3 &&
             ((ch >= '@' && ch <= '~') || session->escape.size() >= 8));
        if (complete) {
          handle_escape(session);
        }
        continue;
      }

      if (ch == 0x1b) {
        session->escape.assign(1, static_cast<char>(ch));
        continue;
      }
      if (ch == '\r' || ch == '\n') {
        session->previous_was_cr = ch == '\r';
        send_editor_newline(session);
        clear_editor_line(session);
        session->cursor = 0;
        continue;
      }
      session->previous_was_cr = false;

      if (ch == 0x08 || ch == 0x7f) {
        if (session->cursor > 0) {
          auto previous = previous_character(session->line, session->cursor);
          session->line.erase(previous, session->cursor - previous);
          session->cursor = previous;
          auto tail = std::string_view(session->line).substr(session->cursor);
          send_editor_text(session, "\b");
          send_editor_text(session, tail);
          send_editor_text(session, " ");
          move_editor_left(session, character_count(tail) + 1);
        }
        continue;
      }
      if (ch < 0x20) {
        continue;
      }

      auto insertion_point = session->cursor;
      session->line.insert(insertion_point, 1, static_cast<char>(ch));
      session->cursor++;
      auto tail = std::string_view(session->line).substr(session->cursor);
      char output = static_cast<char>(ch);
      send_editor_text(session, std::string_view(&output, 1));
      send_editor_text(session, tail);
      move_editor_left(session, character_count(tail));
    }
  }

  void destroy_session(uint64_t session_id) {
    auto iter = sessions_.find(session_id);
    if (iter == sessions_.end()) {
      return;
    }

#ifndef _WIN32
    cleanup_pty(iter->second.get(), true);
#endif
    if (iter->second->ssl) {
      SSL_set_shutdown(iter->second->ssl, SSL_RECEIVED_SHUTDOWN);
      SSL_shutdown(iter->second->ssl);
      iter->second->ssl = nullptr;
    }
    if (iter->second->buffer) {
      bufferevent_free(iter->second->buffer);
      iter->second->buffer = nullptr;
    }
    if (iter->second->decoder) {
      telnet_free(iter->second->decoder);
      iter->second->decoder = nullptr;
    }
    clear_editor_line(iter->second.get());
    sessions_.erase(iter);
  }

  void cleanup_worker() {
    while (!sessions_.empty()) {
      destroy_session(sessions_.begin()->first);
    }
    if (worker_event_) {
      event_del(worker_event_);
      event_free(worker_event_);
      worker_event_ = nullptr;
    }
    if (worker_base_) {
      event_base_free(worker_base_);
      worker_base_ = nullptr;
    }
  }

  void close_notify_sockets() {
    for (auto &fd : main_notify_) {
      if (fd != -1) {
        evutil_closesocket(fd);
        fd = -1;
      }
    }
    for (auto &fd : worker_notify_) {
      if (fd != -1) {
        evutil_closesocket(fd);
        fd = -1;
      }
    }
  }

  std::mutex state_mutex_;
  std::condition_variable state_changed_;
  bool starting_{false};
  bool running_{false};

  std::thread worker_;
  struct event_base *worker_base_{nullptr};
  struct event *worker_event_{nullptr};
  struct event *main_event_{nullptr};
  evutil_socket_t main_notify_[2]{-1, -1};
  evutil_socket_t worker_notify_[2]{-1, -1};

  std::mutex command_mutex_;
  std::deque<TerminalCommand> commands_;
  size_t queued_command_bytes_{0};

  std::mutex event_mutex_;
  std::deque<TerminalEvent> events_;
  size_t queued_event_bytes_{0};

  std::unordered_map<uint64_t, std::unique_ptr<TerminalSession>> sessions_;

  friend bool ::terminal_init(struct event_base *);
  friend void ::terminal_shutdown();
  friend bool ::terminal_attach(uint64_t, int, evutil_socket_t, SSL *);
  friend bool ::terminal_write(uint64_t, std::string_view);
  friend void ::terminal_flush(uint64_t);
  friend void ::terminal_close(uint64_t);
  friend void ::terminal_abort(uint64_t);
  friend void ::terminal_set_editor(uint64_t, bool);
  friend void ::terminal_set_echo(uint64_t, bool);
  friend void ::terminal_begin_compress(uint64_t);
  friend bool ::terminal_start_pty(uint64_t, std::string, std::vector<std::string>,
                                   unsigned short, unsigned short);
  friend void ::terminal_resize_pty(uint64_t, unsigned short, unsigned short);
  friend void ::terminal_signal_pty(uint64_t, int);
  friend void ::terminal_stop_pty(uint64_t);
};

TerminalManager &terminal_manager() {
  return TerminalManager::instance();
}

std::atomic<uint64_t> next_session_id{1};

}  // namespace

bool terminal_init(struct event_base *main_base) {
  return terminal_manager().init(main_base);
}

void terminal_shutdown() {
  terminal_manager().shutdown();
}

uint64_t terminal_next_session_id() {
  return next_session_id.fetch_add(1, std::memory_order_relaxed);
}

bool terminal_attach(uint64_t session_id, int connection_type, evutil_socket_t fd, SSL *ssl) {
  return terminal_manager().attach(session_id, connection_type, fd, ssl);
}

bool terminal_write(uint64_t session_id, std::string_view data) {
  return terminal_manager().write(session_id, data);
}

void terminal_flush(uint64_t session_id) {
  terminal_manager().flush(session_id);
}

void terminal_close(uint64_t session_id) {
  terminal_manager().close(session_id);
}

void terminal_abort(uint64_t session_id) {
  terminal_manager().abort(session_id);
}

void terminal_set_editor(uint64_t session_id, bool enabled) {
  terminal_manager().set_editor(session_id, enabled);
}

void terminal_set_echo(uint64_t session_id, bool enabled) {
  terminal_manager().set_echo(session_id, enabled);
}

void terminal_begin_compress(uint64_t session_id) {
  terminal_manager().begin_compress(session_id);
}

bool terminal_start_pty(uint64_t session_id, std::string executable,
                        std::vector<std::string> arguments, unsigned short width,
                        unsigned short height) {
  return terminal_manager().start_pty(session_id, std::move(executable), std::move(arguments),
                                      width, height);
}

void terminal_resize_pty(uint64_t session_id, unsigned short width, unsigned short height) {
  terminal_manager().resize_pty(session_id, width, height);
}

void terminal_signal_pty(uint64_t session_id, int signal_number) {
  terminal_manager().signal_pty(session_id, signal_number);
}

void terminal_stop_pty(uint64_t session_id) {
  terminal_manager().stop_pty(session_id);
}
