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

with lib;

let

  cfg = config.networking.dhcpcd-prefixdelegation;

  downstreamInterfaceConfig = name: opts: "${name}${
    optionalString (opts.sla_id != null) "/${builtins.toString opts.sla_id}${
      optionalString (opts.prefix_len != null) "/${builtins.toString opts.prefix_len}${
        optionalString (opts.suffix != null) "/${opts.suffix}"
      }"
    }"
  }";

  interfaceConfig = name: opts: ''
    interface ${name}
      ipv6rs
      ia_pd ${builtins.toString opts.iaid}${
        optionalString (opts.prefix != null) "/${opts.prefix}${
          optionalString (opts.prefix_len != null) "/${builtins.toString opts.prefix_len}"
        }"
      } ${concatMapStringsSep " " ({name, value}: downstreamInterfaceConfig name value) (attrsToList opts.interfaces)}
  '';


  dhcpcdConf = pkgs.writeText "dhcpcd.conf" ''
    duid
    noipv6rs
    waitip 6
    ipv6only

    allowinterfaces ${concatStringsSep " " (builtins.attrNames cfg.interfaces)} ${concatMapStringsSep " " ({name, value}: concatStringsSep "" (builtins.attrNames value.interfaces)) (attrsToList cfg.interfaces)}

    ${concatMapStringsSep "\n" ({name, value}: interfaceConfig name value) (attrsToList cfg.interfaces)}
  '';

  downstreamInterfaceOpts = { ... }: {
    options = {
      sla_id = mkOption {
        type = with types; nullOr ints.unsigned;
        default = null;
      };

      prefix_len = mkOption {
        type = with types; nullOr ints.unsigned;
        default = null;
      };

      suffix = mkOption {
        type = with types; nullOr str;
        default = null;
      };
    };
  };

  interfaceOpts = { ... }: {
    options = {
      iaid = mkOption {
        type = with types; ints.unsigned;
        description = ''
          Request a delegated prefix with this IAID on this interface
        '';
      };

      prefix = mkOption {
        type = with types; nullOr str;
        default = null;
      };

      prefix_len = mkOption {
        type = with types; nullOr ints.unsigned;
        default = null;
      };

      interfaces = mkOption {
        type = with types; attrsOf (submodule downstreamInterfaceOpts);
        default = {};
        description =''
          Interfaces to assign IPv6 prefixes to
        '';
      };
    };
  };

in

{

  options = {

    networking.dhcpcd-prefixdelegation = {
      enable = mkEnableOption "dhcpcd for prefixdelegation";

      interfaces = mkOption {
        type = with types; attrsOf (submodule interfaceOpts);
        default = {};
        description = ''
          Interfaces to request IPv6 prefixes from
        '';
      };
    };
  };

  config = mkIf cfg.enable {

    environment.etc."dhcpcd.conf".source = dhcpcdConf;

    systemd.services.dhcpcd-prefixdelegation = {
        description = "DHCP Client for IPv6 Prefix Delegation";

        wantedBy = [ "multi-user.target" ];
        wants = [ "network.target" ];
        before = [ "network-online.target" ];

        # Stopping dhcpcd during a reconfiguration is undesirable
        # because it brings down the network interfaces configured by
        # dhcpcd.  So do a "systemctl restart" instead.
        stopIfChanged = false;

        path = [ pkgs.dhcpcd ];

        unitConfig.ConditionCapability = "CAP_NET_ADMIN";

        serviceConfig =
          { Type = "forking";
            PIDFile = "/run/dhcpcd/pid";
            RuntimeDirectory = "dhcpcd";
            ExecStart = "@${pkgs.dhcpcd}/sbin/dhcpcd dhcpcd --quiet --config ${dhcpcdConf}";
            ExecReload = "${pkgs.dhcpcd}/sbin/dhcpcd --rebind";
            Restart = "always";
          };
      };

    users.users.dhcpcd = {
      isSystemUser = true;
      group = "dhcpcd";
    };
    users.groups.dhcpcd = {};

  };

}