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

/**
 * This backend is an escape valve for the wide world of cloud services.
 */

using GLib;

namespace DejaDup {

public const string RCLONE_ROOT = "Rclone";
public const string RCLONE_REMOTE_KEY = "remote";
public const string RCLONE_FOLDER_KEY = "folder";

public class BackendRclone : Backend
{
  SourceFunc config_password_callback;
  string config_password;

  public BackendRclone(Settings? settings) {
    Object(kind: Kind.RCLONE,
           settings: (settings != null ? settings : get_settings(RCLONE_ROOT)));
  }

  public override bool is_native() {
    return false;
  }

  public override bool needs_dependencies() {
    return Environment.find_program_in_path(Rclone.rclone_command()) == null;
  }

  public override string[] get_dependencies() {
    return Config.RCLONE_PACKAGES.split(",");
  }

  public override Icon? get_icon() {
    return new ThemedIcon("deja-dup-rclone");
  }

  public override async bool is_ready(out string reason, out string message) {
    reason = "rclone-reachable";
    message = _("Backup will begin when a network connection becomes available.");
    return Network.get().connected;
  }

  string get_remote()
  {
    var remote = settings.get_string(RCLONE_REMOTE_KEY);
    // some users have been unsure if colon should be included, so strip if so.
    if (remote.has_suffix(":"))
      return remote.substring(0, remote.length - 1);
    return remote;
  }

  string get_folder()
  {
    return get_folder_key(settings, RCLONE_FOLDER_KEY, true);
  }

  public override string get_location_pretty()
  {
    var remote = get_remote();
    var folder = get_folder();
    if (remote == "")
      return _("Rclone");
    else
      // Translators: %s is a folder.
      return _("%s with Rclone").printf("%s:%s".printf(remote, folder));
  }

  protected override string get_unique_location()
  {
    var remote = get_remote();
    var folder = get_folder();
    return "rclone:%s:%s".printf(remote, folder);
  }

  public override bool is_acceptable(out string reason)
  {
    reason = null;

    if (get_remote() == "") {
      reason = _("An Rclone remote needs to be set.");
      return false;
    }

    return true;
  }

  async bool meets_version(int req_major, int req_minor, int req_micro)
  {
    var rclone = yield Rclone.run({"version"});
    if (rclone == null)
      return false;

    var stdout_raw = rclone.get_stdout_pipe();
    var stdout = new DataInputStream(stdout_raw);
    string line;
    try {
      line = yield stdout.read_line_utf8_async(Priority.DEFAULT);
    } catch (Error err) {
      warning("Could not parse Rclone output: %s", err.message);
      return false;
    }

    var tokens = line.split(" ");
    if (tokens.length != 2 || tokens[0] != "rclone") {
      warning("Could not understand Rclone version line ‘%s’.".printf(line));
      return false;
    }

    var version = tokens[1];
    if (version.has_prefix("v"))
      version = version[1:];

    int major, minor, micro;
    if (!DejaDup.parse_version(version, out major, out minor, out micro)) {
      warning("Could not understand Rclone version ‘%s’.".printf(version));
      return false;
    }

    return DejaDup.meets_version(major, minor, micro, req_major, req_minor, req_micro);
  }

  async bool is_rclone_config_encrypted()
  {
    // The "config encryption" set of commands were added in 1.68.0.
    // Before then, we'll just not support encryption.
    if (!yield meets_version(1, 68, 0))
      return false;

    // There is also an "encryption check" command, but that seems more
    // interested in confirming you have the right password already.
    // Unencrypted returns 1 and encrypted-but-bad-password also returns 1.
    // But trying to remove a password without providing one does have a clear
    // difference. (we do use encryption check below for validating password)
    var rclone = yield Rclone.run({"config", "encryption", "remove"});
    if (rclone == null)
      return false;

    try {
      yield rclone.wait_async();
    } catch (Error err) {
      warning("Could not check if Rclone config is encrypted: %s", err.message);
      return false;
    }

    return rclone.get_if_exited() && rclone.get_exit_status() != 0;
  }

  async bool is_rclone_config_password_valid()
  {
    if (config_password == null)
      return false;

    var rclone = yield Rclone.run({"config", "encryption", "check"}, this, false);
    if (rclone == null)
      return false;

    try {
      yield rclone.wait_async();
    } catch (Error err) {
      warning("Could not check if Rclone password is valid: %s", err.message);
      return false;
    }

    return rclone.get_if_exited() && rclone.get_exit_status() == 0;
  }

  async bool is_remote_valid()
  {
    var remote = get_remote();
    if (remote == "")
      return false;
    if (remote[0] == ':')
      return true; // dynamic remote, not in the config

    // OK this is a remote from the config - check the list
    remote = remote + ":";

    var rclone = yield Rclone.run({"listremotes"}, this, false);
    if (rclone == null)
      return false;

    // Grab stdout
    var stdout_raw = rclone.get_stdout_pipe();
    var stdout = new DataInputStream(stdout_raw);

    // And search for the expected remote
    var found_it = false;
    while (true) {
      try {
        var line = yield stdout.read_line_utf8_async(Priority.DEFAULT);
        if (line == null)
          break;
        if (line == remote) {
          found_it = true;
          break;
        }
      } catch (Error err) {
        warning("Could not parse Rclone output: %s", err.message);
        break;
      }
    }

    rclone.force_exit();
    return found_it;
  }

  Secret.Schema get_secret_schema()
  {
    return new Secret.Schema(
      Config.APPLICATION_ID + ".Rclone", Secret.SchemaFlags.NONE
    );
  }

  public async string? lookup_config_password()
  {
    var schema = get_secret_schema();
    try {
      return Secret.password_lookup_sync(schema, null);
    } catch (Error e) {
      // Ignore, just act like we didn't find it
      return null;
    }
  }

  async void store_config_password()
  {
    var schema = get_secret_schema();
    try {
      Secret.password_store_sync(schema,
                                 Secret.COLLECTION_DEFAULT,
                                 _("Rclone config encryption password"),
                                 config_password,
                                 null);
    } catch (Error e) {
      warning("%s\n", e.message);
    }
  }

  public async void clear_config_password()
  {
    var schema = get_secret_schema();
    try {
      Secret.password_clear_sync(schema, null);
      // any observers need to reset as if we have a new backend (because the
      // user will need to re-authenticate)
      BackendWatcher.get_instance().changed();
    } catch (Error e) {
      // Ignore
    }
  }

  public override async void prepare() throws Error
  {
    if (yield is_rclone_config_encrypted()) {
      if (config_password == null)
        config_password = yield lookup_config_password();

      var repeat = false;
      while (!yield is_rclone_config_password_valid()) {
        config_password_callback = prepare.callback;
        needs_backend_password = true;
        show_backend_password_page(
          _("Your Rclone config file is encrypted. Please provide its password."),
          _("Rclone Config Encryption _Password"),
          repeat ? _("Wrong Rclone config encryption password. Try again.") : null
        );
        yield;
        repeat = true;
      }
    }

    if (!yield is_remote_valid()) {
      var msg = _("Rclone remote '%s' needs to be configured in Rclone first.").printf(get_remote());
      throw new IOError.FAILED("%s", msg);
    }
  }

  public override async void provide_backend_password(string password, bool save)
  {
    needs_backend_password = false;
    config_password = password;

    if (save)
      yield store_config_password();
    else
      yield clear_config_password();

    if (config_password_callback != null)
      config_password_callback();
  }

  public override async void get_space(out uint64 free, out uint64 total)
  {
    yield Rclone.get_space(this, out free, out total);
  }

  public override async List<string> peek_at_files()
  {
    return yield Rclone.list_files(this, 20);
  }

  public string fill_envp(ref List<string> envp)
  {
    if (config_password != null)
      envp.append("RCLONE_CONFIG_PASS=" + config_password);
    return "%s:%s".printf(get_remote(), get_folder());
  }
}

} // end namespace

