# docs
# - x-systemd options: <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
# - fuse options: `man mount.fuse`

{ config, lib, pkgs, sane-lib, utils, ... }:

let
  fsOpts = rec {
    common = [
      "_netdev"
      "noatime"
      # user: allow any user with access to the device to mount the fs.
      #       note that this requires a suid `mount` binary; see: <https://zameermanji.com/blog/2022/8/5/using-fuse-without-root-on-linux/>
      "user"
      "x-systemd.requires=network-online.target"
      "x-systemd.after=network-online.target"
      "x-systemd.mount-timeout=10s"  # how long to wait for mount **and** how long to wait for unmount
    ];
    # x-systemd.automount: mount the fs automatically *on first access*.
    # creates a `path-to-mount.automount` systemd unit.
    automount = [ "x-systemd.automount" ];
    # noauto: don't mount as part of remote-fs.target.
    # N.B.: `remote-fs.target` is a dependency of multi-user.target, itself of graphical.target.
    # hence, omitting `noauto` can slow down boots.
    noauto = [ "noauto" ];
    # lazyMount: defer mounting until first access from userspace.
    # see: `man systemd.automount`, `man automount`, `man autofs`
    lazyMount = noauto ++ automount;

    fuse = [
      "allow_other"  # allow users other than the one who mounts it to access it. needed, if systemd is the one mounting this fs (as root)
      # allow_root: allow root to access files on this fs (if mounted by non-root, else it can always access them).
      #             N.B.: if both allow_root and allow_other are specified, then only allow_root takes effect.
      # "allow_root"
      # default_permissions: enforce local permissions check. CRUCIAL if using `allow_other`.
      # w/o this, permissions mode of sshfs is like:
      # - sshfs runs all remote commands as the remote user.
      # - if a local user has local permissions to the sshfs mount, then their file ops are sent blindly across the tunnel.
      # - `allow_other` allows *any* local user to access the mount, and hence any local user can now freely become the remote mapped user.
      # with default_permissions, sshfs doesn't tunnel file ops from users until checking that said user could perform said op on an equivalent local fs.
      "default_permissions"
    ];
    fuseColin = fuse ++ [
      "uid=1000"
      "gid=100"
    ];

    ssh = common ++ fuse ++ [
      "identityfile=/home/colin/.ssh/id_ed25519"
      # i *think* idmap=user means that `colin` on `localhost` and `colin` on the remote are actually treated as the same user, even if their uid/gid differs?
      # i.e., local colin's id is translated to/from remote colin's id on every operation?
      "idmap=user"
    ];
    sshColin = ssh ++ fuseColin ++ [
      # follow_symlinks: remote files which are symlinks are presented to the local system as ordinary files (as the target of the symlink).
      #                  if the symlink target does not exist, the presentation is unspecified.
      #                  symlinks which point outside the mount ARE followed. so this is more capable than `transform_symlinks`
      "follow_symlinks"
      # symlinks on the remote fs which are absolute paths are presented to the local system as relative symlinks pointing to the expected data on the remote fs.
      # only symlinks which would point inside the mountpoint are translated.
      "transform_symlinks"
    ];
    # sshRoot = ssh ++ [
    #   # we don't transform_symlinks because that breaks the validity of remote /nix stores
    #   "sftp_server=/run/wrappers/bin/sudo\\040/run/current-system/sw/libexec/sftp-server"
    # ];
    # in the event of hunt NFS mounts, consider:
    # - <https://unix.stackexchange.com/questions/31979/stop-broken-nfs-mounts-from-locking-a-directory>

    # NFS options: <https://linux.die.net/man/5/nfs>
    # actimeo=n  = how long (in seconds) to cache file/dir attributes (default: 3-60s)
    # bg         = retry failed mounts in the background
    # retry=n    = for how many minutes `mount` will retry NFS mount operation
    # intr       = allow Ctrl+C to abort I/O (it will error with `EINTR`)
    # soft       = on "major timeout", report I/O error to userspace
    # softreval  = on "major timeout", service the request using known-stale cache results instead of erroring -- if such cache data exists
    # retrans=n  = how many times to retry a NFS request before giving userspace a "server not responding" error (default: 3)
    # timeo=n    = number of *deciseconds* to wait for a response before retrying it (default: 600)
    #              note: client uses a linear backup, so the second request will have double this timeout, then triple, etc.
    # proto=udp  = encapsulate protocol ops inside UDP packets instead of a TCP session.
    #              requires `nfsvers=3` and a kernel compiled with `NFS_DISABLE_UDP_SUPPORT=n`.
    #              UDP might be preferable to TCP because the latter is liable to hang for ~100s (kernel TCP timeout) after a link drop.
    #              however, even UDP has issues with `umount` hanging.
    #
    # N.B.: don't change these without first testing the behavior of sandboxed apps on a flaky network.
    nfs = common ++ [
      # "actimeo=5"
      # "bg"
      "retrans=1"
      "retry=0"
      # "intr"
      "soft"
      "softreval"
      "timeo=30"
      "nofail"  # don't fail remote-fs.target when this mount fails  (not an option for sshfs else would be common)
      # "proto=udp"  # default kernel config doesn't support NFS over UDP: <https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1964093> (see comment 11).
      # "nfsvers=3"  # NFSv4+ doesn't support UDP at *all*. it's ok to omit nfsvers -- server + client will negotiate v3 based on udp requirement. but omitting causes confusing mount errors when the server is *offline*, because the client defaults to v4 and thinks the udp option is a config error.
      # "x-systemd.idle-timeout=10"  # auto-unmount after this much inactivity
    ];

    # manually perform a ftp mount via e.g.
    #   curlftpfs -o ftpfs_debug=2,user=anonymous:anonymous,connect_timeout=10 -f -s ftp://servo-hn /mnt/my-ftp
    ftp = common ++ fuseColin ++ [
      # "ftpfs_debug=2"
      "user=colin:ipauth"
      # connect_timeout=10: casting shows to T.V. fails partway through about half the time
      "connect_timeout=20"
    ];
  };
  remoteHome = host: {
    sane.programs.sshfs-fuse.enableFor.system = true;
    fileSystems."/mnt/${host}/home" = {
      device = "colin@${host}:/home/colin";
      fsType = "fuse.sshfs";
      options = fsOpts.sshColin ++ fsOpts.lazyMount;
      noCheck = true;
    };
    sane.fs."/mnt/${host}/home" = sane-lib.fs.wanted {
      dir.acl.user = "colin";
      dir.acl.group = "users";
      dir.acl.mode = "0700";
    };
  };
  remoteServo = subdir: let
    localPath = "/mnt/servo/${subdir}";
    systemdName = utils.escapeSystemdPath localPath;
  in {
    sane.programs.curlftpfs.enableFor.system = true;
    fileSystems."${localPath}" = {
      device = "ftp://servo-hn:/${subdir}";
      noCheck = true;
      fsType = "fuse.curlftpfs";
      options = fsOpts.ftp ++ fsOpts.noauto;
      # fsType = "nfs";
      # options = fsOpts.nfs ++ fsOpts.lazyMount;
    };
    sane.fs."${localPath}" = {
      dir.acl.user = "colin";
      dir.acl.group = "users";
      dir.acl.mode = "0750";
      wantedBy = [ "default.target" ];
      mount.depends = [ "network-online.target" ];
      #VVV patch so that when the mount fails, we start a timer to remount it.
      #    and for a disconnection after a good mount (onSuccess), restart the timer to be more aggressive
      mount.unitConfig.OnFailure = [ "${systemdName}.timer" ];
      mount.unitConfig.OnSuccess = [ "${systemdName}-restart-timer.target" ];

      mount.mountConfig.TimeoutSec = "10s";
      mount.mountConfig.User = "colin";
      mount.mountConfig.Group = "users";
      # hardening (systemd-analyze security mnt-servo-playground.mount)
      mount.mountConfig.AmbientCapabilities = "CAP_SYS_ADMIN";
      mount.mountConfig.CapabilityBoundingSet = "CAP_SYS_ADMIN";
      mount.mountConfig.LockPersonality = true;
      mount.mountConfig.MemoryDenyWriteExecute = true;
      mount.mountConfig.NoNewPrivileges = true;
      mount.mountConfig.ProtectClock = true;
      mount.mountConfig.ProtectHostname = true;
      mount.mountConfig.RemoveIPC = true;
      mount.mountConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
      #VVV this includes anything it reads from, e.g. /bin/sh; /nix/store/...
      # see `systemd-analyze filesystems` for a full list
      mount.mountConfig.RestrictFileSystems = "@common-block @basic-api fuse";
      mount.mountConfig.RestrictNamespaces = true;
      mount.mountConfig.RestrictRealtime = true;
      mount.mountConfig.RestrictSUIDSGID = true;
      mount.mountConfig.SystemCallArchitectures = "native";
      mount.mountConfig.SystemCallFilter = [
        "@system-service"
        "@mount"
        "~@chown"
        "~@cpu-emulation"
        "~@keyring"
        # could remove almost all io calls, however one has to keep `open`, and `write`, to communicate with the fuse device.
        # so that's pretty useless as a way to prevent write access
      ];
      mount.mountConfig.IPAddressDeny = "any";
      mount.mountConfig.IPAddressAllow = "10.0.10.5";
      mount.mountConfig.DevicePolicy = "closed";  # only allow /dev/{null,zero,full,random,urandom}
      mount.mountConfig.DeviceAllow = "/dev/fuse";
    };

    systemd.targets."${systemdName}-restart-timer" = {
      # hack unit which, when started, stops the timer (if running), and then starts it again.
      after = [ "${systemdName}.timer" ];
      conflicts = [ "${systemdName}.timer" ];
      upholds = [ "${systemdName}.timer" ];
      unitConfig.StopWhenUnneeded = true;
    };
    systemd.timers."${systemdName}" = {
      timerConfig.Unit = "${systemdName}.mount";
      timerConfig.AccuracySec = "2s";
      timerConfig.OnActiveSec = [
        # try to remount at these timestamps, backing off gradually
        # there seems to be an implicit mount attempt at t=0.
        "10s"
        "30s"
        "60s"
        "120s"
      ];
      # cap the backoff to a fixed interval.
      timerConfig.OnUnitActiveSec = [ "120s" ];
    };
  };
in
lib.mkMerge [
  {
    # some services which use private directories error if the parent (/var/lib/private) isn't 700.
    sane.fs."/var/lib/private".dir.acl.mode = "0700";

    # in-memory compressed RAM
    # defaults to compressing at most 50% size of RAM
    # claimed compression ratio is about 2:1
    # - but on moby w/ zstd default i see 4-7:1  (ratio lowers as it fills)
    # note that idle overhead is about 0.05% of capacity (e.g. 2B per 4kB page)
    # docs: <https://www.kernel.org/doc/Documentation/blockdev/zram.txt>
    #
    # to query effectiveness:
    # `cat /sys/block/zram0/mm_stat`. whitespace separated fields:
    # - *orig_data_size*  (bytes)
    # - *compr_data_size* (bytes)
    # - mem_used_total    (bytes)
    # - mem_limit         (bytes)
    # - mem_used_max      (bytes)
    # - *same_pages*  (pages which are e.g. all zeros (consumes no additional mem))
    # - *pages_compacted*  (pages which have been freed thanks to compression)
    # - huge_pages  (incompressible)
    #
    # see also:
    # - `man zramctl`
    zramSwap.enable = true;
    # how much ram can be swapped into the zram device.
    # this shouldn't be higher than the observed compression ratio.
    # the default is 50% (why?)
    # 100% should be "guaranteed" safe so long as the data is even *slightly* compressible.
    # but it decreases working memory under the heaviest of loads by however much space the compressed memory occupies (e.g. 50% if 2:1; 25% if 4:1)
    zramSwap.memoryPercent = 100;

    # environment.pathsToLink = [
    #   # needed to achieve superuser access for user-mounted filesystems (see sshRoot above)
    #   # we can only link whole directories here, even though we're only interested in pkgs.openssh
    #   "/libexec"
    # ];

    programs.fuse.userAllowOther = true;  #< necessary for `allow_other` or `allow_root` options.
  }

  (remoteHome "crappy")
  (remoteHome "desko")
  (remoteHome "lappy")
  (remoteHome "moby")
  # this granularity of servo media mounts is necessary to support sandboxing:
  # for flaky mounts, we can only bind the mountpoint itself into the sandbox,
  # so it's either this or unconditionally bind all of media/.
  (remoteServo "media/archive")
  (remoteServo "media/Books")
  (remoteServo "media/collections")
  # (remoteServo "media/datasets")
  (remoteServo "media/games")
  (remoteServo "media/Music")
  (remoteServo "media/Pictures/macros")
  (remoteServo "media/torrents")
  (remoteServo "media/Videos")
  (remoteServo "playground")
]

