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

with lib;

let
  cfg = config.programs.borgmatic;

  yamlFormat = pkgs.formats.yaml { };

  mkNullableOption = args:
    lib.mkOption (args // {
      type = lib.types.nullOr args.type;
      default = null;
    });

  cleanRepositories = repos:
    map (repo:
      if builtins.isString repo then {
        path = repo;
      } else
        removeNullValues repo) repos;

  mkRetentionOption = frequency:
    mkNullableOption {
      type = types.int;
      description =
        "Number of ${frequency} archives to keep. Use -1 for no limit.";
      example = 3;
    };

  extraConfigOption = mkOption {
    type = yamlFormat.type;
    default = { };
    description = "Extra settings.";
  };

  repositoryOption = types.submodule {
    options = {
      path = mkOption {
        type = types.str;
        example = "ssh://myuser@myrepo.myserver.com/./repo";
        description = "Path of the repository.";
      };

      label = mkOption {
        type = types.nullOr types.str;
        default = null;
        example = "remote";
        description = ''
          Short text describing the repository. Can be used with the
          `--repository` flag to select a repository.
        '';
      };
    };
  };

  consistencyCheckModule = types.submodule {
    options = {
      name = mkOption {
        type = types.enum [ "repository" "archives" "data" "extract" ];
        description = "Name of consistency check to run.";
        example = "repository";
      };

      frequency = mkNullableOption {
        type = types.strMatching "([[:digit:]]+ .*)|always";
        description = "Frequency of this type of check";
        example = "2 weeks";
      };
    };
  };

  configModule = types.submodule ({ config, ... }: {
    config.location.extraConfig.exclude_from =
      mkIf config.location.excludeHomeManagerSymlinks
      (mkAfter [ (toString hmExcludeFile) ]);
    options = {
      location = {
        sourceDirectories = mkOption {
          type = types.listOf types.str;
          description = "Directories to backup.";
          example = literalExpression "[config.home.homeDirectory]";
        };

        repositories = mkOption {
          type = types.listOf (types.either types.str repositoryOption);
          apply = cleanRepositories;
          example = literalExpression ''
            [
              {
                "path" = "ssh://myuser@myrepo.myserver.com/./repo";
                "label" = "server";
              }
              {
                "path" = "/var/lib/backups/local.borg";
                "label" = "local";
              }
            ]
          '';
          description = ''
            List of local or remote repositories with paths and optional labels.
          '';
        };

        excludeHomeManagerSymlinks = mkOption {
          type = types.bool;
          description = ''
            Whether to exclude Home Manager generated symbolic links from
            the backups. This facilitates restoring the whole home
            directory when the Nix store doesn't contain the latest
            Home Manager generation.
          '';
          default = false;
          example = true;
        };

        extraConfig = extraConfigOption;
      };

      storage = {
        encryptionPasscommand = mkNullableOption {
          type = types.str;
          description = "Command writing the passphrase to standard output.";
          example =
            literalExpression ''"''${pkgs.password-store}/bin/pass borg-repo"'';
        };
        extraConfig = extraConfigOption;
      };

      retention = {
        keepWithin = mkNullableOption {
          type = types.strMatching "[[:digit:]]+[Hdwmy]";
          description = "Keep all archives within this time interval.";
          example = "2d";
        };

        keepSecondly = mkRetentionOption "secondly";
        keepMinutely = mkRetentionOption "minutely";
        keepHourly = mkRetentionOption "hourly";
        keepDaily = mkRetentionOption "daily";
        keepWeekly = mkRetentionOption "weekly";
        keepMonthly = mkRetentionOption "monthly";
        keepYearly = mkRetentionOption "yearly";

        extraConfig = extraConfigOption;
      };

      consistency = {
        checks = mkOption {
          type = types.listOf consistencyCheckModule;
          default = [ ];
          description = "Consistency checks to run";
          example = literalExpression ''
            [
              {
                name = "repository";
                frequency = "2 weeks";
              }
              {
                name = "archives";
                frequency = "4 weeks";
              }
              {
                name = "data";
                frequency = "6 weeks";
              }
              {
                name = "extract";
                frequency = "6 weeks";
              }
            ];
          '';
        };

        extraConfig = extraConfigOption;
      };

      output = { extraConfig = extraConfigOption; };

      hooks = { extraConfig = extraConfigOption; };
    };
  });

  removeNullValues = attrSet: filterAttrs (key: value: value != null) attrSet;

  hmFiles = builtins.attrValues config.home.file;
  hmSymlinks = (lib.filter (file: !file.recursive) hmFiles);
  hmExcludePattern = file: ''
    ${config.home.homeDirectory}/${file.target}
  '';
  hmExcludePatterns = lib.concatMapStrings hmExcludePattern hmSymlinks;
  hmExcludeFile = pkgs.writeText "hm-symlinks.txt" hmExcludePatterns;

  writeConfig = config:
    generators.toYAML { } (removeNullValues ({
      source_directories = config.location.sourceDirectories;
      repositories = config.location.repositories;
      encryption_passcommand = config.storage.encryptionPasscommand;
      keep_within = config.retention.keepWithin;
      keep_secondly = config.retention.keepSecondly;
      keep_minutely = config.retention.keepMinutely;
      keep_hourly = config.retention.keepHourly;
      keep_daily = config.retention.keepDaily;
      keep_weekly = config.retention.keepWeekly;
      keep_monthly = config.retention.keepMonthly;
      keep_yearly = config.retention.keepYearly;
      checks = config.consistency.checks;
    } // config.location.extraConfig // config.storage.extraConfig
      // config.retention.extraConfig // config.consistency.extraConfig
      // config.output.extraConfig // config.hooks.extraConfig));
in {
  meta.maintainers = [ maintainers.DamienCassou ];

  options = {
    programs.borgmatic = {
      enable = mkEnableOption "Borgmatic";

      package = mkPackageOption pkgs "borgmatic" { };

      backups = mkOption {
        type = types.attrsOf configModule;
        description = ''
          Borgmatic allows for several named backup configurations,
          each with its own source directories and repositories.
        '';
        example = literalExpression ''
          {
            personal = {
              location = {
                sourceDirectories = [ "/home/me/personal" ];
                repositories = [ "ssh://myuser@myserver.com/./personal-repo" ];
              };
            };
            work = {
              location = {
                sourceDirectories = [ "/home/me/work" ];
                repositories = [ "ssh://myuser@myserver.com/./work-repo" ];
              };
            };
          };
        '';
      };
    };
  };

  config = mkIf cfg.enable {
    assertions = [
      (lib.hm.assertions.assertPlatform "programs.borgmatic" pkgs
        lib.platforms.linux)
    ];

    xdg.configFile = with lib.attrsets;
      mapAttrs' (configName: config:
        nameValuePair ("borgmatic.d/" + configName + ".yaml") {
          text = writeConfig config;
        }) cfg.backups;

    home.packages = [ cfg.package ];
  };
}