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

with lib;
let
  cfg = config.virtualisation.oci-containers;
  proxy_env = config.networking.proxy.envVars;

  runDirSetup = ''
    if [ -z ''${XDG_RUNTIME_DIR} ]; then
        export XDG_RUNTIME_DIR="/run/";
    fi
  '';
  
  podOptions =
    { ... }: {

      options = {

        ports = mkOption {
          type = with types; listOf str;
          default = [];
          description = lib.mdDoc ''
            Network ports to publish from the pod to the outer host.
            Valid formats:
            - `<ip>:<hostPort>:<podPort>`
            - `<ip>::<podPort>`
            - `<hostPort>:<podPort>`
            - `<podPort>`
            Both `hostPort` and `podPort` can be specified as a range of
            ports.  When specifying ranges for both, the number of pod
            ports in the range must match the number of host ports in the
            range.  Example: `1234-1236:1234-1236/tcp`
            When specifying a range for `hostPort` only, the `podPort`
            must *not* be a range.  In this case, the pod port is published
            somewhere within the specified `hostPort` range.
            Example: `1234-1236:1234/tcp`
          '';
          example = literalExpression ''
            [
              "8080:9000"
            ]
          '';
        };

        volumes = mkOption {
          type = with types; listOf str;
          default = [];
          description = lib.mdDoc ''
            List of volumes to attach to this pod.
            Note that this is a list of `"src:dst"` strings to
            allow for `src` to refer to `/nix/store` paths, which
            would be difficult with an attribute set.  There are
            also a variety of mount options available as a third
            field;
          '';
          example = literalExpression ''
            [
              "volume_name:/path/inside/container"
              "/path/on/host:/path/inside/container"
            ]
          '';
        };

        enableInfra = mkOption {
          type = types.bool;
          default = true;
        };

        infraImage = mkOption {
          type = types.str;
          default = "";
        };

        infraName = mkOption {
          type = types.str;
          default = "";
        };
        
        dependsOn = mkOption {
          type = with types; listOf str;
          default = [];
          description = lib.mdDoc ''
            Define which other containers this one depends on. They will be added to both After and Requires for the unit.
            Use the same name as the attribute under `virtualisation.oci-containers.containers`.
          '';
          example = literalExpression ''
            virtualisation.oci-containers.containers = {
              node1 = {};
              node2 = {
                dependsOn = [ "node1" ];
              }
          }
          '';
        };

        contains = mkOption {
          type = with types; listOf str;
          default = [];
          description = lib.mkDoc ''
            Define what containers are contained within this pod,
            with shared ports and volumes as decribed above.
          '';
          example = literalExpression ''
            virtualisation.oci-containers.containers = {
              containers = {
                node1 = {};
                node2 = {};
              };
              pods = {
                nodes = {
                  contains = [
                    "node1"
                    "node2"
                  ];
                };
              };
                 }
          '';
        };

        extraOptions = mkOption {
          type = with types; listOf str;
          default = [];
          description = lib.mdDoc "Extra options for {command}`${defaultBackend} run`.";
          example = literalExpression ''
            ["--network=host"]
          '';
        };

        autoStart = mkOption {
          type = types.bool;
          default = true;
          description = lib.mdDoc ''
            When enabled, the container is automatically started on boot.
            If this option is set to false, the container has to be started on-demand via its service.
          '';
        };
      };
    };
  mkService = name: pod: let
    dependsOn = map (x: "${cfg.backend}-${x}.service") pod.dependsOn;
    contains = map (x: "${cfg.backend}-${x}.service") pod.contains;
    escapedName = escapeShellArg "${name}_pod";
    podPID = "$XDG_RUNTIME_DIR/${escapedName}.pid";
  in rec {
    wantedBy = [] ++ optional (pod.autoStart) "multi-user.target";
    after = dependsOn;
    before = contains;
    requires = dependsOn;
    wants = contains;
    environment = proxy_env;
    path = [ config.virtualisation.podman.package ];

    preStart = runDirSetup + ''
      ${cfg.backend} pod rm --ignore -f --pod-id-file=${podPID} || true
    '';

    script = runDirSetup + (concatStringsSep " \\\n  " ([
      "exec ${cfg.backend} pod create"
      "--name=${escapedName}"
      "--pod-id-file=${podPID}"
      "--replace"
    ] ++ map escapeShellArg pod.extraOptions
    ++ (if pod.enableInfra then ([
      "--infra-conmon-pidfile=$XDG_RUNTIME_DIR/${escapedName}-infra.pid"
      (if (pod.infraImage == "") then "" else "--infra-image=${pod.infraImage}")
      "--infra-name=${escapedName}-infra"
    ] ++ (map (p: "-p ${p}") pod.ports)
    ++ (map (v: "-v ${v}") pod.volumes)) else [
      ""
    ])
    ++ ["--infra=${lib.trivial.boolToString pod.enableInfra}"]
    ));

    preStop = runDirSetup + "${cfg.backend} pod stop --ignore --pod-id-file=${podPID}";
    postStop = runDirSetup + ''
      ${cfg.backend} pod rm --ignore -f --pod-id-file=${podPID} || true
      rm ${podPID}
    '';

    serviceConfig = {
      RemainAfterExit="yes";
    };
  };
in
{
  options.virtualisation.oci-containers.pods = mkOption {
    default = {};
    type = types.attrsOf (types.submodule podOptions);
    description = lib.mdDoc "OCI (podman) Pods to run as a systemd service";
  };
  config = let
    merge = p: lib.lists.foldr (e1: e2: e1 // e2) {} p;
    joinPods = pods: lib.lists.foldr lib.attrsets.unionOfDisjoint {} (map merge (attrValues (mapAttrs (name: pod: map (cont: {"${cont}".extraOptions=["--pod=${name}_pod"];}) pod.contains) pods)));
    makeBinds = pods: lib.lists.foldr lib.attrsets.unionOfDisjoint {} (map merge (attrValues (mapAttrs (name: pod: map (cont: {"${cfg.backend}-${cont}".partOf=["${cfg.backend}_pod-${name}.service"];}) pod.contains) pods)));
    test = pkgs.writeText "THISISATEST" (joinPods cfg.pods);
  in lib.mkIf (cfg.pods != {}) {
    systemd.services = (mapAttrs'
      (n: v: nameValuePair "${cfg.backend}_pod-${n}"
        (mkService n v)) cfg.pods) // (makeBinds cfg.pods);
    virtualisation.oci-containers.containers = joinPods cfg.pods;
  };
}
