#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define USE_OPEN 1
// disabling 'USE_FCLONEFILEAT' will cause repro to stop
#define USE_FCLONEFILEAT 1

static void LOG(const char *fmt, ...)
{
  static dispatch_queue_t queue;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    queue = dispatch_queue_create("com.acronis.log", DISPATCH_QUEUE_SERIAL);
  });

  va_list ap;
  va_start(ap, fmt);
  char* line = nullptr;
  vasprintf(&line, fmt, ap);
  if (line)
  {
    dispatch_async(queue, ^{
      printf("%s\n", line);
      free(line);
    });
  }
  va_end(ap);
}

static std::string volfsPath(uint64_t devId, uint64_t vnodeId)
{
  return "/.vol/" + std::to_string(devId) + "/" + std::to_string(vnodeId);
}

static void cloneAndScheduleDelete(const std::string& sourcePath, dispatch_queue_t queue, uint64_t devId, uint64_t vnodeId)
{
  struct stat st;
  if (stat(sourcePath.c_str(), &st) != 0 || !S_ISREG(st.st_mode))
  {
    return;
  }

#ifdef USE_OPEN
  int srcFd = open(sourcePath.c_str(), O_RDONLY);
  if (srcFd < 0)
  {
    LOG("Failed to open source file: %s", sourcePath.c_str());
    return;
  }
#endif

  const char* cloneDir = "/Users/admin/Downloads/_clone";
  mkdir(cloneDir, 0755);

  const char* filename = strrchr(sourcePath.c_str(), '/');
  filename = filename ? filename + 1 : sourcePath.c_str();
  
  std::string cloneFilename = std::string(filename) + ".clone." + std::to_string(time(nullptr)) + "." + std::to_string(getpid());
  std::string clonePath = std::string(cloneDir) + "/" + cloneFilename;

#ifdef USE_FCLONEFILEAT
  if (fclonefileat(srcFd, AT_FDCWD, clonePath.c_str(), 0) == 0)
  {
    LOG("Cloned file: %s -> %s", sourcePath.c_str(), clonePath.c_str());
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), queue, ^{
      if (unlink(clonePath.c_str()) == 0)
      {
        LOG("Deleted clone: %s", clonePath.c_str());
      }
      else
      {
        LOG("Failed to delete clone: %s", clonePath.c_str());
      }
    });
  }
  else
  {
    LOG("Failed to clone file: %s", sourcePath.c_str());
  }
  
#else
  std::string sourcePathVolfs = volfsPath(devId, vnodeId);
  if (clonefile(sourcePathVolfs.c_str(), clonePath.c_str(), 0) == 0)
  {
    LOG("Cloned file: %s -> %s", sourcePathVolfs.c_str(), clonePath.c_str());
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), queue, ^{
      if (unlink(clonePath.c_str()) == 0)
      {
        LOG("Deleted clone: %s", clonePath.c_str());
      }
      else
      {
        LOG("Failed to delete clone: %s", clonePath.c_str());
      }
    });
  }
  else
  {
    LOG("Failed to clone file: %s", sourcePath.c_str());
  }
#endif

#ifdef USE_OPEN
  close(srcFd);
#endif
}

static void respond(es_client_t* client, const es_message_t* msg, bool cache)
{
  if (msg->action_type == ES_ACTION_TYPE_AUTH)
  {
    if (msg->event_type == ES_EVENT_TYPE_AUTH_OPEN)
    {
      es_respond_flags_result(client, msg, ~0, cache);
    }
    else
    {
      es_respond_auth_result(client, msg, ES_AUTH_RESULT_ALLOW, cache);
    }
  }
}

static const es_file_t* file(const es_message_t* msg)
{
  switch (msg->event_type)
  {
    case ES_EVENT_TYPE_AUTH_OPEN:
      return msg->event.open.file;
    case ES_EVENT_TYPE_AUTH_EXEC:
      return msg->event.exec.target->executable;
    case ES_EVENT_TYPE_AUTH_RENAME:
      return msg->event.rename.source;
  }
    
  return nullptr;
}

int main(int argc, const char * argv[])
{
  dispatch_queue_t esQueue    = dispatch_queue_create("com.acronis.es",      DISPATCH_QUEUE_CONCURRENT);
  dispatch_queue_t cloneQueue = dispatch_queue_create("com.acronis.clone",   DISPATCH_QUEUE_CONCURRENT);

  es_client_t* cli;
  auto ret = es_new_client(&cli, ^(es_client_t* client, const es_message_t * msgc)
  {
    if (msgc->process->is_es_client)
    {
      es_mute_process(client, &msgc->process->audit_token);
      return respond(client, msgc, true);
    }

    dispatch_async(esQueue, ^{
      bool shouldClone = false;
      if (msgc->event_type == ES_EVENT_TYPE_AUTH_OPEN)
      {
        auto& ev = msgc->event.open;
        if (ev.fflag & (FWRITE | O_RDWR | O_WRONLY | O_TRUNC | O_APPEND))
        {
          shouldClone = true;
        }
      }
      else if (msgc->event_type == ES_EVENT_TYPE_AUTH_UNLINK || msgc->event_type == ES_EVENT_TYPE_AUTH_RENAME)
      {
        shouldClone = true;
      }

      if (shouldClone)
      {
        if (auto f = ::file(msgc))
          cloneAndScheduleDelete(f->path.data, cloneQueue, f->stat.st_dev, f->stat.st_ino);
      }

      respond(client, msgc, true);
    });
  });
  LOG("es_new_client -> %d", ret);

  es_mute_path(cli, "/usr/sbin/notifyd", ES_MUTE_PATH_TYPE_LITERAL);
  es_mute_path(cli, "/usr/sbin/spindump", ES_MUTE_PATH_TYPE_LITERAL);
  es_mute_path(cli, "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", ES_MUTE_PATH_TYPE_LITERAL);

  es_event_type_t events[] = {
    ES_EVENT_TYPE_AUTH_OPEN,
    ES_EVENT_TYPE_AUTH_EXEC,
    ES_EVENT_TYPE_AUTH_RENAME,
    ES_EVENT_TYPE_AUTH_UNLINK,
  };
  es_subscribe(cli, events, sizeof(events) / sizeof(*events));
  dispatch_main();
}
