# This module exposes options to build a disk image with a GUID Partition Table
# (GPT). It uses systemd-repart to build the image.

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

let
  cfg = config.image.repart;

  inherit (utils.systemdUtils.lib) GPTMaxLabelLength;

  partitionOptions = {
    options = {
      storePaths = lib.mkOption {
        type = with lib.types; listOf path;
        default = [ ];
        description = "The store paths to include in the partition.";
      };

      stripNixStorePrefix = lib.mkOption {
        type = lib.types.bool;
        default = false;
        description = ''
          Whether to strip `/nix/store/` from the store paths. This is useful
          when you want to build a partition that only contains store paths and
          is mounted under `/nix/store`.
        '';
      };

      contents = lib.mkOption {
        type =
          with lib.types;
          attrsOf (submodule {
            options = {
              source = lib.mkOption {
                type = types.path;
                description = "Path of the source file.";
              };
            };
          });
        default = { };
        example = lib.literalExpression ''
          {
            "/EFI/BOOT/BOOTX64.EFI".source =
              "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";

            "/loader/entries/nixos.conf".source = systemdBootEntry;
          }
        '';
        description = "The contents to end up in the filesystem image.";
      };

      repartConfig = lib.mkOption {
        type =
          with lib.types;
          attrsOf (oneOf [
            str
            int
            bool
            (listOf str)
          ]);
        example = {
          Type = "home";
          SizeMinBytes = "512M";
          SizeMaxBytes = "2G";
        };
        description = ''
          Specify the repart options for a partiton as a structural setting.
          See {manpage}`repart.d(5)`
          for all available options.
        '';
      };
    };
  };

  mkfsOptionsToEnv =
    opts:
    lib.mapAttrs' (fsType: options: {
      name = "SYSTEMD_REPART_MKFS_OPTIONS_${lib.toUpper fsType}";
      value = builtins.concatStringsSep " " options;
    }) opts;
in
{
  imports = [
    ./repart-verity-store.nix
    ./file-options.nix
    (lib.mkRenamedOptionModuleWith {
      sinceRelease = 2411;
      from = [
        "image"
        "repart"
        "imageFileBasename"
      ];
      to = [
        "image"
        "baseName"
      ];
    })
    (lib.mkRenamedOptionModuleWith {
      sinceRelease = 2411;
      from = [
        "image"
        "repart"
        "imageFile"
      ];
      to = [
        "image"
        "fileName"
      ];
    })
  ];

  options.image.repart = {

    name = lib.mkOption {
      type = lib.types.str;
      description = ''
          Name of the image.

        If this option is unset but config.system.image.id is set,
        config.system.image.id is used as the default value.
      '';
    };

    version = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = config.system.image.version;
      defaultText = lib.literalExpression "config.system.image.version";
      description = "Version of the image";
    };

    compression = {
      enable = lib.mkEnableOption "Image compression";

      algorithm = lib.mkOption {
        type = lib.types.enum [
          "zstd"
          "xz"
          "zstd-seekable"
        ];
        default = "zstd";
        description = "Compression algorithm";
      };

      level = lib.mkOption {
        type = lib.types.int;
        description = ''
          Compression level. The available range depends on the used algorithm.
        '';
      };
    };

    seed = lib.mkOption {
      type = with lib.types; nullOr str;
      # Generated with `uuidgen`. Random but fixed to improve reproducibility.
      default = "0867da16-f251-457d-a9e8-c31f9a3c220b";
      description = ''
        A UUID to use as a seed. You can set this to `null` to explicitly
        randomize the partition UUIDs.
      '';
    };

    split = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = ''
        Enables generation of split artifacts from partitions. If enabled, for
        each partition with SplitName= set, a separate output file containing
        just the contents of that partition is generated.
      '';
    };

    sectorSize = lib.mkOption {
      type = with lib.types; nullOr int;
      default = 512;
      example = lib.literalExpression "4096";
      description = ''
        The sector size of the disk image produced by systemd-repart. This
        value must be a power of 2 between 512 and 4096.
      '';
    };

    package = lib.mkPackageOption pkgs "systemd-repart" {
      # We use buildPackages so that repart images are built with the build
      # platform's systemd, allowing for cross-compiled systems to work.
      default = [
        "buildPackages"
        "systemd"
      ];
      example = "pkgs.buildPackages.systemdMinimal.override { withCryptsetup = true; }";
    };

    partitions = lib.mkOption {
      type = with lib.types; attrsOf (submodule partitionOptions);
      default = { };
      example = lib.literalExpression ''
        {
          "10-esp" = {
            contents = {
              "/EFI/BOOT/BOOTX64.EFI".source =
                "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
            }
            repartConfig = {
              Type = "esp";
              Format = "fat";
            };
          };
          "20-root" = {
            storePaths = [ config.system.build.toplevel ];
            repartConfig = {
              Type = "root";
              Format = "ext4";
              Minimize = "guess";
            };
          };
        };
      '';
      description = ''
        Specify partitions as a set of the names of the partitions with their
        configuration as the key.
      '';
    };

    mkfsOptions = lib.mkOption {
      type = with lib.types; attrsOf (listOf str);
      default = { };
      example = lib.literalExpression ''
        {
          vfat = [ "-S 512" "-c" ];
        }
      '';
      description = ''
        Specify extra options for created file systems. The specified options
        are converted to individual environment variables of the format
        `SYSTEMD_REPART_MKFS_OPTIONS_<FSTYPE>`.

        See [upstream systemd documentation](https://github.com/systemd/systemd/blob/v255/docs/ENVIRONMENT.md?plain=1#L575-L577)
        for information about the usage of these environment variables.

        The example would produce the following environment variable:
        ```
        SYSTEMD_REPART_MKFS_OPTIONS_VFAT="-S 512 -c"
        ```
      '';
    };

    finalPartitions = lib.mkOption {
      type = lib.types.attrs;
      internal = true;
      readOnly = true;
      description = ''
        Convenience option to access partitions with added closures.
      '';
    };

    assertions = lib.mkOption {
      type = options.assertions.type;
      default = [ ];
      internal = true;
      visible = false;
      description = ''
        Assertions only evaluated by the repart image, not by the system toplevel.
      '';
    };

    warnings = lib.mkOption {
      type = options.warnings.type;
      default = [ ];
      internal = true;
      visible = false;
      description = ''
        Warnings only evaluated by the repart image, not by the system toplevel.
      '';
    };

  };

  config = {
    image.baseName =
      let
        version = config.image.repart.version;
        versionInfix = if version != null then "_${version}" else "";
      in
      cfg.name + versionInfix;
    image.extension =
      let
        compressionSuffix =
          lib.optionalString cfg.compression.enable
            {
              "zstd" = ".zst";
              "xz" = ".xz";
              "zstd-seekable" = ".zst";
            }
            ."${cfg.compression.algorithm}";

      in
      "raw" + compressionSuffix;

    image.repart =
      let
        makeClosure = paths: pkgs.closureInfo { rootPaths = paths; };

        # Add the closure of the provided Nix store paths to cfg.partitions so
        # that amend-repart-definitions.py can read it.
        addClosure =
          _name: partitionConfig:
          partitionConfig
          // (lib.optionalAttrs (partitionConfig.storePaths or [ ] != [ ]) {
            closure = "${makeClosure partitionConfig.storePaths}/store-paths";
          });
      in
      {
        name = lib.mkIf (config.system.image.id != null) (lib.mkOptionDefault config.system.image.id);
        compression = {
          # Generally default to slightly faster than default compression
          # levels under the assumption that most of the building will be done
          # for development and release builds will be customized.
          level =
            lib.mkOptionDefault
              {
                "zstd" = 3;
                "xz" = 3;
                "zstd-seekable" = 3;
              }
              ."${cfg.compression.algorithm}";
        };

        finalPartitions = lib.mapAttrs addClosure cfg.partitions;

        assertions = lib.mapAttrsToList (
          fileName: partitionConfig:
          let
            inherit (partitionConfig) repartConfig;
            labelLength = builtins.stringLength repartConfig.Label;
          in
          {
            assertion = repartConfig ? Label -> GPTMaxLabelLength >= labelLength;
            message = ''
              The partition label '${repartConfig.Label}'
              defined for '${fileName}' is ${toString labelLength} characters long,
              but the maximum label length supported by UEFI is ${toString GPTMaxLabelLength}.
            '';
          }
        ) cfg.partitions;

        warnings = lib.filter (v: v != null) (
          lib.mapAttrsToList (
            fileName: partitionConfig:
            let
              inherit (partitionConfig) repartConfig;
              suggestedMaxLabelLength = GPTMaxLabelLength - 2;
              labelLength = builtins.stringLength repartConfig.Label;
            in
            if (repartConfig ? Label && labelLength >= suggestedMaxLabelLength) then
              ''
                The partition label '${repartConfig.Label}'
                defined for '${fileName}' is ${toString labelLength} characters long.
                The suggested maximum label length is ${toString suggestedMaxLabelLength}.

                If you use sytemd-sysupdate style A/B updates, this might
                not leave enough space to increment the version number included in
                the label in a future release. For example, if your label is
                ${toString GPTMaxLabelLength} characters long (the maximum enforced by UEFI) and
                you're at version 9, you cannot increment this to 10.
              ''
            else
              null
          ) cfg.partitions
        );
      };

    system.build.image =
      let
        fileSystems = lib.filter (f: f != null) (
          lib.mapAttrsToList (_n: v: v.repartConfig.Format or null) cfg.partitions
        );

        format = pkgs.formats.ini { listsAsDuplicateKeys = true; };

        definitionsDirectory = utils.systemdUtils.lib.definitions "repart.d" format (
          lib.mapAttrs (_n: v: { Partition = v.repartConfig; }) cfg.finalPartitions
        );

        mkfsEnv = mkfsOptionsToEnv cfg.mkfsOptions;
        val = pkgs.callPackage ./repart-image.nix {
          systemd = cfg.package;
          imageFileBasename = config.image.baseName;
          inherit (cfg)
            name
            version
            compression
            split
            seed
            sectorSize
            finalPartitions
            ;
          inherit fileSystems definitionsDirectory mkfsEnv;
        };
      in
      lib.asserts.checkAssertWarn cfg.assertions cfg.warnings val;
  };

  meta.maintainers = with lib.maintainers; [
    nikstur
    willibutz
  ];
}
