# Edit this configuration file to define what should be installed on
# your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running `nixos-help`).

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

with lib;
let
  netif = "enp1s0";
  hostname = "linde";
  net-ip4 = "116.203.248.56";
  net-mask4 = "32";
  net-gw = "172.31.1.1";
  net-ip6 = "2a01:4f8:c012:23a4::1";
  net-rdnsip = "2a01:4f8:c012:23a4::53";
  net-redisip = "2a01:4f8:c012:23a4::6379";
  net-mask6 = "64";
  net-gw6 = "fe80::1";
  domain = "alanpearce.eu";
  ts-domain = "hydra-pinecone.ts.net";
in
{
  imports =
    [
      # Include the results of the hardware scan.
      ./linde-hardware.nix

      ./settings/services/git-server.nix
    ];
  age.secrets = {
    paperless =
      let
        cfg = config.services.paperless;
      in
      {
        file = ../secrets/paperless.age;
        path = "${cfg.dataDir}/nixos-paperless-secret-key";
        owner = cfg.user;
        mode = "400";
        symlink = false;
      };
    acme.file = ../secrets/acme.age;
    binarycache.file = ../secrets/binarycache.age;
    dex.file = ../secrets/dex.age;
    powerdns.file = ../secrets/powerdns.age;
    redis-website.file = ../secrets/redis-website.age;
    golink = let golink = config.services.golink; in {
      # hope this doesn't collide...
      path = "${golink.dataDir}/.config/tsnet-golink/auth.key";
      owner = golink.user;
      mode = "400";
      symlink = false;
      file = ../secrets/golink.age;
    };
  };

  # Use the systemd-boot EFI boot loader.
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.loader.efi.efiSysMountPoint = "/boot/efi";

  time.timeZone = "Europe/Berlin";

  i18n.defaultLocale = "en_GB.UTF-8";

  environment.enableAllTerminfo = true;
  environment.homeBinInPath = true;
  environment.localBinInPath = true;
  environment.systemPackages = with pkgs; [
    htop
    lsof
    powerdns
    sqlite-interactive
    knot-dns

    nixpkgs-review
    nix-output-monitor
  ];

  # Initial empty root password for easy login:
  users.users.root.initialHashedPassword = "";
  services.openssh = {
    enable = true;
    settings = {
      PermitRootLogin = "no";
      PasswordAuthentication = false;
      KbdInteractiveAuthentication = false;
    };
  };
  services.sshguard = {
    enable = true;
    services = [ "sshd" ];
  };
  programs.mosh.enable = true;

  system.autoUpgrade = {
    enable = true;
    dates = "02:10";
    randomizedDelaySec = "59 min";
    allowReboot = true;
    flake = "git+file://${config.services.gitolite.dataDir}/repositories/nixfiles.git";
    flags = [
      "--no-write-lock-file"
      "--impure"
      "--update-input"
      "nixpkgs-small"
      "--update-input"
      "searchix"
    ];
  };

  nix = {
    daemonCPUSchedPolicy = "batch";
    daemonIOSchedPriority = 6;
    settings = {
      max-jobs = 2;
      auto-optimise-store = true;
      trusted-users = [ "root" "nixremote" ];
      experimental-features = [ "nix-command" "flakes" ];
      substituters = [
        "https://nix-community.cachix.org"
      ];

      trusted-public-keys = [
        "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
      ];
    };
    gc = {
      automatic = true;
      dates = "08:15";
      options = "--delete-older-than 14d";
    };
    optimise = {
      automatic = true;
      dates = [ "02:30" ];
    };
  };

  services.nix-serve = {
    enable = true;
    package = pkgs.nix-serve-ng;
    secretKeyFile = config.age.secrets.binarycache.path;
  };

  programs.neovim = {
    enable = true;
    defaultEditor = true;
    viAlias = true;
    vimAlias = true;
  };

  networking = {
    hostName = hostname;
    inherit domain;
    useDHCP = false;
    dhcpcd.enable = false;
    nameservers = [
      "2606:4700:4700::1111"
      "2606:4700:4700::1001"
      "1.1.1.1"
      "1.0.0.1"
    ];
    hosts = lib.mkForce {
      ${net-ip4} = [ "${hostname}.alanpearce.eu" hostname ];
      ${net-ip6} = [ "${hostname}.alanpearce.eu" hostname ];
      ${net-rdnsip} = [ "dns" ];
      ${net-redisip} = [ "redis" ];
    };
    firewall = {
      enable = true;
      allowPing = true;
      pingLimit = "--limit 60/minute --limit-burst 30";
      logRefusedConnections = false;
      allowedTCPPorts = [
        22
        80
        443
        53
        853
        6379
        9418
        6922
      ];
      allowedUDPPorts = [
        53
        443 # HTTP/3 (QUIC)
        3478
        6885 # DHT
        6922
      ];
      trustedInterfaces = [ "tailscale0" ];
    };
    resolvconf = {
      enable = false;
      useLocalResolver = false;
    };
  };
  services.resolved = {
    enable = true;
    llmnr = "false";
    dnssec = "true";
  };
  systemd.network = {
    enable = true;
    networks.${netif} =
      {
        name = netif;
        routes = [
          {
            Gateway = net-gw6;
            PreferredSource = net-ip6;
            QuickAck = true;
            InitialCongestionWindow = 30;
            InitialAdvertisedReceiveWindow = 30;
          }
          {
            Gateway = net-gw;
            QuickAck = true;
            InitialCongestionWindow = 30;
            InitialAdvertisedReceiveWindow = 30;
          }
        ];
        address = [
          "${net-ip6}/${net-mask6}"
          "${net-rdnsip}/${net-mask6}"
          "${net-redisip}/${net-mask6}"
        ];
        addresses = [{
          Address = "${net-ip4}/${net-mask4}";
          Peer = "${net-gw}/32";
        }];
      };
    wait-online = {
      extraArgs = [ "--interface=${netif}" ];
    };
  };

  services.tailscale = {
    enable = true;
    extraUpFlags = [ "--accept-routes" ];
    useRoutingFeatures = "client";
  };
  services.golink = {
    enable = true;
    tailscaleAuthKeyFile = config.age.secrets.golink.path;
  };

  services.journald.extraConfig = ''
    MaxRetentionSec=1 month
  '';

  zramSwap = {
    enable = true;
    algorithm = "zstd";
  };

  boot.kernel.sysctl =
    let
      buffer_size = 16 * 1024 * 1024;
      server_count = 2;
      max_clients = 100;
      page_size = 4096;
      # This server might have 100 clients simultaneously, so:
      #   max(tcp_wmem) * 2 * 100 / 4096
      mem = toString (buffer_size * server_count * max_clients / page_size);
    in
    {
      "net.ipv4.tcp_allowed_congestion_control" = "bbr illinois reno";
      "net.ipv4.tcp_congestion_control" = "bbr";
      "net.core.default_qdisc" = "fq";

      # Provide adequate buffer memory.
      # rmem_max and wmem_max are TCP max buffer size
      # settable with setsockopt(), in bytes
      # tcp_rmem and tcp_wmem are per socket in bytes.
      # tcp_mem is for all TCP streams, in 4096-byte pages.
      # The following are suggested on IBM's
      # High Performance Computing page
      "net.core.rmem_max" = buffer_size;
      "net.core.wmem_max" = buffer_size;
      "net.core.rmem_default" = buffer_size;
      "net.core.wmem_default" = buffer_size;
      "net.ipv4.tcp_rmem" = "4096 87380 ${toString buffer_size}";
      "net.ipv4.tcp_wmem" = "4096 87380 ${toString buffer_size}";
      "net.ipv4.tcp_mem" = "${mem} ${mem} ${mem}";

      "net.ipv4.tcp_sack" = false;
      "net.ipv4.tcp_dsack" = false;

      "net.ipv4.tcp_slow_start_after_idle" = false;
    };

  security.sudo.execWheelOnly = true;
  security.sudo.extraConfig = ''
    Defaults:root,%wheel env_keep+=EDITOR
  '';

  nixpkgs = {
    config.allowUnfree = true;
  };

  programs.fish = {
    enable = true;
    interactiveShellInit = ''
      set --universal fish_greeting ""
    '';
  };
  programs.zsh.enable = true;
  users.users.root.shell = "${pkgs.fish}/bin/fish";
  users.users.alan = {
    shell = "${pkgs.fish}/bin/fish";
    extraGroups = [ "wheel" "caddy" "docker" "laminar" ];
    isNormalUser = true;
    home = "/home/alan";
    createHome = true;

    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII8VIII+598QOBxi/52O1Kb19RdUdX0aZmS1/dNoyqc5 alan@hetzner.strongbox"
      "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEsD23eNmxBQj+FCxXXvSAX9U+9HPLKei95vdHPGWbyA5TVlD0N8ROxcfRwDepaQfCqOxEchh7liLAivI1PRCd0= secretive@macbook-pro"
    ];
  };

  users.users.nixremote = {
    shell = "/bin/sh";
    isNormalUser = true;
    home = "/var/lib/nixremote/";
    createHome = true;
    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBxa7lxDu0M4chats/VvpFzjT3ruexKa3J9UC6ASo3bN root@NanoPi.lan"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBmDSZnUzIPQowLrKSa24eSb1WFQe7yPjTcDPPe3UY0Q nix@mba"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE9of82WBHK8nr8L9RGeieLMfcAWaFCeCkmvYHM9LCuT nanopi"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIy9jFioBvV0JA0lc+De2N+vDOABGHgCECW6vkD33CE4 sourcehut"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII7sWEwsm8JZiJ0LUnjSt0Kg1RXypG6p5AzP/R2n5ca actions@github.com"
      "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEsD23eNmxBQj+FCxXXvSAX9U+9HPLKei95vdHPGWbyA5TVlD0N8ROxcfRwDepaQfCqOxEchh7liLAivI1PRCd0= secretive@macbook-pro"
    ];
  };

  # This value determines the NixOS release from which the default
  # settings for stateful data, like file locations and database versions
  # on your system were taken. It's perfectly fine and recommended to leave
  # this value at the release version of the first install of this system.
  # Before changing this value read the documentation for this option
  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
  system.stateVersion = "23.05"; # Did you read the comment?

  services.goatcounter = {
    enable = true;
    address = "localhost";
    port = 8082;
    proxy = true;
    extraArgs = [
      "-db"
      "sqlite3+db/goatcounter.sqlite3"
      "-websocket"
      "-automigrate"
      "-smtp"
      "smtp://localhost:25"
    ];
  };

  services.powerdns =
    let
      inherit (lib.lists) flatten;
      inherit (lib.strings) concatStringsSep;
      he = {
        notify = "216.218.130.2";
        axfr = [
          "216.218.133.2"
          "2001:470:600::2"
        ];
      };
      iplist = ips: concatStringsSep "," (flatten ips);
    in
    {
      enable = true;
      secretFile = config.age.secrets.powerdns.path;
      extraConfig = ''
        launch=gsqlite3
        dnsupdate=yes
        allow-dnsupdate-from=0.0.0.0/0,::/0
        only-notify=
        also-notify=${iplist [ he.notify ]}
        allow-axfr-ips=${iplist [ he.axfr ]}
        outgoing-axfr-expand-alias=yes
        expand-alias=yes
        resolver=1.1.1.1
        local-address=${net-ip4} ${net-ip6}
        reuseport=yes
        log-dns-details=no
        log-dns-queries=no
        loglevel=5
        primary=yes
        secondary=yes
        send-signed-notify=no
        prevent-self-notification=no

        default-soa-edit=inception-increment

        api=yes
        # replaced by secretFile/envsubst
        api-key=$API_KEY

        gsqlite3-database=/var/db/pdns/zones.db
        gsqlite3-pragma-foreign-keys=yes
        gsqlite3-dnssec=yes
      '';
    };

  systemd.services.hagezi-blocklist-update = {
    enable = true;
    startAt = "daily";
    serviceConfig = {
      CacheDirectory = "blocklist";
      UMask = "0077";
      DynamicUser = "yes";
      ProtectSystem = "strict";
      ProtectHome = true;
      PrivateTmp = true;
      PrivateDevices = true;
      PrivateUsers = true;
      ProtectClock = true;
      ProtectKernelTunables = true;
      ProtectKernelModules = true;
      ProtectKernelLogs = true;
      ProtectControlGroups = true;
      ProtectProc = "invisible";
      RestrictAddressFamilies = "AF_INET AF_INET6";
      RestrictNamespaces = true;
      RestrictRealtime = true;
      LockPersonality = true;
      MemoryDenyWriteExecute = "true";
      SystemCallFilter = [
        "~@clock"
        "~@cpu-emulation"
        "~@debug"
        "~@module"
        "~@mount"
        "~@obsolete"
        "~@privileged"
        "~@raw-io"
        "~@reboot"
        "~@resources"
        "~@swap"
      ];
      SystemCallArchitectures = "native";
      CapabilityBoundingSet = "";
      DevicePolicy = "closed";
      ProcSubset = "pid";
      NoNewPrivileges = true;
      ExecStart = "${pkgs.curl}/bin/curl --no-progress-meter --output %C/blocklist/hagezi.rpz https://raw.githubusercontent.com/hagezi/dns-blocklists/main/rpz/pro.plus.txt";
      #  https://raw.githubusercontent.com/hagezi/dns-blocklists/main/rpz/pro.plus.txt"
      ExecStartPost = [
        "+/bin/sh -c 'exec install --compare --mode=644 %C/blocklist/hagezi.rpz /etc/knot-resolver/blocklist.rpz'"
        "-/bin/sh -c 'exec rm -f %C/blocklist/hagezi.rpz'"
      ];
      Environment = [
        "HOME=%C/blocklist"
      ];
    };
  };

  services.postfix =
    let
      localUser = "alan";
      forwardingAddress = "alan@alanpearce.eu";
    in
    {
      enable = true;
      destination = [ ];
      domain = config.networking.domain;
      virtual = ''
        @${config.networking.hostName}.${config.networking.domain} ${localUser}
        ${localUser} ${forwardingAddress}
      '';
      config = {
        inet_interfaces = "loopback-only";
      };
    };

  services.kresd = {
    enable = true;
    # package = pkgs.knot-resolver.override { extraFeatures = true; };
    listenPlain = [
      "[${net-rdnsip}]:53"
    ];
    listenTLS = [
      "127.0.0.1:853"
      "[::1]:853"
      "${net-ip4}:853"
      "[${net-ip6}]:853"
    ];
    listenDoH = [
      "[::1]:443"
      "127.0.0.1:443"
    ];
    instances = 2;
    extraConfig = ''
      modules = {
        'rebinding < iterate',
        'hints > iterate',
        'serve_stale < cache',
        'stats',
        predict = {
          window = 30,
          period = 24 * (60/30),
        },
        'nsid',
      }

      local systemd_instance = os.getenv("SYSTEMD_INSTANCE")
      nsid.name(systemd_instance)

      log_groups({ 'policy' })

      cache.size = 500 * MB

      net.tls(
        '/var/lib/acme/dns.alanpearce.eu/cert.pem',
        '/var/lib/acme/dns.alanpearce.eu/key.pem'
      )

      -- override blocklist
      policy.add(policy.suffix(policy.PASS, policy.todnames({
      })))

      policy.add(policy.rpz(
        policy.DENY_MSG('domain blocked by hagezi'),
        '/etc/knot-resolver/blocklist.rpz',
        false -- needs wrapped kresd
        -- true -- will watch the file for updates
      ))

      policy.add(policy.domains(policy.REFUSE, policy.todnames({
        'use-application-dns.net',
        'telemetry.astro.build',
      })))

      -- disable DNSSEC when using Quad9 since they do it
      -- trust_anchors.remove('.')
      -- policy.add(policy.all(policy.TLS_FORWARD({
      --   {'2620:fe::fe', hostname='dns.quad9.net'},
      --   {'2620:fe::9', hostname='dns.quad9.net'},
      --   {'9.9.9.9', hostname='dns.quad9.net'},
      --   {'149.112.122.122', hostname='dns.quad9.net'},
      -- })))
    '';
  };

  users.groups.ntfy = { };
  users.users.ntfy = {
    isSystemUser = true;
    group = "ntfy";
  };
  services.ntfy-sh = {
    enable = true;
    user = "ntfy";
    group = "ntfy";
    settings = {
      base-url = "https://ntfy.alanpearce.eu";
      listen-http = ":2586";
      behind-proxy = true;
      manager-interval = "1h";
      cache-startup-queries = ''
        PRAGMA journal_mode = WAL;
        PRAGMA busy_timeout = 5000;
        PRAGMA synchronous = NORMAL;
      '';
      cache-file = "/var/cache/ntfy/cache.db";
      attachment-cache-dir = "/var/cache/ntfy/attachments";
      auth-default-access = "deny-all";
      auth-file = "/var/lib/ntfy/user.db";
      upstream-base-url = "https://ntfy.sh";
    };
  };
  systemd.services.ntfy-sh = {
    serviceConfig = {
      DynamicUser = lib.mkForce false;
      StateDirectory = lib.mkForce "ntfy";
      RuntimeDirectory = "ntfy";
      CacheDirectory = "ntfy";
    };
  };

  systemd.services.backup-gitolite = {
    startAt = "daily";
    path = with pkgs; [
      rdiff-backup
      openssh
    ];
    script = ''
      rdiff-backup --api-version 201 backup ${config.services.gitolite.dataDir} ${hostname}@home.alanpearce.eu::gitolite
      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@home.alanpearce.eu::gitolite
    '';
    serviceConfig.Type = "oneshot";
  };

  systemd.services.backup-paperless = {
    startAt = "daily";
    path = with pkgs; [
      sudo
      rdiff-backup
      openssh
    ];
    script = ''
      sudo -u paperless ./paperless-manage document_exporter --delete --use-filename-format --no-archive --no-thumbnail --no-progress-bar ./export
      rdiff-backup --api-version 201 backup ./export ${hostname}@home.alanpearce.eu::paperless
      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@home.alanpearce.eu::paperless
    '';
    serviceConfig = {
      Type = "oneshot";
      WorkingDirectory = config.services.paperless.dataDir;
    };
  };

  security.acme = {
    defaults = {
      email = "alan@alanpearce.eu";
      dnsProvider = "pdns";
      dnsResolver = "1.1.1.1:53";
      credentialsFile = config.age.secrets.acme.path;
      reloadServices = [ "caddy" ];
      validMinDays = 32;
    };
    acceptTerms = true;
    certs."alanpearce.eu" = {
      extraDomainNames = [ "*.alanpearce.eu" "*.linde.alanpearce.eu" ];
    };
    certs."dns.alanpearce.eu" = {
      reloadServices = map (x: "kresd@${toString x}") (range 1 config.services.kresd.instances);
      group = "knot-resolver";
    };
    certs."stats.alanpearce.eu" = {
      extraDomainNames = [ "*.stats.alanpearce.eu" ];
    };
    certs."redis.alanpearce.eu" = {
      group = "redis-website";
    };
  };
  users.groups.acme.members = [
    "caddy"
  ];

  services.caddy = {
    enable = true;
    group = "caddy";
    globalConfig = ''
      auto_https disable_certs
      default_bind ${net-ip6} ${net-ip4}
    '';
    virtualHosts =
      let
        inherit (import ../lib/caddy.nix { inherit lib; }) security-headers;
      in
      {
        "http://" = {
          # Needed for HTTP->HTTPS servers
        };
        "alanpearce.eu" = {
          serverAliases = [ "www.alanpearce.eu" "test.alanpearce.eu" ];
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            root * /srv/http/website/public
            file_server
            ${security-headers {}}
            handle_errors {
              rewrite * /404.html
              file_server
            }
          '';
        };
        "${hostname}.alanpearce.eu" = {
          serverAliases = [ "https://" ];
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            respond * 204
            ${security-headers {}}
          '';
        };
        "pdns.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            log {
              output discard
            }
            reverse_proxy 127.0.0.1:8081
          '';
        };
        "id.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            ${security-headers {}}
            reverse_proxy http://${config.services.dex.settings.web.http}
          '';
        };
        "dns.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            log {
              output discard
            }
            encode zstd gzip
            reverse_proxy localhost:443 {
              transport http {
                tls_server_name dns.alanpearce.eu
              }
            }
          '';
        };
        "files.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            ${security-headers {}}
            root * /srv/http/files
            file_server browse
          '';
        };
        "ntfy.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            ${security-headers {}}
            reverse_proxy localhost${config.services.ntfy-sh.settings.listen-http} {
              health_uri /v1/health
              health_body `"healthy":true`
            }
          '';
        };
        "searchix.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          serverAliases = [ "searchix.linde.alanpearce.eu" ];
          extraConfig = ''
            reverse_proxy localhost:${toString config.services.searchix.settings.web.port} {
              health_uri /health
              health_status 2xx
            }
            encode zstd gzip {
              match {
                header Content-Type text/*
                header Content-Type application/json*
                header Content-Type application/javascript*
                header Content-Type application/opensearchdescription+xml
                header Content-Type application/atom+xml*
                header Content-Type application/rss+xml*
                header Content-Type image/svg+xml*
              }
            }
          '';
        };
        "binarycache.alanpearce.eu" =
          let
            ns = config.services.nix-serve;
          in
          {
            useACMEHost = "alanpearce.eu";
            extraConfig = ''
              reverse_proxy ${ns.bindAddress}:${toString ns.port}
            '';
          };
        "ci.alanpearce.eu" =
          let
            srv = config.services.laminar;
          in
          {
            useACMEHost = "alanpearce.eu";
            extraConfig = ''
              reverse_proxy ${srv.settings.bindHTTP}
            '';
          };
        "stats.alanpearce.eu" =
          let
            srv = config.services.goatcounter;
          in
          {
            useACMEHost = "stats.alanpearce.eu";
            serverAliases = [ "*.stats.alanpearce.eu" ];
            extraConfig = ''
              reverse_proxy ${srv.address}:${toString srv.port}
            '';
          };
        "go.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            ${security-headers {}}
            root * /srv/http/go
            file_server
          '';
        };
      };
  };
  systemd.services.caddy.serviceConfig = {
    UMask = "007";
  };

  networking.nat = {
    enable = true;
    internalInterfaces = [ "ve-+" ];
    externalInterface = netif;
    enableIPv6 = true;
  };

  users.users.paperless = {
    group = "paperless";
    uid = config.ids.uids.paperless;
    home = "/srv/paperless";
  };
  users.groups.paperless.members = [ "alan" "syncthing" ];
  containers.papers =
    let
      hostDataDir = config.users.users.paperless.home;
      localAddress6 = "fc00::2";
      tsHostname = "papers.${ts-domain}";
      tsPort = 41642;
    in
    {
      # or maybe socket activated?
      autoStart = true;
      # does TS need this?
      enableTun = true;
      privateNetwork = true;
      hostAddress6 = "fc00::1";
      inherit localAddress6;
      forwardPorts = [{
        hostPort = tsPort;
      }];
      bindMounts = {
        ${config.services.paperless.dataDir} = {
          hostPath = hostDataDir;
          isReadOnly = false;
        };
      };
      config = {
        environment.systemPackages = with pkgs; [
          lsof
        ];
        networking = {
          useHostResolvConf = false;
          resolvconf.enable = false;
          firewall.trustedInterfaces = [ "tailscale0" ];
          firewall.rejectPackets = true;
          nameservers = config.networking.nameservers;
        };
        services.resolved = {
          enable = true;
          llmnr = "false";
        };
        services.tailscale = {
          enable = true;
          openFirewall = true;
          permitCertUid = "caddy";
          port = tsPort;
        };
        services.tailscaleAuth = {
          enable = true;
          group = "caddy";
        };
        services.caddy = {
          enable = true;
          email = "caddy@alanpearce.eu";
          virtualHosts = {
            "http://" = {
              # avoid logging to an awkward file name based on the attribute name i.e. http://
              hostName = "papers";
              extraConfig = ''
                redir ${tsHostname}{uri}
              '';
            };
            ${tsHostname} = {
              extraConfig = ''
                encode zstd gzip
                tls {
                  get_certificate tailscale
                }
                handle_path /static/* {
                  root * ${config.services.paperless.package}/lib/paperless-ngx/static
                  file_server
                }
                forward_auth unix//run/tailscale-nginx-auth/tailscale-nginx-auth.sock {
                  uri /auth
                  header_up Expected-Tailnet "${ts-domain}."
                  header_up Remote-Addr {remote_host}
                  header_up Remote-Port {remote_port}
                  header_up Original-URI {uri}
                  copy_headers {
                    Tailscale-User>X-Webauth-User
                    Tailscale-Name>X-Webauth-Name
                    Tailscale-Login>X-Webauth-Login
                    Tailscale-Tailnet>X-Webauth-Tailnet
                    Tailscale-Profile-Picture>X-Webauth-Profile-Picture
                  }
                }
                reverse_proxy [::1]:${toString config.services.paperless.port}
              '';
            };
          };
        };
        services.paperless = {
          enable = true;
          address = "[::1]";
          settings = {
            PAPERLESS_DBENGINE = "sqlite";
            PAPERLESS_TIME_ZONE = "Europe/Berlin";

            PAPERLESS_URL = "https://${tsHostname}";
            PAPERLESS_TRUSTED_PROXIES = "[::1]";
            PAPERLESS_USE_X_FORWARD_HOST = true;
            PAPERLESS_USE_X_FORWARD_PORT = true;
            PAPERLESS_PROXY_SSL_HEADER = [ "HTTP_X_FORWARDED_PROTO" "https" ];
            PAPERLESS_ENABLE_COMPRESSION = false; # let caddy do it

            PAPERLESS_ENABLE_HTTP_REMOTE_USER = true;
            PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME = "HTTP_X_WEBAUTH_LOGIN";

            PAPERLESS_OCR_SKIP_ARCHIVE_FILE = "with_text";
            PAPERLESS_OCR_LANGUAGE = "deu+eng";
            PAPERLESS_IGNORE_DATES = "09.08.90";

            PAPERLESS_TASK_WORKERS = 2;
            PAPERLESS_THREADS_PER_WORKER = 1;
            PAPERLESS_NUMBER_OF_SUGGESTED_DATES = 4;

            PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ".stfolder/*" ".stversions/*" ];

            PAPERLESS_FILENAME_FORMAT = "{correspondent}/{created} {title} {asn}";
            PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = true;
          };
        };
        system.stateVersion = "24.11";
      };
    };

  services.etcd = {
    enable = true;
    initialClusterState = "existing";
    dataDir = "/var/lib/etcd"; # TODO backup
    extraConf = {
      AUTO_COMPACTION_RETENTION = "1h";
    };
  };

  services.dex =
    let
      issuer = "https://id.alanpearce.eu/";
    in
    {
      enable = true;
      environmentFile = config.age.secrets.dex.path;
      settings = {
        inherit issuer;
        storage = {
          type = "etcd";
          config = {
            endpoints = config.services.etcd.listenClientUrls;
            namespace = "dex/";
          };
        };
        web.http = "127.0.0.1:5556";
        connectors = [{
          type = "github";
          id = "github";
          name = "GitHub";
          config = {
            clientID = "$GITHUB_CLIENT_ID";
            clientSecret = "$GITHUB_CLIENT_SECRET";
            redirectURI = "${issuer}callback";
            orgs = [{
              name = "alan-pearce";
            }];
            teamNameField = "slug";
            useLoginAsID = true;
          };
        }];
        staticClients = [
          {
            name = "Tailscale";
            id = "oCaiv7aije1thaep0eib";
            secretEnv = "TAILSCALE_CLIENT_SECRET";
            redirectURIs = [ "https://login.tailscale.com/a/oauth_response" ];
          }
        ];
      };
    };
  systemd.services.dex.unitConfig = {
    After = [ "etcd.service" ];
    Requires = [ "etcd.service" ];
  };

  services.redis = {
    servers = {
      website = {
        enable = true;
        port = 0;
        bind = net-redisip;
        databases = 1;
        maxclients = 6;
        requirePassFile = config.age.secrets.redis-website.path;
        settings = {
          tls-port = 6379;
          tls-cert-file = "/var/lib/acme/redis.alanpearce.eu/cert.pem";
          tls-key-file = "/var/lib/acme/redis.alanpearce.eu/key.pem";
          tls-ca-cert-file = "/etc/ssl/certs/ca-certificates.crt";
          tls-auth-clients = false;
        };
      };
    };
  };

  services.syncthing = {
    enable = true;
    dataDir = "/srv/syncthing";
    configDir = "/var/lib/syncthing";
    guiAddress = "[::]:8384";
    openDefaultPorts = true;
    overrideDevices = false;
    overrideFolders = false;
  };

  services.searchix = {
    enable = true;
    settings = {
      web =
        let
          baseURL = "https://searchix.alanpearce.eu";
        in
        {
          inherit baseURL;
          sentryDSN = "https://26d4cd8d20157ae2f6b4726ceae1a563@o4507187730120704.ingest.de.sentry.io/4507187734970448";
          contentSecurityPolicy =
            let
              self = "'self'";
            in
            {
              script-src = [
                (baseURL + "/static/")
                "https://searchix.stats.alanpearce.eu"
                "https://js-de.sentry-cdn.com"
                "https://browser.sentry-cdn.com"
              ];
              img-src = [
                self
                "https://searchix.stats.alanpearce.eu"
              ];
              connect-src = [
                self
                "https://searchix.stats.alanpearce.eu/count"
                "*.sentry.io"
              ];
              worker-src = [
                "blob:"
              ];
            };
          extraHeadHTML = ''
            <script async
              src="https://js-de.sentry-cdn.com/d735e99613a86e1625fb85d0e8e762de.min.js"
              crossorigin="anonymous"></script>
            <script data-goatcounter="https://searchix.stats.alanpearce.eu/count"
                    async src="//searchix.stats.alanpearce.eu/count.v4.js"
                    crossorigin="anonymous"
                    integrity="sha384-nRw6qfbWyJha9LhsOtSb2YJDyZdKvvCFh0fJYlkquSFjUxp9FVNugbfy8q1jdxI+"></script>
          '';
        };

      importer.sources = {
        darwin = {
          enable = true;
          fetcher = "download";
          url = "https://alanpearce.github.io/nix-options/darwin";
        };
        home-manager = {
          enable = true;
          fetcher = "download";
          url = "https://alanpearce.github.io/nix-options/home-manager";
        };
        nixpkgs = {
          enable = true;
          fetcher = "channel-nixpkgs";
          channel = "nixos-unstable";
        };
        nixos = {
          enable = true;
          fetcher = "channel-nixpkgs";
          channel = "nixos-unstable";
        };
      };
    };
  };

  programs.git = {
    enable = true;
    package = pkgs.gitMinimal;
    config = {
      advice = {
        detachedHead = false;
        mergeConflict = false;
      };
    };
  };

  systemd.services.laminar.environment = {
    NIX_PATH = "nixpkgs=flake:nixpkgs";
  };
  services.laminar = {
    enable = true;
    path = with pkgs; [
      bash
      coreutils
      git
      cached-nix-shell
      nix
      config.programs.ssh.package
      moreutils
      flock
      just
    ];
    settings = {
      bindHTTP = "[::1]:8002";
      keepRundirs = 1;
    };
  };
  users.users.laminar = {
    homeMode = "770";
  };

  virtualisation.containers = {
    enable = true;
    policy = {
      default = [{ type = "insecureAcceptAnything"; }];
    };
  };
}