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

using GLib;

namespace DejaDup {

public const string REMOTE_ROOT = "Remote";
public const string REMOTE_URI_KEY = "uri";
public const string REMOTE_FOLDER_KEY = "folder";

public class BackendRemote : BackendFile
{
  public BackendRemote(Settings? settings) {
    Object(kind: Kind.GVFS,
           settings: (settings != null ? settings : get_settings(REMOTE_ROOT)));
  }

  public virtual string get_folder()
  {
    return get_folder_key(settings, REMOTE_FOLDER_KEY, true);
  }

  // Get mountable root
  protected override File? get_root_from_settings()
  {
    var uri = settings.get_string(REMOTE_URI_KEY);
    var parsed = File.parse_name(uri);
    // Now you might think you could just return `parsed` and be done.
    // But I've encountered issues where a URL like schema://host:/path
    // (note the extra colon there) have caused mounts to hang. But the extra
    // colon is dropped from `get_uri()` - so roundtripping the URI through
    // get_uri() seems to help clean the URI up. So we'll do that here.
    // Reported the colon thing here:
    // https://gitlab.gnome.org/GNOME/gvfs/-/issues/827
    return File.new_for_uri(parsed.get_uri());
  }

  // Get full URI to backup folder
  internal override File? get_file_from_settings()
  {
    var root = get_root_from_settings();
    var folder = get_folder();

    // So ideally the user just put the server address ("sftp://example.org" or
    // "dav://example.org/remote.php/webdav/").  And then we add the folder on
    // top of whatever that location gives as the default location -- which
    // might be the user's home directory or whatever.
    //
    // However... the user might put more in the server address field (and we
    // ourselves might have migrated an old gsettings key into the address
    // field that had the full path as part of it). So if it looks like the
    // URI has more than the mount root in it, we add that together with the
    // folder value to make a new path from the mount root (not the default
    // location root).

    try {
      var mount = root.find_enclosing_mount(null);
      var mount_root = mount.get_root();

      // I've had inconsistent results from gvfs.  On davs://, sometimes
      // equal() isn't correct, but has_prefix() is.  On sftp://, sometimes
      // equal() is correct, but has_prefix() isn't.  We test both, hopefully
      // they both won't be wrong.  The point of this check is that we *should*
      // use default_location(), but won't if the user has added extra bits to
      // the URI for us.  Once GNOME bug 786217 is fixed for a while, we can
      // simply check if there is a relative path between the two.
      if (root.equal(mount_root) || !root.has_prefix(mount_root))
        root = mount.get_default_location();
    }
    catch (IOError.NOT_MOUNTED e) {
      // ignore
    }
    catch (Error e) {
      warning("%s", e.message);
    }

    return root.resolve_relative_path(folder);
  }

  public override bool is_native() {
    return false;
  }

  // Check if we should give nicer message
  string get_unready_message(File root, Error e)
  {
    // SMB likes to give back a very generic error when the host is not
    // available ("Invalid argument").  Try to work around that here.
    // Upstream issue: https://gitlab.gnome.org/GNOME/gvfs/-/issues/315
    if (root.get_uri_scheme() == "smb")
    {
      // Sometimes we see INVALID_ARGUMENT and sometimes we get a vaguer
      // "EGAIN" with no IOError code. Not sure why (server dependent?)
      var egain_error = Posix.errno == Posix.EAGAIN && e.matches(IOError.quark(), 0);
      var invalid_arg_error = e.matches(IOError.quark(), IOError.INVALID_ARGUMENT);
      if (egain_error || invalid_arg_error) {
        return _("The network server is not available");
      }
    }

    return e.message;
  }

  public override async bool is_ready(out string reason, out string message)
  {
    var root = get_root_from_settings();
    reason = "remote-mounted";
    message = null;
    try {
      // Test if we can mount successfully (this is better than simply
      // testing if network is reachable, since ssh configs and all sorts of
      // things might be taken into account by GIO but not by a simple
      // network test). If we do end up mounting it, that's fine.  This is
      // only called right before attempting an operation.
      return yield root.mount_enclosing_volume(MountMountFlags.NONE, mount_op, null);
    }
    catch (IOError.ALREADY_MOUNTED e) {
      // We're mounted! Great. However, the remote server might have become
      // unavailable since being mounted before. Maybe we've walked to a cafe.
      // So let's just do simple query to confirm it's reachable.
      try {
        yield root.query_info_async(FileAttribute.STANDARD_NAME,
                                    FileQueryInfoFlags.NONE,
                                    Priority.DEFAULT, null);
        return true;
      }
      catch (Error e) {
        message = get_unready_message(root, e);
        return false;
      }
    }
    catch (IOError.FAILED_HANDLED e) {
      // Needed user input, so we know we can reach server
      return true;
    }
    catch (Error e) {
      message = get_unready_message(root, e);
      return false;
    }
  }

  public override Icon? get_icon()
  {
    try {
      return Icon.new_for_string("network-server");
    }
    catch (Error e) {
      warning("%s", e.message);
      return null;
    }
  }

  string uninstalled_scheme_message(string scheme)
  {
    return _(
      "You must first install a gvfs backend that can connect to ‘%s’ network locations."
    ).printf(scheme);
  }

  public override bool is_acceptable(out string reason)
  {
    var root = get_root_from_settings();

    if (root.get_uri() == "") {
      reason = _(
        "The server’s network location needs to be specified in the storage location preferences."
      );
      return false;
    }
    if (root.get_uri_scheme() == null) {
      reason = _(
        "The server’s network location ‘%s’ does not look like a network location."
      ).printf(root.get_uri());
      return false;
    }

    if (root.get_uri_scheme() == "http" || root.get_uri_scheme() == "https") {
      reason = _(
        "HTTP network locations are not supported. " +
        "If you want to connect to a WebDAV server, use `davs://` or `dav://` instead."
      );
      return false;
    }

    if (root.get_uri_scheme() == "rest") {
      reason = _(
        "Remote restic servers are not supported. " +
        "Try another connection method (like `sftp://`) instead."
      );
      return false;
    }

    if (root.get_uri_scheme() == "file") {
      // Check if someone put it a local path. This is kinder than the
      // "volume doesn't implement mount" message they'd get otherwise.
      var orig_uri = settings.get_string(REMOTE_URI_KEY);
      var scheme = Uri.peek_scheme(orig_uri);
      if (scheme == null || scheme == "file") {
        reason = _(
          "‘%s’ does not appear to be a network location."
        ).printf(orig_uri);
        return false;
      }

      // This means that we either couldn't talk to the gvfs daemon or the
      // specific backend we need isn't installed - for whatever reason, the
      // real scheme isn't being used and a path like the following is being
      // used instead: file:///home/xxx/ftp:/example.com/
      // Older (or from outside flatpak?) GVfs daemons seem to use this path.
      // Newer (or from inside flatpak?) GVfs daemons let this pass and we hit an
      // error during mount.
      reason = uninstalled_scheme_message(scheme);
      return false;
    }

    if (root.get_uri_scheme() == "smb" && root.get_uri().split("/").length < 5) {
      // Special sanity check for some edge cases like smb:// where if the user
      // just puts in smb://server/ as the root, GIO thinks it's a valid root,
      // but the share never ends up mounted.
      reason = _("Samba network locations must include both a hostname and a share name.");
      return false;
    }

    return base.is_acceptable(out reason);
  }

  public void check_fuse(File root) throws Error
  {
    if (root.get_path() == null) {
      throw new IOError.FAILED("%s", _(
        "You must first install FUSE support for GVfs, " +
        "likely provided by the gvfs-fuse package. " +
        "Then log out, log back in, and try again."
      ));
    }
  }

  public override async bool mount() throws Error
  {
    if (!Network.get().connected) {
      pause_op(_("Storage location not available"),
               _("Waiting for a network connection…"));
      var sigid = Network.get().notify["connected"].connect(() => {
        if (Network.get().connected)
          mount.callback();
      });
      yield;
      Network.get().disconnect(sigid);
      pause_op(null, null);
    }

    var root = get_root_from_settings();
    if (root == null)
      throw new IOError.FAILED("%s", _("Could not mount storage location."));

    string reason;
    if (!is_acceptable(out reason)) {
      throw new IOError.FAILED("%s", reason);
    }

    try {
      yield root.mount_enclosing_volume(MountMountFlags.NONE, mount_op, null);
    } catch (IOError.ALREADY_MOUNTED e) {
      check_fuse(root);
      return false;
    } catch (IOError.FAILED_HANDLED e) {
      // needed mount_op but none provided
      throw new BackendError.MOUNT_OP_NEEDED("");
    } catch (IOError.PERMISSION_DENIED e) {
      throw new BackendError.MOUNT_OP_NEEDED("");
    } catch (IOError.NOT_MOUNTED e) {
      // Use a more descriptive error than the default "Location is not mountable" message
      throw new IOError.NOT_MOUNTED(uninstalled_scheme_message(root.get_uri_scheme()));
    }

    check_fuse(root);

    // FIXME: I've found issues with mounting and unmounting often - it seems
    // to confuse FUSE. Which, fair. So just litter the user's session with
    // our mount and return false here.
    return false;
  }
}

} // end namespace
