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

with utils;
with lib;

let
  cfg = config.services.pgbackrest;
  settingsFormat = pkgs.formats.ini {
    listsAsDuplicateKeys = true;
    mkKeyValue = lib.generators.mkKeyValueDefault {
      mkValueString = v: with builtins;
        let err = t: v: abort
              ("mkValueString: " +
               "${t} not supported: ${toPretty {} v}");
        in   if isInt      v then toString v
        # convert derivations to store paths
        else if lib.isDerivation v then toString v
        # we default to not quoting strings
        else if isString   v then v
        # isString returns "1", which is not a good default
        else if true  ==   v then "y"
        # here it returns to "", which is even less of a good default
        else if false ==   v then "n"
        else if null  ==   v then "null"
        # if you have lists you probably want to replace this
        else if isList     v then err "lists" v
        # same as for lists, might want to replace
        else if isAttrs    v then err "attrsets" v
        # functions can’t be printed of course
        else if isFunction v then err "functions" v
        # Floats currently can't be converted to precise strings,
        # condition warning on nix version once this isn't a problem anymore
        # See https://github.com/NixOS/nix/pull/3480
        else if isFloat    v then libStr.floatToString v
        else err "this value is" (toString v);
    } "=";
  };

  mkDSCPOption = options: mkOption {
    type = types.numbers.between 0 63;
  } // options;

  loglevelType = types.enum ["off" "error" "warn" "info" "detail" "debug" "trace"];
  inherit (utils.systemdUtils.unitOptions) unitOption;
in {
  options = {
    services.pgbackrest = {
      enable = mkEnableOption "pgBackRest";

      package = mkPackageOption pkgs "pgbackrest" {};
      dscpPackage = mkPackageOption pkgs "libdscp" { nullable = true; default = null; };

      dscp.archive-push = mkDSCPOption { default = 24; description = "DSCP during archive push"; };
      dscp.backup = mkDSCPOption { default = 8; description = "DSCP during backup"; };

      configurePostgresql = {
        enable = mkEnableOption "configuring PostgreSQL for sending WAL to pgBackRest" // {
          default = config.services.postgresql.enable;
          defaultText = literalExpression "config.systemd.services.postgresql.enable";
        };

        stanza = mkOption {
          type = types.str;
          default = config.networking.hostName;
          defaultText = literalExpression "config.networking.hostName";
          description = "Stanza";
        };
      };

      settings = mkOption {
        type = types.submodule {
          freeformType = settingsFormat.type;

          options = {
            global.log-level-console = mkOption {
              type = loglevelType;
              default = "detail";
              description = "Log level to console";
            };
            global.log-level-file = mkOption {
              type = loglevelType;
              default = "off";
              description = "Log level to logfile";
            };
            global.log-level-stderr = mkOption {
              type = loglevelType;
              default = "warn";
              description = "Log level to stderr";
            };

            global.log-subprocess = mkOption {
              type = types.bool;
              default = true;
              description = "Log subprocesses?";
            };
            global.log-timestamp = mkOption {
              type = types.bool;
              default = false;
              description = "Log timestamps?";
            };
          };
        };
        default = {};
        description = ''
          Configuration for pgBackRest
        '';
      };

      tlsServer = {
        enable = mkEnableOption "pgBackRest TLS Server";

        user = mkOption {
          type = types.str;
          default = "postgres";
          description = "User";
        };
        group = mkOption {
          type = types.str;
          default = "postgres";
          description = "Group";
        };
      };

      backups = mkOption {
        type = types.attrsOf (types.submodule ({ name, ... }: {
          options = {
            type = mkOption {
              type = types.enum ["full" "incr" "diff"];
              default = "full";
              description = "Type";
            };

            stanza = mkOption {
              type = types.str;
              default = cfg.configurePostgresql.stanza;
              defaultText = literalExpression "config.services.pgbackrest.configurePostgresql.stanza";
              description = "Stanza";
            };
            repo = mkOption {
              type = types.nullOr (types.strMatching "^[0-9]+$");
              description = "Repository number";
            };

            user = mkOption {
              type = types.str;
              default = "postgres";
              description = "User";
            };
            group = mkOption {
              type = types.str;
              default = "postgres";
              description = "Group";
            };

            timerConfig = mkOption {
              type = types.attrsOf unitOption;
              description = "Systemd timer options";
            };
          };
        }));
        default = {};
        description = "Configure backups";
      };
    };
  };

  config = mkIf cfg.enable {
    environment.systemPackages = [ cfg.package ];

    services.postgresql.settings = mkIf cfg.configurePostgresql.enable {
      archive_command = let
        pgbackrest-dscp-wrapped = pkgs.writeShellApplication {
          name = "pgbackrest-dscp";
          runtimeInputs = [ cfg.package ];
          text = ''
            export LD_PRELOAD
            LD_PRELOAD=''${LD_PRELOAD:+':'$LD_PRELOAD':'}
            if [[ $LD_PRELOAD != *':'''${cfg.dscpPackage}/lib/libdscp.so''':'* ]]; then
                LD_PRELOAD='${cfg.dscpPackage}/lib/libdscp.so'$LD_PRELOAD
            fi
            LD_PRELOAD=''${LD_PRELOAD#':'}
            LD_PRELOAD=''${LD_PRELOAD%':'}

            : "''${DSCP:=${toString cfg.dscp.archive-push}}"

            exec -- pgbackrest "$@"
          '';
        };
        pgbackrest = if cfg.dscpPackage != null then "${pgbackrest-dscp-wrapped}/bin/pgbackrest-dscp" else "${cfg.package}/bin/pgbackrest";
      in "${pgbackrest} --stanza ${escapeShellArg cfg.configurePostgresql.stanza} archive-push %p";
      archive_mode = true;
      max_wal_senders = mkDefault 3;
      wal_level = "replica";
    };

    systemd.services = {
      pgbackrest-tls-server = mkIf cfg.tlsServer.enable {
        description = "pgBackRest TLS-Server";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];

        restartTriggers = [ config.environment.etc."pgbackrest/pgbackrest.conf".source ];

        unitConfig = {
          StartLimitIntervalSec = 0;
        };

        serviceConfig = {
          Type = "simple";
          Restart = "always";
          RestartSec = 1;
          User = cfg.tlsServer.user;
          Group = cfg.tlsServer.group;
          ExecStart = "${cfg.package}/bin/pgbackrest server";
          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
        };
      };
    } // mapAttrs' (name: backupCfg: nameValuePair "pgbackrest-backup@${escapeSystemdPath name}" {
      description = "Perform pgBackRest Backup (${name}${optionalString (!(isNull backupCfg.repo)) " repo${backupCfg.repo}"})";
      serviceConfig = {
        Type = "oneshot";
        ExecStart = "${cfg.package}/bin/pgbackrest --type ${escapeSystemdExecArg backupCfg.type} --stanza ${escapeSystemdExecArg backupCfg.stanza}${optionalString (!(isNull backupCfg.repo)) " --repo ${backupCfg.repo}"} backup";
        User = backupCfg.user;
        Group = backupCfg.group;
        Restart = "on-failure";
        RestartSec = "5min";

        Environment = mkIf (cfg.dscpPackage != null) [
          "LD_PRELOAD=\"${cfg.dscpPackage}/lib/libdscp.so\""
          "DSCP=\"${toString cfg.dscp.backup}\""
        ];
      };
    }) cfg.backups;

    systemd.timers = mapAttrs' (name: backupCfg: nameValuePair "pgbackrest-backup@${escapeSystemdPath name}" {
      wantedBy = [ "timers.target" ];
      inherit (backupCfg) timerConfig;
    }) cfg.backups;

    environment.etc."pgbackrest/pgbackrest.conf".source = settingsFormat.generate "pgbackrest.conf" cfg.settings;
  };
}