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

with lib;

let

  cfg = config.services.i2pd;

  homeDir = "/var/lib/i2pd";

  strOpt = k: v: k + " = " + v;
  boolOpt = k: v: k + " = " + boolToString v;
  intOpt = k: v: k + " = " + toString v;
  lstOpt = k: xs: k + " = " + concatStringsSep "," xs;
  optionalNullString = o: s: optional (s != null) (strOpt o s);
  optionalNullBool = o: b: optional (b != null) (boolOpt o b);
  optionalNullInt = o: i: optional (i != null) (intOpt o i);
  optionalEmptyList = o: l: optional ([] != l) (lstOpt o l);

  mkEnableTrueOption = name: mkEnableOption name // { default = true; };

  mkEndpointOpt = name: addr: port: {
    enable = mkEnableOption name;
    name = mkOption {
      type = types.str;
      default = name;
      description = "The endpoint name.";
    };
    address = mkOption {
      type = types.str;
      default = addr;
      description = "Bind address for ${name} endpoint.";
    };
    port = mkOption {
      type = types.int;
      default = port;
      description = "Bind port for ${name} endoint.";
    };
  };

  i2cpOpts = name: {
    length = mkOption {
      type = types.int;
      description = "Guaranteed minimum hops for ${name} tunnels.";
      default = 3;
    };
    quantity = mkOption {
      type = types.int;
      description = "Number of simultaneous ${name} tunnels.";
      default = 5;
    };
  };

  mkKeyedEndpointOpt = name: addr: port: keyloc:
    (mkEndpointOpt name addr port) // {
      keys = mkOption {
        type = with types; nullOr str;
        default = keyloc;
        description = ''
          File to persist ${lib.toUpper name} keys.
        '';
      };
      inbound = i2cpOpts name;
      outbound = i2cpOpts name;
      latency.min = mkOption {
        type = with types; nullOr int;
        description = "Min latency for tunnels.";
        default = null;
      };
      latency.max = mkOption {
        type = with types; nullOr int;
        description = "Max latency for tunnels.";
        default = null;
      };
    };

  commonTunOpts = name: {
    outbound = i2cpOpts name;
    inbound = i2cpOpts name;
    crypto.tagsToSend = mkOption {
      type = types.int;
      description = "Number of ElGamal/AES tags to send.";
      default = 40;
    };
    destination = mkOption {
      type = types.str;
      description = "Remote endpoint, I2P hostname or b32.i2p address.";
    };
    keys = mkOption {
      type = types.str;
      default = name + "-keys.dat";
      description = "Keyset used for tunnel identity.";
    };
  } // mkEndpointOpt name "127.0.0.1" 0;

  sec = name: "\n[" + name + "]";
  notice = "# DO NOT EDIT -- this file has been generated automatically.";
  i2pdConf = let
    opts = [
      notice
      (strOpt "loglevel" cfg.logLevel)
      (boolOpt "logclftime" cfg.logCLFTime)
      (boolOpt "ipv4" cfg.enableIPv4)
      (boolOpt "ipv6" cfg.enableIPv6)
      (boolOpt "notransit" cfg.notransit)
      (boolOpt "floodfill" cfg.floodfill)
      (intOpt "netid" cfg.netid)
    ] ++ (optionalNullInt "bandwidth" cfg.bandwidth)
      ++ (optionalNullInt "port" cfg.port)
      ++ (optionalNullString "family" cfg.family)
      ++ (optionalNullString "datadir" cfg.dataDir)
      ++ (optionalNullInt "share" cfg.share)
      ++ (optionalNullBool "ssu" cfg.ssu)
      ++ (optionalNullBool "ntcp" cfg.ntcp)
      ++ (optionalNullString "ntcpproxy" cfg.ntcpProxy)
      ++ (optionalNullString "ifname" cfg.ifname)
      ++ (optionalNullString "ifname4" cfg.ifname4)
      ++ (optionalNullString "ifname6" cfg.ifname6)
      ++ [
      (sec "limits")
      (intOpt "transittunnels" cfg.limits.transittunnels)
      (intOpt "coresize" cfg.limits.coreSize)
      (intOpt "openfiles" cfg.limits.openFiles)
      (intOpt "ntcphard" cfg.limits.ntcpHard)
      (intOpt "ntcpsoft" cfg.limits.ntcpSoft)
      (intOpt "ntcpthreads" cfg.limits.ntcpThreads)
      (sec "upnp")
      (boolOpt "enabled" cfg.upnp.enable)
      (sec "precomputation")
      (boolOpt "elgamal" cfg.precomputation.elgamal)
      (sec "reseed")
      (boolOpt "verify" cfg.reseed.verify)
    ] ++ (optionalNullString "file" cfg.reseed.file)
      ++ (optionalEmptyList "urls" cfg.reseed.urls)
      ++ (optionalNullString "floodfill" cfg.reseed.floodfill)
      ++ (optionalNullString "zipfile" cfg.reseed.zipfile)
      ++ (optionalNullString "proxy" cfg.reseed.proxy)
      ++ [
      (sec "trust")
      (boolOpt "enabled" cfg.trust.enable)
      (boolOpt "hidden" cfg.trust.hidden)
    ] ++ (optionalEmptyList "routers" cfg.trust.routers)
      ++ (optionalNullString "family" cfg.trust.family)
      ++ [
      (sec "websockets")
      (boolOpt "enabled" cfg.websocket.enable)
      (strOpt "address" cfg.websocket.address)
      (intOpt "port" cfg.websocket.port)
      (sec "exploratory")
      (intOpt "inbound.length" cfg.exploratory.inbound.length)
      (intOpt "inbound.quantity" cfg.exploratory.inbound.quantity)
      (intOpt "outbound.length" cfg.exploratory.outbound.length)
      (intOpt "outbound.quantity" cfg.exploratory.outbound.quantity)
      (sec "ntcp2")
      (boolOpt "enabled" cfg.ntcp2.enable)
      (boolOpt "published" cfg.ntcp2.published)
      (intOpt "port" cfg.ntcp2.port)
      (sec "addressbook")
      (strOpt "defaulturl" cfg.addressbook.defaulturl)
    ] ++ (optionalEmptyList "subscriptions" cfg.addressbook.subscriptions)
      ++ (flip mapAttrs
      (collect (name: proto: proto ? port && proto ? address && proto ? name) cfg.proto)
      (proto: let protoOpts = [
        (sec name)
        (boolOpt "enabled" proto.enable)
        (strOpt "address" proto.address)
        (intOpt "port" proto.port)
        ] ++ (if proto ? keys then optionalNullString "keys" proto.keys else [])
        ++ (if proto ? auth then optionalNullBool "auth" proto.auth else [])
        ++ (if proto ? user then optionalNullString "user" proto.user else [])
        ++ (if proto ? pass then optionalNullString "pass" proto.pass else [])
        ++ (if proto ? strictHeaders then optionalNullBool "strictheaders" proto.strictHeaders else [])
        ++ (if proto ? hostname then optionalNullString "hostname" proto.hostname else [])
        ++ (if proto ? outproxy then optionalNullString "outproxy" proto.outproxy else [])
        ++ (if proto ? outproxyPort then optionalNullInt "outproxyport" proto.outproxyPort else [])
        ++ (if proto ? outproxyEnable then optionalNullBool "outproxy.enabled" proto.outproxyEnable else []);
        in (concatStringsSep "\n" protoOpts)
      ));
  in
    pkgs.writeText "i2pd.conf" (concatStringsSep "\n" opts);

  tunnelConf = let opts = [
    notice
    (flip mapAttrs
      (collect (name: tun: tun ? port && tun ? destination) cfg.outTunnels)
      (tun: let outTunOpts = [
        (sec name)
        "type = client"
        (intOpt "port" tun.port)
        (strOpt "destination" tun.destination)
        ] ++ (if tun ? destinationPort then optionalNullInt "destinationport" tun.destinationPort else [])
        ++ (if tun ? keys then
            optionalNullString "keys" tun.keys else [])
        ++ (if tun ? address then
            optionalNullString "address" tun.address else [])
        ++ (if tun ? inbound.length then
            optionalNullInt "inbound.length" tun.inbound.length else [])
        ++ (if tun ? inbound.quantity then
            optionalNullInt "inbound.quantity" tun.inbound.quantity else [])
        ++ (if tun ? outbound.length then
            optionalNullInt "outbound.length" tun.outbound.length else [])
        ++ (if tun ? outbound.quantity then
            optionalNullInt "outbound.quantity" tun.outbound.quantity else [])
        ++ (if tun ? crypto.tagsToSend then
            optionalNullInt "crypto.tagstosend" tun.crypto.tagsToSend else []);
        in concatStringsSep "\n" outTunOpts))
    (flip mapAttrs
      (collect (name: tun: tun ? port && tun ? address) cfg.inTunnels)
      (tun: let inTunOpts = [
        (sec name)
        "type = server"
        (intOpt "port" tun.port)
        (strOpt "host" tun.address)
      ] ++ (if tun ? destination then
            optionalNullString "destination" tun.destination else [])
        ++ (if tun ? keys then
            optionalNullString "keys" tun.keys else [])
        ++ (if tun ? inPort then
            optionalNullInt "inport" tun.inPort else [])
        ++ (if tun ? accessList then
            optionalEmptyList "accesslist" tun.accessList else []);
        in concatStringsSep "\n" inTunOpts))];
    in pkgs.writeText "i2pd-tunnels.conf" opts;

  i2pdSh = pkgs.writeScriptBin "i2pd" ''
    #!/bin/sh
    exec ${pkgs.i2pd}/bin/i2pd \
      ${if cfg.address == null then "" else "--host="+cfg.address} \
      --service \
      --conf=${i2pdConf} \
      --tunconf=${tunnelConf}
  '';

in

{

  imports = [
    (mkRenamedOptionModule [ "services" "i2pd" "extIp" ] [ "services" "i2pd" "address" ])
  ];

  ###### interface

  options = {

    services.i2pd = {

      enable = mkEnableOption "I2Pd daemon" // {
        description = ''
          Enables I2Pd as a running service upon activation.
          Please read http://i2pd.readthedocs.io/en/latest/ for further
          configuration help.
        '';
      };

      logLevel = mkOption {
        type = types.enum ["debug" "info" "warn" "error"];
        default = "error";
        description = ''
          The log level. <command>i2pd</command> defaults to "info"
          but that generates copious amounts of log messages.

          We default to "error" which is similar to the default log
          level of <command>tor</command>.
        '';
      };

      logCLFTime = mkEnableOption "Full CLF-formatted date and time to log";

      address = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Your external IP or hostname.
        '';
      };

      family = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Specify a family the router belongs to.
        '';
      };

      dataDir = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Alternative path to storage of i2pd data (RI, keys, peer profiles, ...)
        '';
      };

      share = mkOption {
        type = types.int;
        default = 100;
        description = ''
          Limit of transit traffic from max bandwidth in percents.
        '';
      };

      ifname = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Network interface to bind to.
        '';
      };

      ifname4 = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          IPv4 interface to bind to.
        '';
      };

      ifname6 = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          IPv6 interface to bind to.
        '';
      };

      ntcpProxy = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Proxy URL for NTCP transport.
        '';
      };

      ntcp = mkEnableTrueOption "ntcp";
      ssu = mkEnableTrueOption "ssu";

      notransit = mkEnableOption "notransit" // {
        description = ''
          Tells the router to not accept transit tunnels during startup.
        '';
      };

      floodfill = mkEnableOption "floodfill" // {
        description = ''
          If the router is declared to be unreachable and needs introduction nodes.
        '';
      };

      netid = mkOption {
        type = types.int;
        default = 2;
        description = ''
          I2P overlay netid.
        '';
      };

      bandwidth = mkOption {
        type = with types; nullOr int;
        default = null;
        description = ''
           Set a router bandwidth limit integer in KBps.
           If not set, <command>i2pd</command> defaults to 32KBps.
        '';
      };

      port = mkOption {
        type = with types; nullOr int;
        default = null;
        description = ''
          I2P listen port. If no one is given the router will pick between 9111 and 30777.
        '';
      };

      enableIPv4 = mkEnableTrueOption "IPv4 connectivity";
      enableIPv6 = mkEnableOption "IPv6 connectivity";
      nat = mkEnableTrueOption "NAT bypass";

      upnp.enable = mkEnableOption "UPnP service discovery";
      upnp.name = mkOption {
        type = types.str;
        default = "I2Pd";
        description = ''
          Name i2pd appears in UPnP forwardings list.
        '';
      };

      precomputation.elgamal = mkEnableTrueOption "Precomputed ElGamal tables" // {
        description = ''
          Whenever to use precomputated tables for ElGamal.
          <command>i2pd</command> defaults to <literal>false</literal>
          to save 64M of memory (and looses some performance).

          We default to <literal>true</literal> as that is what most
          users want anyway.
        '';
      };

      reseed.verify = mkEnableOption "SU3 signature verification";

      reseed.file = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Full path to SU3 file to reseed from.
        '';
      };

      reseed.urls = mkOption {
        type = with types; listOf str;
        default = [];
        description = ''
          Reseed URLs.
        '';
      };

      reseed.floodfill = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Path to router info of floodfill to reseed from.
        '';
      };

      reseed.zipfile = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Path to local .zip file to reseed from.
        '';
      };

      reseed.proxy = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          URL for reseed proxy, supports http/socks.
        '';
      };

     addressbook.defaulturl = mkOption {
        type = types.str;
        default = "http://joajgazyztfssty4w2on5oaqksz6tqoxbduy553y34mf4byv6gpq.b32.i2p/export/alive-hosts.txt";
        description = ''
          AddressBook subscription URL for initial setup
        '';
      };
     addressbook.subscriptions = mkOption {
        type = with types; listOf str;
        default = [
          "http://inr.i2p/export/alive-hosts.txt"
          "http://i2p-projekt.i2p/hosts.txt"
          "http://stats.i2p/cgi-bin/newhosts.txt"
        ];
        description = ''
          AddressBook subscription URLs
        '';
      };

      trust.enable = mkEnableOption "Explicit trust options";

      trust.family = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Router Familiy to trust for first hops.
        '';
      };

      trust.routers = mkOption {
        type = with types; listOf str;
        default = [];
        description = ''
          Only connect to the listed routers.
        '';
      };

      trust.hidden = mkEnableOption "Router concealment";

      websocket = mkEndpointOpt "websockets" "127.0.0.1" 7666;

      exploratory.inbound = i2cpOpts "exploratory";
      exploratory.outbound = i2cpOpts "exploratory";

      ntcp2.enable = mkEnableTrueOption "NTCP2.";
      ntcp2.published = mkEnableOption "NTCP2 publication";
      ntcp2.port = mkOption {
        type = types.int;
        default = 0;
        description = ''
          Port to listen for incoming NTCP2 connections (0=auto).
        '';
      };

      limits.transittunnels = mkOption {
        type = types.int;
        default = 2500;
        description = ''
          Maximum number of active transit sessions.
        '';
      };

      limits.coreSize = mkOption {
        type = types.int;
        default = 0;
        description = ''
          Maximum size of corefile in Kb (0 - use system limit).
        '';
      };

      limits.openFiles = mkOption {
        type = types.int;
        default = 0;
        description = ''
          Maximum number of open files (0 - use system default).
        '';
      };

      limits.ntcpHard = mkOption {
        type = types.int;
        default = 0;
        description = ''
          Maximum number of active transit sessions.
        '';
      };

      limits.ntcpSoft = mkOption {
        type = types.int;
        default = 0;
        description = ''
          Threshold to start probabalistic backoff with ntcp sessions (default: use system limit).
        '';
      };

      limits.ntcpThreads = mkOption {
        type = types.int;
        default = 1;
        description = ''
          Maximum number of threads used by NTCP DH worker.
        '';
      };

      proto.http = (mkEndpointOpt "http" "127.0.0.1" 7070) // {

        auth = mkEnableOption "Webconsole authentication";

        user = mkOption {
          type = types.str;
          default = "i2pd";
          description = ''
            Username for webconsole access
          '';
        };

        pass = mkOption {
          type = types.str;
          default = "i2pd";
          description = ''
            Password for webconsole access.
          '';
        };

        strictHeaders = mkOption {
          type = with types; nullOr bool;
          default = null;
          description = ''
            Enable strict host checking on WebUI.
          '';
        };

        hostname = mkOption {
          type = with types; nullOr str;
          default = null;
          description = ''
            Expected hostname for WebUI.
          '';
        };
      };

      proto.httpProxy = (mkKeyedEndpointOpt "httpproxy" "127.0.0.1" 4444 "httpproxy-keys.dat")
      // {
        outproxy = mkOption {
          type = with types; nullOr str;
          default = null;
          description = "Upstream outproxy bind address.";
        };
      };
      proto.socksProxy = (mkKeyedEndpointOpt "socksproxy" "127.0.0.1" 4447 "socksproxy-keys.dat")
      // {
        outproxyEnable = mkEnableOption "SOCKS outproxy";
        outproxy = mkOption {
          type = types.str;
          default = "127.0.0.1";
          description = "Upstream outproxy bind address.";
        };
        outproxyPort = mkOption {
          type = types.int;
          default = 4444;
          description = "Upstream outproxy bind port.";
        };
      };

      proto.sam = mkEndpointOpt "sam" "127.0.0.1" 7656;
      proto.bob = mkEndpointOpt "bob" "127.0.0.1" 2827;
      proto.i2cp = mkEndpointOpt "i2cp" "127.0.0.1" 7654;
      proto.i2pControl = mkEndpointOpt "i2pcontrol" "127.0.0.1" 7650;

      outTunnels = mkOption {
        default = {};
        type = with types; loaOf (submodule (
          { name, ... }: {
            options = {
              destinationPort = mkOption {
                type = with types; nullOr int;
                default = null;
                description = "Connect to particular port at destination.";
              };
            } // commonTunOpts name;
            config = {
              name = mkDefault name;
            };
          }
        ));
        description = ''
          Connect to someone as a client and establish a local accept endpoint
        '';
      };

      inTunnels = mkOption {
        default = {};
        type = with types; loaOf (submodule (
          { name, ... }: {
            options = {
              inPort = mkOption {
                type = types.int;
                default = 0;
                description = "Service port. Default to the tunnel's listen port.";
              };
              accessList = mkOption {
                type = with types; listOf str;
                default = [];
                description = "I2P nodes that are allowed to connect to this service.";
              };
            } // commonTunOpts name;
            config = {
              name = mkDefault name;
            };
          }
        ));
        description = ''
          Serve something on I2P network at port and delegate requests to address inPort.
        '';
      };
    };
  };


  ###### implementation

  config = mkIf cfg.enable {

    users.users.i2pd = {
      group = "i2pd";
      description = "I2Pd User";
      home = homeDir;
      createHome = true;
      uid = config.ids.uids.i2pd;
    };

    users.groups.i2pd.gid = config.ids.gids.i2pd;

    systemd.services.i2pd = {
      description = "Minimal I2P router";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig =
      {
        User = "i2pd";
        WorkingDirectory = homeDir;
        Restart = "on-abort";
        ExecStart = "${i2pdSh}/bin/i2pd";
      };
    };
  };
}
