/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*-
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: Michael Terry
 */

using GLib;

internal class ResticInstance : ToolInstance
{
  public signal void message(string? msgid, Json.Reader reader);
  public signal void fatal_error(int code, string msg);

  // Custom codes
  public const int ERROR_OUT_OF_SPACE = -1;

  // Official restic codes
  public const int ERROR_GENERIC = 1;
  public const int ERROR_GO_RUNTIME = 2;
  public const int ERROR_INVALID_SOURCE_DATA = 3;
  public const int ERROR_NO_REPOSITORY = 10;
  public const int ERROR_ALREADY_LOCKED = 11;
  public const int ERROR_BAD_PASSWORD = 12;
  public const int ERROR_CANCELED = 130;

  string stray_fatal_message = null;
  string stray_message = null;
  bool had_per_item_errors = false;
  bool had_exit_error = false;

  construct {
    exited.connect(handle_exit);
  }

  protected override string _name()
  {
    return "restic";
  }

  protected override void _send_error(Error e)
  {
    fatal_error(ERROR_GENERIC, e.message);
  }

  protected override void _edit_envp(List<string> envp)
  {
    // Add our custom bin folder to PATH (it holds fusermount wrappers)
    var new_path = Config.PKG_LIBEXEC_DIR;
    var cur_path = Environment.get_variable("PATH");
    if (cur_path != null && cur_path != "")
      new_path += ":" + cur_path;
    // We are the only current code editing PATH, so we don't need to check
    // for conflicting PATH edits.
    envp.append("PATH=" + new_path);
  }

  protected override bool _process_line(string stanza, string line, out bool loggable) throws Error
  {
    loggable = true;

    // Most messages are dictionaries, but the snapshots command gives an array
    var is_stanza = (line.has_prefix("{") && line.has_suffix("}")) ||
                    (line.has_prefix("[") && line.has_suffix("]"));
    if (!is_stanza) {
      // Catch stray fatal error messages, which still sometimes come through
      // even with --json (see for example backing up with an unknown rclone
      // remote)
      if (line.has_prefix("Fatal: ")) {
        stray_fatal_message = line;
      } else {
        // Grab anything else, in case it's the best output we can show
        stray_message = line;
      }

      return true;
    }

    var parser = new Json.Parser.immutable_new();
    parser.load_from_data(stanza);

    var root = parser.get_root();
    var reader = new Json.Reader(root);

    string msgid = null;
    if (reader.read_member("message_type"))
      msgid = reader.get_string_value();
    reader.end_member();

    if (msgid == "status")
      loggable = false; // status is too noisy when debugging

    // Flag if we saw individual file errors during restore or backup.
    // We do this because in those cases, there will be a stray fatal message
    // at the end like "Fatal: There were 2 errors" with exit code 1 but we
    // don't want to treat that as an actual stray unexplained fatal error
    // that should be reported to the user.
    if (msgid == "error")
      had_per_item_errors = true;

    // Handle exit errors separately, because older versions may not yet
    // have a proper code for some errors, and we have to inspect the error
    // string to detect and assign a code.
    if (msgid == "exit_error")
      handle_exit_error(reader);
    else
      message(msgid, reader);

    return true;
  }

  void process_exit_error(int code, string message)
  {
    had_exit_error = true;

    // Ignore "at least one source file could not be read" errors.
    // We separately catch each error message as it comes through and collect
    // them, but we don't want to treat it like a complete backup error.
    if (code == ERROR_INVALID_SOURCE_DATA)
      return;

    // If we had per-item errors, we'll get a general "there were errors" msg
    // at the end - ignore it, upper levels will display "success with errors."
    if (code == ERROR_GENERIC && had_per_item_errors)
      return;

    // Handle cases that aren't converted to a code yet
    // FIXME: need to fix these upstream to emit a json message instead
    if (message.has_suffix(": no space left on device")) {
      // Restic behavior when low on space:
      // - If there isn't enough space for a chunk of data, it reports a fatal
      //   "no space left on device" error. Which we catch here. We can resume
      //   the backup from the last index written, once we free up some space.
      // - Although sometimes it instead jumps to 100% and sits there forever... 🤷
      // - If there isn't enough space to even prune (because it repacks some data),
      //   then... I'm not fully sure, it's hard to reproduce.
      code = ERROR_OUT_OF_SPACE;
    }

    // Strip leading prefix
    var user_message = message;
    if (user_message.has_prefix("Fatal: "))
      user_message = user_message.substring(7);

    fatal_error(code, user_message);
  }

  void handle_exit_error(Json.Reader reader)
  {
    reader.read_member("code");
    var code = (int32)reader.get_int_value();
    reader.end_member();

    reader.read_member("message");
    var message = reader.get_string_value();
    if (message == null)
      message = "";
    reader.end_member();

    process_exit_error(code, message);
  }

  void handle_exit(int code)
  {
    if (code == 0 || had_exit_error)
      return;

    // Sometimes Restic doesn't emit an exit_error message like it should.
    // So we insert an error message here. And sometimes we caught a stray
    // "Fatal:" message being printed to the console (--json didn't kick in).
    // A stray fatal can happen, for example, when calling "snapshots" on a
    // non-existent folder.
    if (stray_fatal_message != null)
      process_exit_error(code, stray_fatal_message);
    else if (stray_message != null)
      process_exit_error(code, stray_message);
    else
      process_exit_error(code, _("Failed with an unknown error."));
  }
}
