{ self }:
{
  config,
  lib,
  pkgs,
  ...
}:
let
  system = pkgs.stdenv.hostPlatform.system;
  hostedCfg = config.services.malice;
  deviceCfg = config.programs.malice;
  inherit (lib)
    mkEnableOption
    mkIf
    mkMerge
    mkOption
    types
    ;
  deviceUrl = "http://127.0.0.1:${toString deviceCfg.port}/";
  browserCommand =
    if lib.hasInfix "%u" deviceCfg.browser then
      lib.replaceStrings [ "%u" ] [ deviceUrl ] deviceCfg.browser
    else
      "${deviceCfg.browser} ${deviceUrl}";
  malicOpen = pkgs.writeShellScriptBin "malice-open" ''
    exec ${browserCommand}
  '';
  malicDesktopItem = pkgs.makeDesktopItem {
    name = "malice";
    desktopName = "Malice";
    comment = "Open the on-device Malice Matrix client";
    exec = "${malicOpen}/bin/malice-open";
    terminal = false;
    categories = [
      "Network"
      "InstantMessaging"
    ];
  };
in
{
  options.services.malice = {
    enable = mkEnableOption "Malice hosted Matrix client";

    package = mkOption {
      type = types.package;
      default = self.packages.${system}.default;
      description = "Malice server and bundled Svelte frontend.";
    };

    listenAddress = mkOption {
      type = types.str;
      default = "127.0.0.1";
      description = "Address to bind. Keep behind an HTTPS reverse proxy for secure browser cookies.";
    };

    port = mkOption {
      type = types.port;
      default = 3000;
      description = "HTTP listener port.";
    };

    openFirewall = mkOption {
      type = types.bool;
      default = false;
      description = "Open the HTTP listener port in the firewall.";
    };
  };

  options.programs.malice = {
    enable = mkEnableOption "Malice on-device Matrix client";

    package = mkOption {
      type = types.package;
      default = self.packages.${system}.local;
      description = "The malice binary wrapped for the on-device build (MALICE_WEB_DIR and MALICE_STATIC_ONLY set).";
    };

    port = mkOption {
      type = types.port;
      default = 38465;
      description = ''
        Loopback port the socket-activated server listens on. This port is
        part of the browser origin malice runs at, so changing it hides
        accounts logged in at the old origin (their data is still on disk,
        just unreachable at the new URL). Everyone logged into this machine
        at once shares this one port; per-user ports are not supported yet.
      '';
    };

    browser = mkOption {
      type = types.str;
      default = "xdg-open %u";
      description = ''
        Command the desktop entry runs to open malice. `%u` (desktop-entry
        convention) is replaced with malice's URL; if the command has no
        `%u`, the URL is appended after a space instead. E.g. "chromium
        --app=%u" to open malice in an app window instead of a browser tab.
      '';
    };
  };

  config = mkMerge [
    (mkIf hostedCfg.enable {
      systemd.services.malice = {
        description = "Malice hosted Matrix client";
        wantedBy = [ "multi-user.target" ];
        after = [ "network-online.target" ];
        wants = [ "network-online.target" ];
        environment = {
          MALICE_BIND = "${hostedCfg.listenAddress}:${toString hostedCfg.port}";
          MALICE_DATA_DIR = "/var/lib/malice";
          MALICE_WEB_DIR = "${self.packages.${system}.web}/share/malice";
          MALICE_SESSION_COOKIE_SECURE = "true";
        };
        serviceConfig = {
          ExecStart = lib.getExe' hostedCfg.package "malice";
          DynamicUser = true;
          StateDirectory = "malice";
          StateDirectoryMode = "0700";
          UMask = "0077";
          Restart = "on-failure";
          RestartSec = 5;
          NoNewPrivileges = true;
          ProtectSystem = "strict";
          ProtectHome = true;
          PrivateTmp = true;
          RestrictAddressFamilies = [
            "AF_INET"
            "AF_INET6"
            "AF_UNIX"
          ];
        };
      };
      networking.firewall.allowedTCPPorts = lib.optionals hostedCfg.openFirewall [ hostedCfg.port ];
    })
    (mkIf deviceCfg.enable {
      systemd.user.sockets.malice = {
        description = "Malice on-device Matrix client socket";
        wantedBy = [ "sockets.target" ];
        socketConfig.ListenStream = "127.0.0.1:${toString deviceCfg.port}";
      };
      systemd.user.services.malice = {
        description = "Malice on-device Matrix client";
        environment = {
          MALICE_STATIC_ONLY = "1";
          MALICE_IDLE_EXIT_SECS = "300";
        };
        serviceConfig = {
          ExecStart = lib.getExe' deviceCfg.package "malice";
        };
      };
      environment.systemPackages = [
        malicOpen
        malicDesktopItem
      ];
    })
  ];
}
