{
  config,
  lib,
  pkgs,
  ...
}: let
  inherit
    (lib)
    hasAttr
    hasPrefix
    maintainers
    mapAttrs
    mkDefault
    mkEnableOption
    mkIf
    mkMerge
    mkOption
    mkPackageOption
    optional
    optionalAttrs
    optionalString
    types
    ;

  cfg = config.services.immich;
  serverCfg = config.services.immich.server;
  backendCfg = serverCfg.backend;
  microservicesCfg = serverCfg.microservices;
  webCfg = cfg.web;
  mlCfg = cfg.machineLearning;

  isServerPostgresUnix = hasPrefix "/" serverCfg.postgres.host;
  postgresEnv =
    if isServerPostgresUnix
    then {
      # If passwordFile is given, this will be overwritten in ExecStart
      DB_URL = "socket://${serverCfg.postgres.host}?dbname=${serverCfg.postgres.database}";
    }
    else {
      DB_HOSTNAME = serverCfg.postgres.host;
      DB_PORT = toString serverCfg.postgres.port;
      DB_DATABASE_NAME = serverCfg.postgres.database;
      DB_USERNAME = serverCfg.postgres.username;
    };

  typesenseEnv =
    {
      TYPESENSE_ENABLED = toString serverCfg.typesense.enable;
    }
    // optionalAttrs serverCfg.typesense.enable {
      TYPESENSE_HOST = serverCfg.typesense.host;
      TYPESENSE_PORT = toString serverCfg.typesense.port;
      TYPESENSE_PROTOCOL = serverCfg.typesense.protocol;
    };

  # Don't start a redis instance if the user sets a custom redis connection
  enableRedis = !hasAttr "REDIS_URL" serverCfg.extraConfig && !hasAttr "REDIS_SOCKET" serverCfg.extraConfig;
  redisServerCfg = config.services.redis.servers.immich;
  redisEnv = optionalAttrs enableRedis {
    REDIS_SOCKET = redisServerCfg.unixSocket;
  };

  serverEnv =
    postgresEnv
    // typesenseEnv
    // redisEnv
    // {
      NODE_ENV = "production";

      IMMICH_MEDIA_LOCATION = serverCfg.mediaDir;
      IMMICH_MACHINE_LEARNING_URL =
        if serverCfg.machineLearningUrl != null
        then serverCfg.machineLearningUrl
        else "false";
    };

  serverStartWrapper = program: ''
    set -euo pipefail
    mkdir -p ${serverCfg.mediaDir}

    ${optionalString (serverCfg.postgres.passwordFile != null) (
      if isServerPostgresUnix
      then ''export DB_URL="socket://${serverCfg.postgres.username}:$(cat ${serverCfg.postgres.passwordFile})@${serverCfg.postgres.host}?dbname=${serverCfg.postgres.database}"''
      else "export DB_PASSWORD=$(cat ${serverCfg.postgres.passwordFile})"
    )}

    ${optionalString serverCfg.typesense.enable ''
      export TYPESENSE_API_KEY=$(cat ${serverCfg.typesense.apiKeyFile})
    ''}

    exec ${program}
  '';

  commonServiceConfig = {
    Restart = "on-failure";

    # Hardening
    CapabilityBoundingSet = "";
    LockPersonality = true;
    MemoryDenyWriteExecute = true;
    NoNewPrivileges = true;
    PrivateUsers = true;
    PrivateTmp = true;
    PrivateDevices = true;
    PrivateMounts = true;
    ProtectClock = true;
    ProtectControlGroups = true;
    ProtectHome = true;
    ProtectHostname = true;
    ProtectKernelLogs = true;
    ProtectKernelModules = true;
    ProtectKernelTunables = true;
    ProtectProc = "invisible";
    ProcSubset = "pid";
    # Would re-mount paths ignored by temporary root
    # TODO ProtectSystem = "strict";
    RemoveIPC = true;
    RestrictAddressFamilies = [
      "AF_INET"
      "AF_INET6"
      "AF_UNIX"
    ];
    RestrictNamespaces = true;
    RestrictRealtime = true;
    RestrictSUIDSGID = true;
    SystemCallArchitectures = "native";
    SystemCallFilter = [
      "@system-service"
      "~@privileged"
      "@pkey"
    ];
    UMask = "0077";
  };

  serverServiceConfig = {
    DynamicUser = true;
    User = "immich";
    Group = "immich";
    SupplementaryGroups = optional enableRedis redisServerCfg.user;

    StateDirectory = "immich";
    StateDirectoryMode = "0750";
    WorkingDirectory = "/var/lib/immich";

    MemoryDenyWriteExecute = false; # nodejs requires this.
    EnvironmentFile = mkIf (serverCfg.environmentFile != null) serverCfg.environmentFile;

    TemporaryFileSystem = "/:ro";
    BindReadOnlyPaths =
      [
        "/nix/store"
        "-/etc/resolv.conf"
        "-/etc/nsswitch.conf"
        "-/etc/hosts"
        "-/etc/localtime"
        "-/run/postgresql"
      ]
      ++ optional enableRedis redisServerCfg.unixSocket;
  };
in {
  options.services.immich = {
    enable =
      mkEnableOption "immich"
      // {
        description = ''
          Enables immich which consists of a backend server, microservices,
          machine-learning and web ui. You can disable or reconfigure components
          individually using the subsections.
        '';
      };

    package = mkPackageOption pkgs "immich" {};

    server = {
      mediaDir = mkOption {
        type = types.str;
        default = "/var/lib/immich/media";
        description = "Directory used to store media files.";
      };

      backend = {
        enable =
          mkEnableOption "immich backend server"
          // {
            default = true;
          };
        port = mkOption {
          type = types.port;
          default = 3001;
          description = "Port to bind to.";
        };

        openFirewall = mkOption {
          default = false;
          type = types.bool;
          description = "Whether to open the firewall for the specified port.";
        };

        extraConfig = mkOption {
          type = types.attrs;
          default = {};
          example = {
            LOG_LEVEL = "debug";
          };
          description = ''
            Extra configuration options (environment variables).
            Refer to [the documented variables](https://documentation.immich.app/docs/install/environment-variables) tagged with 'server' for available options.
          '';
        };

        environmentFile = mkOption {
          type = types.nullOr types.path;
          default = null;
          description = ''
            Environment file as defined in systemd.exec(5). May be used to provide
            additional secret variables to the service without adding them to the
            world-readable Nix store.
          '';
        };
      };

      microservices = {
        enable =
          mkEnableOption "immich microservices"
          // {
            default = true;
          };

        port = mkOption {
          type = types.port;
          default = 3002;
          description = "Port to bind to.";
        };

        openFirewall = mkOption {
          default = false;
          type = types.bool;
          description = "Whether to open the firewall for the specified port.";
        };

        extraConfig = mkOption {
          type = types.attrs;
          default = {};
          example = {
            REVERSE_GEOCODING_PRECISION = 1;
          };
          description = ''
            Extra configuration options (environment variables).
            Refer to [the documented variables](https://documentation.immich.app/docs/install/environment-variables) tagged with 'microservices' for available options.
          '';
        };

        environmentFile = mkOption {
          type = types.nullOr types.path;
          default = null;
          description = ''
            Environment file as defined in systemd.exec(5). May be used to provide
            additional secret variables to the service without adding them to the
            world-readable Nix store.
          '';
        };
      };

      typesense = {
        enable =
          mkEnableOption "typesense"
          // {
            default = true;
          };

        host = mkOption {
          type = types.str;
          default = "127.0.0.1";
          example = "typesense.example.com";
          description = "Hostname/address of the typesense server to use.";
        };

        port = mkOption {
          type = types.port;
          default = 8108;
          description = "The port of the typesense server to use.";
        };

        protocol = mkOption {
          type = types.str;
          default = "http";
          description = "The protocol to use when connecting to the typesense server.";
        };

        apiKeyFile = mkOption {
          type = types.path;
          description = "Sets the api key for authentication with typesense.";
        };
      };

      postgres = {
        host = mkOption {
          type = types.str;
          default = "/run/postgresql";
          description = "Hostname/address of the postgres server to use. If an absolute path is given here, it will be interpreted as a unix socket path.";
        };

        port = mkOption {
          type = types.port;
          default = 5432;
          description = "The port of the postgres server to use.";
        };

        username = mkOption {
          type = types.str;
          default = "immich";
          description = "The postgres username to use.";
        };

        passwordFile = mkOption {
          type = types.nullOr types.path;
          default = null;
          description = ''
            Sets the password for authentication with postgres.
            May be unset when using socket authentication.
          '';
        };

        database = mkOption {
          type = types.str;
          default = "immich";
          description = "The postgres database to use.";
        };
      };

      useMachineLearning = mkOption {
        description = "Use the given machine learning server endpoint to enable ML functionality in immich.";
        default = true;
        type = types.bool;
      };

      machineLearningUrl = mkOption {
        type = types.str;
        default = "http://127.0.0.1:3003";
        example = "https://immich-ml.internal.example.com";
        description = "The machine learning server endpoint to use.";
      };

      extraConfig = mkOption {
        type = types.attrs;
        default = {};
        example = {
          REDIS_SOCKET = "/run/custom-redis";
        };
        description = ''
          Extra configuration options (environment variables) for both backend and microservices.
          Refer to [the documented variables](https://documentation.immich.app/docs/install/environment-variables) tagged with both 'server' and 'microservices' for available options.
        '';
      };

      environmentFile = mkOption {
        type = types.nullOr types.path;
        default = null;
        description = ''
          Environment file as defined in systemd.exec(5). May be used to provide
          additional secret variables to the backend and microservices servers without
          adding them to the world-readable Nix store.
        '';
      };
    };

    web = {
      enable =
        mkEnableOption "immich web frontend"
        // {
          default = true;
        };

      port = mkOption {
        type = types.port;
        default = 3000;
        description = "Port to bind to.";
      };

      openFirewall = mkOption {
        default = false;
        type = types.bool;
        description = "Whether to open the firewall for the specified port.";
      };

      serverUrl = mkOption {
        type = types.str;
        default = "http://127.0.0.1:3001";
        example = "https://immich-backend.internal.example.com";
        description = "The backend server url to use.";
      };

      apiUrlExternal = mkOption {
        type = types.str;
        default = "/web";
        description = "The api url to use for external requests.";
      };

      extraConfig = mkOption {
        type = types.attrs;
        default = {};
        example = {
          PUBLIC_LOGIN_PAGE_MESSAGE = "My awesome Immich instance!";
        };
        description = ''
          Extra configuration options (environment variables).
          Refer to [the documented variables](https://documentation.immich.app/docs/install/environment-variables) tagged with 'web' for available options.
        '';
      };
    };

    machineLearning = {
      enable =
        mkEnableOption "immich machine-learning server"
        // {
          default = true;
        };

      port = mkOption {
        type = types.port;
        default = 3003;
        description = "Port to bind to.";
      };

      openFirewall = mkOption {
        default = false;
        type = types.bool;
        description = "Whether to open the firewall for the specified port.";
      };

      extraConfig = mkOption {
        type = types.attrs;
        default = {};
        example = {
          MACHINE_LEARNING_MODEL_TTL = 600;
        };
        description = ''
          Extra configuration options (environment variables).
          Refer to [the documented variables](https://documentation.immich.app/docs/install/environment-variables) tagged with 'machine learning' for available options.
        '';
      };
    };
  };

  config = mkIf cfg.enable {
    assertions = [
      {
        assertion = !isServerPostgresUnix -> serverCfg.postgres.passwordFile != null;
        message = "A database password must be provided when unix sockets are not used.";
      }
    ];

    networking.firewall.allowedTCPPorts = mkMerge [
      (mkIf (backendCfg.enable && backendCfg.openFirewall) [backendCfg.port])
      (mkIf (microservicesCfg.enable && microservicesCfg.openFirewall) [microservicesCfg.port])
      (mkIf (webCfg.enable && webCfg.openFirewall) [webCfg.port])
      (mkIf (mlCfg.enable && mlCfg.openFirewall) [mlCfg.port])
    ];

    services.redis.servers.immich.enable = mkIf enableRedis true;
    services.redis.vmOverCommit = mkIf enableRedis (mkDefault true);

    systemd.services.immich-server = mkIf backendCfg.enable {
      description = "Immich backend server (Self-hosted photo and video backup solution)";
      after =
        [
          "network.target"
          "typesense.service"
          "postgresql.service"
          "immich-machine-learning.service"
        ]
        ++ optional enableRedis "redis-immich.service";
      wantedBy = ["multi-user.target"];

      environment =
        serverEnv
        // {
          SERVER_PORT = toString backendCfg.port;
        }
        // mapAttrs (_: toString) serverCfg.extraConfig
        // mapAttrs (_: toString) backendCfg.extraConfig;

      script = serverStartWrapper "${cfg.package}/bin/server";
      serviceConfig = mkMerge [
        (commonServiceConfig // serverServiceConfig)
        {
          EnvironmentFile = mkIf (backendCfg.environmentFile != null) backendCfg.environmentFile;
        }
      ];
    };

    systemd.services.immich-microservices = mkIf microservicesCfg.enable {
      description = "Immich microservices (Self-hosted photo and video backup solution)";
      after =
        [
          "network.target"
          "typesense.service"
          "postgresql.service"
          "immich-machine-learning.service"
        ]
        ++ optional enableRedis "redis-immich.service";
      wantedBy = ["multi-user.target"];

      environment =
        serverEnv
        // {
          MICROSERVICES_PORT = toString microservicesCfg.port;
        }
        // mapAttrs (_: toString) serverCfg.extraConfig
        // mapAttrs (_: toString) microservicesCfg.extraConfig;

      script = serverStartWrapper "${cfg.package}/bin/microservices";
      serviceConfig = mkMerge [
        (commonServiceConfig // serverServiceConfig)
        {
          EnvironmentFile = mkIf (microservicesCfg.environmentFile != null) microservicesCfg.environmentFile;
        }
      ];
    };

    systemd.services.immich-web = mkIf webCfg.enable {
      description = "Immich web (Self-hosted photo and video backup solution)";
      after = [
        "network.target"
        "immich-server.service"
      ];
      wantedBy = ["multi-user.target"];

      environment =
        {
          NODE_ENV = "production";
          PORT = toString webCfg.port;
          IMMICH_SERVER_URL = webCfg.serverUrl;
          IMMICH_API_URL_EXTERNAL = webCfg.apiUrlExternal;
        }
        // mapAttrs (_: toString) webCfg.extraConfig;

      script = ''
        set -euo pipefail
        export PUBLIC_IMMICH_SERVER_URL=$IMMICH_SERVER_URL
        export PUBLIC_IMMICH_API_URL_EXTERNAL=$IMMICH_API_URL_EXTERNAL
        exec ${cfg.package.web}/bin/web
      '';
      serviceConfig =
        commonServiceConfig
        // {
          DynamicUser = true;
          User = "immich-web";
          Group = "immich-web";

          MemoryDenyWriteExecute = false; # nodejs requires this.

          TemporaryFileSystem = "/:ro";
          BindReadOnlyPaths = [
            "/nix/store"
            "-/etc/resolv.conf"
            "-/etc/nsswitch.conf"
            "-/etc/hosts"
            "-/etc/localtime"
          ];
        };
    };

    systemd.services.immich-machine-learning = mkIf mlCfg.enable {
      description = "Immich machine learning (Self-hosted photo and video backup solution)";
      after = ["network.target"];
      wantedBy = ["multi-user.target"];

      environment =
        {
          NODE_ENV = "production";
          MACHINE_LEARNING_PORT = toString mlCfg.port;

          MACHINE_LEARNING_CACHE_FOLDER = "/var/cache/immich-ml";
          TRANSFORMERS_CACHE = "/var/cache/immich-ml";
        }
        // mapAttrs (_: toString) mlCfg.extraConfig;

      serviceConfig =
        commonServiceConfig
        // {
          ExecStart = "${cfg.package.machine-learning}/bin/machine-learning";
          DynamicUser = true;
          User = "immich-ml";
          Group = "immich-ml";

          MemoryDenyWriteExecute = false; # onnxruntime_pybind11 requires this.
          ProcSubset = "all"; # Needs /proc/cpuinfo

          CacheDirectory = "immich-ml";
          CacheDirectoryMode = "0700";

          # TODO gpu access

          TemporaryFileSystem = "/:ro";
          BindReadOnlyPaths = [
            "/nix/store"
            "-/etc/resolv.conf"
            "-/etc/nsswitch.conf"
            "-/etc/hosts"
            "-/etc/localtime"
          ];
        };
    };

    meta.maintainers = with maintainers; [oddlama];
  };
}
