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

using GLib;

public errordomain DejaDup.MountError {
  NO_FUSERMOUNT
}

public class DejaDup.MountManager : Object
{
  static MountManager instance;
  public static MountManager get_instance()
  {
    if (instance == null)
      instance = new MountManager();
    return instance;
  }

  public bool has_mounts()
  {
    return operations.length > 0;
  }

  // Checks if fusermount is installed, to offer a nicer user message.
  async void confirm_fusermount() throws MountError
  {
    try {
      var process = new Subprocess(
        SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE,
        Path.build_filename(Config.PKG_LIBEXEC_DIR, "deja-dup-find-fusermount")
      );
      yield process.wait_check_async(null);
    } catch (Error e) {
      throw new MountError.NO_FUSERMOUNT(
        _("The fusermount command is required, but was not found. Please install FUSE.")
      );
    }
  }

  public async string? mount(DejaDup.Operation.State state, string tag, Cancellable cancellable) throws Error
  {
    yield confirm_fusermount();
    yield state.backend.prepare();

    var dir = get_mount_dir(state.tool, state.backend);

    yield fix_stale_mountpoint(dir);
    if (yield is_mountpoint(dir))
      return yield descend(dir, tag);

    var error = "";

    // OK so we have to mount it.
    var op = new OperationMount(state);
    op.done.connect(handle_operation_finished);
    op.raise_error.connect((errstr, detail) => {
      error = errstr;
    });
    operations.insert(op, dir);
    op.start.begin();

    // Wait for it to finish mounting for a bit
    while (!cancellable.is_cancelled()) {
      yield wait(1);
      if (error != "")
        throw new IOError.FAILED(error);
      if (yield is_mountpoint(dir))
        return yield descend(dir, tag);
    }

    op.stop();
    return null;
  }

  // Returns false if any unmount was a failure
  public bool unmount_all()
  {
    var success = true;
    operations.foreach((op, dir) => {
      success = success && unmount(dir);
    });
    return success;
  }

  // A failure (false return) likely means the mount is in use
  public bool unmount(string dir)
  {
    var fusermount = Path.build_filename(Config.PKG_LIBEXEC_DIR, "fusermount");

    try {
      var process = new Subprocess(
        SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE,
        fusermount, "-u", dir
      );
      // flag if we couldn't unmount the dir - it is likely in use.
      return process.wait_check();
    }
    catch (Error err) {
      return false;
    }
  }

  // Private variables
  HashTable<Operation, string> operations;

  construct {
    operations = new HashTable<Operation, string>.full(str_hash, str_equal, unref, free);
  }

  internal string get_mount_dir(ToolPlugin tool, Backend backend)
  {
    var rundir = Environment.get_user_runtime_dir();
    var id = backend.get_unique_id();
    var dir = Path.build_filename(rundir, "deja-dup", tool.name, id);

    // Ensure it exists before we use it
    DirUtils.create_with_parents(dir, 0700);

    return dir;
  }

  async void fix_stale_mountpoint(string dir)
  {
    var file = File.new_for_path(dir);
    try {
      yield file.enumerate_children_async(
        FileAttribute.STANDARD_TYPE + "," + FileAttribute.STANDARD_TYPE,
        FileQueryInfoFlags.NONE, Priority.DEFAULT, null
      );
    } catch (Error e) {
      // This must be a stale mount. We closed down, but the user was using
      // the mount, so the fuse mount persists but broken. Errors like
      // "Transport endpoint is not connected" appear when examining it.
      // So. Try to unmount it to make it viable.
      unmount(dir);
    }
  }

  async bool is_mountpoint(string dir)
  {
    var file = File.new_for_path(dir);
    try {
      var info = yield file.query_info_async(FileAttribute.UNIX_IS_MOUNTPOINT,
                                             FileQueryInfoFlags.NONE,
                                             Priority.DEFAULT, null);
      return info.get_attribute_boolean(FileAttribute.UNIX_IS_MOUNTPOINT);
    }
    catch (Error e) {
      // This could be a stale mount. But not always. It's more reliable to
      // try to enumerate children to detect stale mounts. Anyway, that is done
      // in the is_stale_mount() method. Here we just say it isn't mounted.
      warning("Mountpoint is not ready yet: %s", e.message);
      return false;
    }
  }

  async string descend_recurse(string root)
  {
    // We show the user the first non-trivial folder.
    // i.e. we skip past /home and /home/user etc.
    var file = File.new_for_path(root);

    try {
      var enumerator = yield file.enumerate_children_async(
        FileAttribute.STANDARD_TYPE + "," + FileAttribute.STANDARD_TYPE,
        FileQueryInfoFlags.NONE, Priority.DEFAULT, null
      );
      var infos = yield enumerator.next_files_async(2, Priority.DEFAULT, null);

      // If we have interesting real data, this is the dir we want.
      if (infos.length() != 1)
        return root;
      if (infos.data.get_file_type() != FileType.DIRECTORY)
        return root;

      // We have one single dir child. Keep going!
      var full_child = file.get_child(infos.data.get_name());
      return yield descend_recurse(full_child.get_path());
    }
    catch (Error e) {
      return root;
    }
  }

  async string descend(string root, string tag)
  {
    var new_root = Path.build_filename(root, "ids", tag);
    return yield descend_recurse(new_root);
  }

  void handle_operation_finished(Operation op, bool success, bool canceled, string? detail)
  {
    operations.remove(op);
  }
}
