{ config, pkgs, lib, ... }:

with lib;

let
  mkKeyValue = key: value:
    if value == true then
    # sets with a true boolean value are coerced to just the key name
      key
    else if value == false then
    # or omitted completely when false
      ""
    else
      (generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);

  mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);

  mkValueString = value:
    if isList value then
      (concatStringsSep ", " (map mkValueString value))
    else if isAttrs value then
      "\n" + (mkAttrsString value)
    else
      (generators.mkValueStringDefault { } value);

  mkSectionName = value: "[" + (escape [ "[" "]" ] value) + "]";

  mkSection = name: attrs: ''
    ${mkSectionName name}
    ${mkAttrsString attrs}
  '';

  mkVolume = name: attrs: ''
    ${mkSectionName name}
    ${attrs.path}
    ${mkAttrsString {
      accs = attrs.access;
      flags = attrs.flags;
    }}
  '';

  passwordPlaceholder = name: "{{password-${name}}}";

  accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);

  configStr = ''
    ${mkSection "global" cfg.settings}
    ${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
    ${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
  '';

  name = "copyparty";
  cfg = config.services.copyparty;
  configFile = pkgs.writeText "${name}.conf" configStr;
  runtimeConfigPath = "/run/${name}/${name}.conf";
  home = "/var/lib/${name}";
  defaultShareDir = "${home}/data";
in {
  options.services.copyparty = {
    enable = mkEnableOption "web-based file manager";

    package = mkOption {
      type = types.package;
      default = pkgs.copyparty;
      defaultText = "pkgs.copyparty";
      description = ''
        Package of the application to run, exposed for overriding purposes.
      '';
    };

    openFilesLimit = mkOption {
      default = 4096;
      type = types.either types.int types.str;
      description = "Number of files to allow copyparty to open.";
    };

    settings = mkOption {
      type = types.attrs;
      description = ''
        Global settings to apply.
        Directly maps to values in the [global] section of the copyparty config.
        See `${getExe cfg.package} --help` for more details.
      '';
      default = {
        i = "127.0.0.1";
        no-reload = true;
      };
      example = literalExpression ''
        {
          i = "0.0.0.0";
          no-reload = true;
        }
      '';
    };

    accounts = mkOption {
      type = types.attrsOf (types.submodule ({ ... }: {
        options = {
          passwordFile = mkOption {
            type = types.str;
            description = ''
              Runtime file path to a file containing the user password.
              Must be readable by the copyparty user.
            '';
            example = "/run/keys/copyparty/ed";
          };
        };
      }));
      description = ''
        A set of copyparty accounts to create.
      '';
      default = { };
      example = literalExpression ''
        {
          ed.passwordFile = "/run/keys/copyparty/ed";
        };
      '';
    };

    volumes = mkOption {
      type = types.attrsOf (types.submodule ({ ... }: {
        options = {
          path = mkOption {
            type = types.str;
            description = ''
              Path of a directory to share.
            '';
          };
          access = mkOption {
            type = types.attrs;
            description = ''
              Attribute list of permissions and the users to apply them to.

              The key must be a string containing any combination of allowed permission:
                "r" (read):   list folder contents, download files
                "w" (write):  upload files; need "r" to see the uploads
                "m" (move):   move files and folders; need "w" at destination
                "d" (delete): permanently delete files and folders
                "g" (get):    download files, but cannot see folder contents
                "G" (upget):  "get", but can see filekeys of their own uploads
                "h" (html):   "get", but folders return their index.html
                "a" (admin):  can see uploader IPs, config-reload

              For example: "rwmd"

              The value must be one of:
                an account name, defined in `accounts`
                a list of account names
                "*", which means "any account"
            '';
            example = literalExpression ''
              {
                # wG = write-upget = see your own uploads only
                wG = "*";
                # read-write-modify-delete for users "ed" and "k"
                rwmd = ["ed" "k"];
              };
            '';
          };
          flags = mkOption {
            type = types.attrs;
            description = ''
              Attribute list of volume flags to apply.
              See `${getExe cfg.package} --help-flags` for more details.
            '';
            example = literalExpression ''
              {
                # "fk" enables filekeys (necessary for upget permission) (4 chars long)
                fk = 4;
                # scan for new files every 60sec
                scan = 60;
                # volflag "e2d" enables the uploads database
                e2d = true;
                # "d2t" disables multimedia parsers (in case the uploads are malicious)
                d2t = true;
                # skips hashing file contents if path matches *.iso
                nohash = "\.iso$";
              };
            '';
            default = { };
          };
        };
      }));
      description = "A set of copyparty volumes to create";
      default = {
        "/" = {
          path = defaultShareDir;
          access = { r = "*"; };
        };
      };
      example = literalExpression ''
        {
          "/" = {
            path = ${defaultShareDir};
            access = {
              # wG = write-upget = see your own uploads only
              wG = "*";
              # read-write-modify-delete for users "ed" and "k"
              rwmd = ["ed" "k"];
            };
          };
        };
      '';
    };
  };

  config = mkIf cfg.enable {
    systemd.services.copyparty = {
      description = "http file sharing hub";
      wantedBy = [ "multi-user.target" ];

      environment = {
        PYTHONUNBUFFERED = "true";
        XDG_CONFIG_HOME = "${home}/.config";
      };

      preStart = let
        replaceSecretCommand = name: attrs:
          "${getExe pkgs.replace-secret} '${
            passwordPlaceholder name
          }' '${attrs.passwordFile}' ${runtimeConfigPath}";
      in ''
        set -euo pipefail
        install -m 600 ${configFile} ${runtimeConfigPath}
        ${concatStringsSep "\n"
        (mapAttrsToList replaceSecretCommand cfg.accounts)}
      '';

      serviceConfig = {
        Type = "simple";
        ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";

        # Hardening options
        User = "copyparty";
        Group = "copyparty";
        RuntimeDirectory = name;
        RuntimeDirectoryMode = "0700";
        StateDirectory = [ name "${name}/data" "${name}/.config" ];
        StateDirectoryMode = "0700";
        WorkingDirectory = home;
        TemporaryFileSystem = "/:ro";
        BindReadOnlyPaths = [
          "/nix/store"
          "-/etc/resolv.conf"
          "-/etc/nsswitch.conf"
          "-/etc/hosts"
          "-/etc/localtime"
        ] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
        BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
        # Would re-mount paths ignored by temporary root
        #ProtectSystem = "strict";
        ProtectHome = true;
        PrivateTmp = true;
        PrivateDevices = true;
        ProtectKernelTunables = true;
        ProtectControlGroups = true;
        RestrictSUIDSGID = true;
        PrivateMounts = true;
        ProtectKernelModules = true;
        ProtectKernelLogs = true;
        ProtectHostname = true;
        ProtectClock = true;
        ProtectProc = "invisible";
        ProcSubset = "pid";
        RestrictNamespaces = true;
        RemoveIPC = true;
        UMask = "0077";
        LimitNOFILE = cfg.openFilesLimit;
        NoNewPrivileges = true;
        LockPersonality = true;
        RestrictRealtime = true;
      };
    };

    users.groups.copyparty = { };
    users.users.copyparty = {
      description = "Service user for copyparty";
      group = "copyparty";
      home = home;
      isSystemUser = true;
    };
  };
}
