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

with lib;

let

  pkg = pkgs.cjdns;

  cfg = config.services.cjdns;

  connectToSubmodule =
  { ... }:
  { options =
    { password = mkOption {
        type = types.str;
        description = lib.mdDoc "Authorized password to the opposite end of the tunnel.";
      };
      login = mkOption {
        default = "";
        type = types.str;
        description = lib.mdDoc "(optional) name your peer has for you";
      };
      peerName = mkOption {
        default = "";
        type = types.str;
        description = lib.mdDoc "(optional) human-readable name for peer";
      };
      publicKey = mkOption {
        type = types.str;
        description = lib.mdDoc "Public key at the opposite end of the tunnel.";
      };
      hostname = mkOption {
        default = "";
        example = "foobar.hype";
        type = types.str;
        description = lib.mdDoc "Optional hostname to add to /etc/hosts; prevents reverse lookup failures.";
      };
    };
  };

  # Additional /etc/hosts entries for peers with an associated hostname
  cjdnsExtraHosts = pkgs.runCommand "cjdns-hosts" {} ''
    exec >$out
    ${concatStringsSep "\n" (mapAttrsToList (k: v:
        optionalString (v.hostname != "")
          "echo $(${pkgs.cjdns}/bin/publictoip6 ${v.publicKey}) ${v.hostname}")
        (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo))}
  '';

  parseModules = x:
    x // { connectTo = mapAttrs (name: value: { inherit (value) password publicKey; }) x.connectTo; };

  cjdrouteConf = builtins.toJSON ( recursiveUpdate {
    admin = {
      bind = cfg.admin.bind;
      password = "@CJDNS_ADMIN_PASSWORD@";
    };
    authorizedPasswords = map (p: { password = p; }) cfg.authorizedPasswords;
    interfaces = {
      ETHInterface = if (cfg.ETHInterface.bind != "") then [ (parseModules cfg.ETHInterface) ] else [ ];
      UDPInterface = if (cfg.UDPInterface.bind != "") then [ (parseModules cfg.UDPInterface) ] else [ ];
    };

    privateKey = "@CJDNS_PRIVATE_KEY@";

    resetAfterInactivitySeconds = 100;

    router = {
      interface = { type = "TUNInterface"; };
      ipTunnel = {
        allowedConnections = [];
        outgoingConnections = [];
      };
    };

    security = [ { exemptAngel = 1; setuser = "nobody"; } ];

  } cfg.extraConfig);

in

{
  options = {

    services.cjdns = {

      enable = mkOption {
        type = types.bool;
        default = false;
        description = lib.mdDoc ''
          Whether to enable the cjdns network encryption
          and routing engine. A file at /etc/cjdns.keys will
          be created if it does not exist to contain a random
          secret key that your IPv6 address will be derived from.
        '';
      };

      extraConfig = mkOption {
        type = types.attrs;
        default = {};
        example = { router.interface.tunDevice = "tun10"; };
        description = lib.mdDoc ''
          Extra configuration, given as attrs, that will be merged recursively
          with the rest of the JSON generated by this module, at the root node.
        '';
      };

      confFile = mkOption {
        type = types.nullOr types.path;
        default = null;
        example = "/etc/cjdroute.conf";
        description = lib.mdDoc ''
          Ignore all other cjdns options and load configuration from this file.
        '';
      };

      authorizedPasswords = mkOption {
        type = types.listOf types.str;
        default = [ ];
        example = [
          "snyrfgkqsc98qh1y4s5hbu0j57xw5s0"
          "z9md3t4p45mfrjzdjurxn4wuj0d8swv"
          "49275fut6tmzu354pq70sr5b95qq0vj"
        ];
        description = lib.mdDoc ''
          Any remote cjdns nodes that offer these passwords on
          connection will be allowed to route through this node.
        '';
      };

      admin = {
        bind = mkOption {
          type = types.str;
          default = "127.0.0.1:11234";
          description = lib.mdDoc ''
            Bind the administration port to this address and port.
          '';
        };
      };

      UDPInterface = {
        bind = mkOption {
          type = types.str;
          default = "";
          example = "192.168.1.32:43211";
          description = lib.mdDoc ''
            Address and port to bind UDP tunnels to.
          '';
         };
        connectTo = mkOption {
          type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
          default = { };
          example = literalExpression ''
            {
              "192.168.1.1:27313" = {
                hostname = "homer.hype";
                password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
                publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
              };
            }
          '';
          description = lib.mdDoc ''
            Credentials for making UDP tunnels.
          '';
        };
      };

      ETHInterface = {
        bind = mkOption {
          type = types.str;
          default = "";
          example = "eth0";
          description =
            lib.mdDoc ''
              Bind to this device for native ethernet operation.
              `all` is a pseudo-name which will try to connect to all devices.
            '';
        };

        beacon = mkOption {
          type = types.int;
          default = 2;
          description = lib.mdDoc ''
            Auto-connect to other cjdns nodes on the same network.
            Options:
              0: Disabled.
              1: Accept beacons, this will cause cjdns to accept incoming
                 beacon messages and try connecting to the sender.
              2: Accept and send beacons, this will cause cjdns to broadcast
                 messages on the local network which contain a randomly
                 generated per-session password, other nodes which have this
                 set to 1 or 2 will hear the beacon messages and connect
                 automatically.
          '';
        };

        connectTo = mkOption {
          type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
          default = { };
          example = literalExpression ''
            {
              "01:02:03:04:05:06" = {
                hostname = "homer.hype";
                password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
                publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
              };
            }
          '';
          description = lib.mdDoc ''
            Credentials for connecting look similar to UDP credientials
            except they begin with the mac address.
          '';
        };
      };

      addExtraHosts = mkOption {
        type = types.bool;
        default = false;
        description = lib.mdDoc ''
          Whether to add cjdns peers with an associated hostname to
          {file}`/etc/hosts`.  Beware that enabling this
          incurs heavy eval-time costs.
        '';
      };

    };

  };

  config = mkIf cfg.enable {

    boot.kernelModules = [ "tun" ];

    # networking.firewall.allowedUDPPorts = ...

    systemd.services.cjdns = {
      description = "cjdns: routing engine designed for security, scalability, speed and ease of use";
      wantedBy = [ "multi-user.target" "sleep.target"];
      after = [ "network-online.target" ];
      bindsTo = [ "network-online.target" ];

      preStart = if cfg.confFile != null then "" else ''
        [ -e /etc/cjdns.keys ] && source /etc/cjdns.keys

        if [ -z "$CJDNS_PRIVATE_KEY" ]; then
            shopt -s lastpipe
            ${pkg}/bin/makekeys | { read private ipv6 public; }

            umask 0077
            echo "CJDNS_PRIVATE_KEY=$private" >> /etc/cjdns.keys
            echo -e "CJDNS_IPV6=$ipv6\nCJDNS_PUBLIC_KEY=$public" > /etc/cjdns.public

            chmod 600 /etc/cjdns.keys
            chmod 444 /etc/cjdns.public
        fi

        if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then
            echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" \
                >> /etc/cjdns.keys
        fi
      '';

      script = (
        if cfg.confFile != null then "${pkg}/bin/cjdroute < ${cfg.confFile}" else
          ''
            source /etc/cjdns.keys
            (cat <<'EOF'
            ${cjdrouteConf}
            EOF
            ) | sed \
                -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \
                -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \
                | ${pkg}/bin/cjdroute
         ''
      );

      startLimitIntervalSec = 0;
      serviceConfig = {
        Type = "forking";
        Restart = "always";
        RestartSec = 1;
        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID";
        ProtectSystem = true;
        # Doesn't work on i686, causing service to fail
        MemoryDenyWriteExecute = !pkgs.stdenv.isi686;
        ProtectHome = true;
        PrivateTmp = true;
      };
    };

    networking.hostFiles = mkIf cfg.addExtraHosts [ cjdnsExtraHosts ];

    assertions = [
      { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile != null );
        message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined.";
      }
      { assertion = config.networking.enableIPv6;
        message = "networking.enableIPv6 must be enabled for CJDNS to work";
      }
    ];

  };

}
