{
  pkgs,
  config,
  lib,
  ...
}: let
  cfg = config.services.tangled.appview;
in
  with lib; {
    options = {
      services.tangled.appview = {
        enable = mkOption {
          type = types.bool;
          default = false;
          description = "Enable tangled appview";
        };

        package = mkOption {
          type = types.package;
          description = "Package to use for the appview";
        };

        # core configuration
        port = mkOption {
          type = types.port;
          default = 3000;
          description = "Port to run the appview on";
        };

        listenAddr = mkOption {
          type = types.str;
          default = "0.0.0.0:${toString cfg.port}";
          description = "Listen address for the appview service";
        };

        dbPath = mkOption {
          type = types.str;
          default = "/var/lib/appview/appview.db";
          description = "Path to the SQLite database file";
        };

        appviewHost = mkOption {
          type = types.str;
          default = "tangled.org";
          example = "example.com";
          description = "Public host URL for the appview instance";
        };

        appviewName = mkOption {
          type = types.str;
          default = "Tangled";
          description = "Display name for the appview instance";
        };

        dev = mkOption {
          type = types.bool;
          default = false;
          description = "Enable development mode";
        };

        disallowedNicknamesFile = mkOption {
          type = types.nullOr types.path;
          default = null;
          description = "Path to file containing disallowed nicknames";
        };

        # redis configuration
        redis = {
          addr = mkOption {
            type = types.str;
            default = "localhost:6379";
            description = "Redis server address";
          };

          db = mkOption {
            type = types.int;
            default = 0;
            description = "Redis database number";
          };
        };

        # jetstream configuration
        jetstream = {
          endpoint = mkOption {
            type = types.str;
            default = "wss://jetstream1.us-east.bsky.network/subscribe";
            description = "Jetstream WebSocket endpoint";
          };
        };

        # knotstream consumer configuration
        knotstream = {
          retryInterval = mkOption {
            type = types.str;
            default = "60s";
            description = "Initial retry interval for knotstream consumer";
          };

          maxRetryInterval = mkOption {
            type = types.str;
            default = "120m";
            description = "Maximum retry interval for knotstream consumer";
          };

          connectionTimeout = mkOption {
            type = types.str;
            default = "5s";
            description = "Connection timeout for knotstream consumer";
          };

          workerCount = mkOption {
            type = types.int;
            default = 64;
            description = "Number of workers for knotstream consumer";
          };

          queueSize = mkOption {
            type = types.int;
            default = 100;
            description = "Queue size for knotstream consumer";
          };
        };

        # spindlestream consumer configuration
        spindlestream = {
          retryInterval = mkOption {
            type = types.str;
            default = "60s";
            description = "Initial retry interval for spindlestream consumer";
          };

          maxRetryInterval = mkOption {
            type = types.str;
            default = "120m";
            description = "Maximum retry interval for spindlestream consumer";
          };

          connectionTimeout = mkOption {
            type = types.str;
            default = "5s";
            description = "Connection timeout for spindlestream consumer";
          };

          workerCount = mkOption {
            type = types.int;
            default = 64;
            description = "Number of workers for spindlestream consumer";
          };

          queueSize = mkOption {
            type = types.int;
            default = 100;
            description = "Queue size for spindlestream consumer";
          };
        };

        # resend configuration
        resend = {
          sentFrom = mkOption {
            type = types.str;
            default = "noreply@notifs.tangled.sh";
            description = "Email address to send notifications from";
          };
        };

        # posthog configuration
        posthog = {
          endpoint = mkOption {
            type = types.str;
            default = "https://eu.i.posthog.com";
            description = "PostHog API endpoint";
          };
        };

        # camo configuration
        camo = {
          host = mkOption {
            type = types.str;
            default = "https://camo.tangled.sh";
            description = "Camo proxy host URL";
          };
        };

        # avatar configuration
        avatar = {
          host = mkOption {
            type = types.str;
            default = "https://avatar.tangled.sh";
            description = "Avatar service host URL";
          };
        };

        plc = {
          url = mkOption {
            type = types.str;
            default = "https://plc.directory";
            description = "PLC directory URL";
          };
        };

        pds = {
          host = mkOption {
            type = types.str;
            default = "https://tngl.sh";
            description = "PDS host URL";
          };
        };

        label = {
          defaults = mkOption {
            type = types.listOf types.str;
            default = [
              "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/wontfix"
              "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue"
              "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/duplicate"
              "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/documentation"
              "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/assignee"
            ];
            description = "Default label definitions";
          };

          goodFirstIssue = mkOption {
            type = types.str;
            default = "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue";
            description = "Good first issue label definition";
          };
        };

        environmentFile = mkOption {
          type = with types; nullOr path;
          default = null;
          example = "/etc/appview.env";
          description = ''
            Additional environment file as defined in {manpage}`systemd.exec(5)`.

            Sensitive secrets such as {env}`TANGLED_COOKIE_SECRET`,
            {env}`TANGLED_OAUTH_CLIENT_SECRET`, {env}`TANGLED_RESEND_API_KEY`,
            {env}`TANGLED_CAMO_SHARED_SECRET`, {env}`TANGLED_AVATAR_SHARED_SECRET`,
            {env}`TANGLED_REDIS_PASS`, {env}`TANGLED_PDS_ADMIN_SECRET`,
            {env}`TANGLED_CLOUDFLARE_API_TOKEN`, {env}`TANGLED_CLOUDFLARE_ZONE_ID`,
            {env}`TANGLED_CLOUDFLARE_TURNSTILE_SITE_KEY`,
            {env}`TANGLED_CLOUDFLARE_TURNSTILE_SECRET_KEY`,
            {env}`TANGLED_POSTHOG_API_KEY`, {env}`TANGLED_APP_PASSWORD`,
            and {env}`TANGLED_ALT_APP_PASSWORD` may be passed to the service
            without making them world readable in the nix store.
          '';
        };
      };
    };

    config = mkIf cfg.enable {
      services.redis.servers.appview = {
        enable = true;
        port = 6379;
      };

      systemd.services.appview = {
        description = "tangled appview service";
        wantedBy = ["multi-user.target"];
        after = ["redis-appview.service" "network-online.target"];
        requires = ["redis-appview.service"];
        wants = ["network-online.target"];

        path = [pkgs.diffutils];

        serviceConfig = {
          Type = "simple";
          ExecStart = "${cfg.package}/bin/appview";
          Restart = "always";
          RestartSec = "10s";
          EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;

          # state directory
          StateDirectory = "appview";
          WorkingDirectory = "/var/lib/appview";

          # security hardening
          NoNewPrivileges = true;
          PrivateTmp = true;
          ProtectSystem = "strict";
          ProtectHome = true;
          ReadWritePaths = ["/var/lib/appview"];
        };

        environment =
          {
            TANGLED_DB_PATH = cfg.dbPath;
            TANGLED_LISTEN_ADDR = cfg.listenAddr;
            TANGLED_APPVIEW_HOST = cfg.appviewHost;
            TANGLED_APPVIEW_NAME = cfg.appviewName;
            TANGLED_DEV =
              if cfg.dev
              then "true"
              else "false";
          }
          // optionalAttrs (cfg.disallowedNicknamesFile != null) {
            TANGLED_DISALLOWED_NICKNAMES_FILE = cfg.disallowedNicknamesFile;
          }
          // {
            TANGLED_REDIS_ADDR = cfg.redis.addr;
            TANGLED_REDIS_DB = toString cfg.redis.db;

            TANGLED_JETSTREAM_ENDPOINT = cfg.jetstream.endpoint;

            TANGLED_KNOTSTREAM_RETRY_INTERVAL = cfg.knotstream.retryInterval;
            TANGLED_KNOTSTREAM_MAX_RETRY_INTERVAL = cfg.knotstream.maxRetryInterval;
            TANGLED_KNOTSTREAM_CONNECTION_TIMEOUT = cfg.knotstream.connectionTimeout;
            TANGLED_KNOTSTREAM_WORKER_COUNT = toString cfg.knotstream.workerCount;
            TANGLED_KNOTSTREAM_QUEUE_SIZE = toString cfg.knotstream.queueSize;

            TANGLED_SPINDLESTREAM_RETRY_INTERVAL = cfg.spindlestream.retryInterval;
            TANGLED_SPINDLESTREAM_MAX_RETRY_INTERVAL = cfg.spindlestream.maxRetryInterval;
            TANGLED_SPINDLESTREAM_CONNECTION_TIMEOUT = cfg.spindlestream.connectionTimeout;
            TANGLED_SPINDLESTREAM_WORKER_COUNT = toString cfg.spindlestream.workerCount;
            TANGLED_SPINDLESTREAM_QUEUE_SIZE = toString cfg.spindlestream.queueSize;

            TANGLED_RESEND_SENT_FROM = cfg.resend.sentFrom;

            TANGLED_POSTHOG_ENDPOINT = cfg.posthog.endpoint;

            TANGLED_CAMO_HOST = cfg.camo.host;

            TANGLED_AVATAR_HOST = cfg.avatar.host;

            TANGLED_PLC_URL = cfg.plc.url;

            TANGLED_PDS_HOST = cfg.pds.host;

            TANGLED_LABEL_DEFAULTS = concatStringsSep "," cfg.label.defaults;
            TANGLED_LABEL_GFI = cfg.label.goodFirstIssue;
          };
      };
    };
  }
