{
  clanLib,
  ...
}:
{
  _class = "clan.service";
  manifest.name = "phonebox";
  manifest.description = "A peer to peer phone relay network built on top of yggdrasil.";
  manifest.readme = "A peer to peer phone relay network built on top of yggdrasil.";
  manifest.categories = [ "System" ];

  roles.default = {
    description = "a default server role";
    interface =
      { lib, ... }:
      {
        options.ata-ethernet-iface = lib.mkOption {
          type = lib.types.str;
          description = "An Ethernet interface that connect to ATA box.";
          default = "enp2s0";
        };
        options.ownerName = lib.mkOption {
          type = lib.types.str;
          description = "";
          default = "";
        };
      };
    perInstance =
      {
        roles,
        settings,
        ...
      }:
      {

        nixosModule =
          {
            lib,
            config,
            pkgs,
            ...
          }:
          let
            asterisk = pkgs.asterisk.overrideAttrs (old: {
              propagatedNativeBuildInputs = [ pkgs.spandsp3 ];
            });

            machines = lib.attrNames roles.default.machines;

            user = "asterisk";
            faxDir = "/run/asterisk/fax";
            rtpPortFrom = 10000;
            rtpPortTo = 20000;
            ata-interface = settings.ata-ethernet-iface;

            contactList = builtins.map (machineName: {
              name = "${clanLib.getPublicValue {
                flake = config.clan.core.settings.directory;
                machine = machineName;
                generator = "phonebox";
                file = "owner-name";
                default = null;
              }}";
              number = "${
                clanLib.getPublicValue {
                  flake = config.clan.core.settings.directory;
                  machine = machineName;
                  generator = "phonebox";
                  file = "server-prefix-number";
                  default = null;
                }
              }${
                clanLib.getPublicValue {
                  flake = config.clan.core.settings.directory;
                  machine = machineName;
                  generator = "phonebox";
                  file = "ata-local-number";
                  default = null;
                }
              }";
            }) machines;

            createContactListTiff =
              let
                contactTXT = lib.concatStringsSep "\n" (
                  builtins.map (contact: "${contact.number}\t\t: \t\t${contact.name}") contactList
                );
              in
              pkgs.writeShellApplication {
                name = "create-contact-tiff";
                text = ''
                  magick -background white -fill black -pointsize 20 -font DejaVu-Sans label:"${contactTXT}" "$1"
                  magick "$1" -border 20x50 -bordercolor white "$1"
                  magick "$1" -resize 1728x -units PixelsPerInch -compress Group4 -density 204x196 -monochrome -depth 1 "$1"
                '';
                runtimeInputs = [ pkgs.imagemagick ];
              };

            genServerSIPEndpoint =
              { hostname, address }:
              ''
                [${hostname}](internal_endpoint)
                aors=${hostname}

                [${hostname}](ip_auth)
                endpoint=${hostname}
                match=[${address}]

                [${hostname}](dynamiic_aor)
                contact=sip:[${address}]
              '';

            genLocalSIPEndpoint =
              { localNumber }:
              ''
                [${localNumber}](internal_endpoint)
                aors=${localNumber}
                auth=${localNumber}

                [${localNumber}](userpass_auth)
                username=${localNumber}
                password=${localNumber}

                [${localNumber}](dynamiic_aor)
                max_contacts=1
                remove_existing=yes
              '';

            genLocalExtenConf =
              { localNumber }:
              ''
                exten => ${localNumber},1,Dial(PJSIP/${localNumber},20)
              '';
            genExtentConf =
              {
                prefixNumber,
                hostname,
                localNumber,
              }:
              let
                replaceWithX =
                  ln: builtins.concatStringsSep "" (builtins.genList (_: "X") (builtins.stringLength ln));
              in
              ''
                exten => _${prefixNumber}${replaceWithX localNumber},1,Dial(PJSIP/''${EXTEN:${builtins.toString (builtins.stringLength prefixNumber)}}@${hostname},30)
              '';

            getYggdrasilIP =
              machineName:
              if config.clan.core.vars.generators.yggdrasil.files.address ? value then
                clanLib.getPublicValue {
                  flake = config.clan.core.settings.directory;
                  machine = machineName;
                  generator = "yggdrasil";
                  file = "address";
                  default = null;
                }
              else
                throw "clanService/yggdrasil is required";
          in
          {
            clan.core.vars.generators.phonebox = builtins.break {
              files = {
                server-prefix-number.secret = false;
                ata-local-number.secret = false;
                owner-name.secret = false;
              };

              prompts = {
                server-prefix-number = {
                  type = "line";
                  persist = true;
                  description = "Server prefix number: indicate server to connect to [10XX]";
                };
                ata-local-number = {
                  persist = true;
                  type = "line";
                  description = "Local suffix number: indicate local number on the server [XX00]";
                };
                owner-name = {
                  persist = true;
                  type = "line";
                  description = "The owner's name for this unit";
                };
              };

              script = ''
                cat $prompts/server-prefix-number > $out/server-prefix-number
                cat $prompts/ata-local-number > $out/ata-local-number
                cat $prompts/owner-name > $out/owner-name
              '';
            };

            networking.interfaces = {
              ${ata-interface} = {
                useDHCP = false;
                ipv4.addresses = [
                  {
                    address = "192.168.254.1";
                    prefixLength = 24;
                  }
                ];
              };
            };

            services.dnsmasq = {
              enable = true;

              settings = {
                bind-dynamic = true;
                listen-address = "192.168.254.1";
                # enable-ra = true;
                domain-needed = true;
                domain = "localhost";
                dhcp-range = [
                  "192.168.254.100,192.168.254.100,255.255.255.0,3m"
                ];
                dhcp-leasefile = "/dev/null";
                dhcp-option = [
                  "3,192.168.254.1"
                ];
                interface = [ ata-interface ];
              };
            };

            services.nginx = {
              enable = true;
              virtualHosts = {
                "_" = {
                  locations."/" = {
                    proxyPass = "http://192.168.254.100";
                    extraConfig = ''
                      client_max_body_size 100M;
                    '';
                  };
                };
              };
            };

            networking.firewall.allowedUDPPortRanges = [
              {
                from = rtpPortFrom;
                to = rtpPortTo;
              }
            ];

            networking.firewall.allowedUDPPorts = [
              53
              67
              5060
            ];
            networking.firewall.allowedTCPPorts = [
              53
            ];
            networking.firewall.interfaces =
              let
                matchAll = if !config.networking.nftables.enable then "zt+" else "zt*";
              in
              {
                "${matchAll}".allowedTCPPorts = [ 80 ];
              };

            services.asterisk = {
              enable = lib.mkDefault true;
              package = lib.mkDefault asterisk;
              confFiles =
                let
                  nodes = builtins.foldl' (
                    nodes: name:
                    nodes
                    ++ [
                      {
                        hostname = name;
                        address = getYggdrasilIP name;
                        prefixNumber = clanLib.getPublicValue {
                          flake = config.clan.core.settings.directory;
                          machine = name;
                          generator = "phonebox";
                          file = "server-prefix-number";
                          default = null;
                        };

                        localNumber = clanLib.getPublicValue {
                          flake = config.clan.core.settings.directory;
                          machine = name;
                          generator = "phonebox";
                          file = "ata-local-number";
                          default = null;
                        };
                      }
                    ]
                  ) [ ] machines;
                in
                {
                  "logger.conf" = ''
                    [general]
                    dateformat = %F %T.%3q ; ISO 8601 date format with milliseconds
                    use_callids = yes
                    appendhostname = no
                    queue_log = yes
                    queue_log_to_file = no
                    queue_log_name = queue_log
                    queue_log_realtime_use_gmt = no
                    rotatestrategy = rotate
                    exec_after_rotate=gzip -9 $\{filename\}.2
                    [logfiles]
                    console => notice,warning,error
                    security => security
                    messages => notice,warning,error
                    full => notice,warning,error,verbose,dtmf,fax
                    syslog.local0 => notice,warning,error
                  '';

                  "modules.conf" = ''
                    [modules]
                    autoload=yes

                    load => res_fax_spandsp.so
                  '';

                  # Dial plan config
                  "extensions.conf" =
                    let
                      serverConf = builtins.foldl' (
                        config: node:
                        config
                        + (genExtentConf {
                          inherit (node) prefixNumber hostname localNumber;
                        })
                      ) "" nodes;
                    in
                    ''
                      [from-internal]
                      exten => 999,1,Answer()
                      same  =>     n,Playback(hello-world)
                      same  =>     n,Hangup()

                      exten => 000,1,Answer()
                      same => n,ReceiveFAX(${faxDir}/echo-''${UNIQUEID}.tiff)
                      same => n,Set(FAXFILE=${faxDir}/echo-''${UNIQUEID}.tiff)
                      same => n,Set(FAXECHO=true)

                      exten => 888,1,Answer()
                      same => n,Set(FAXFILE=${faxDir}/contact.tiff)
                      same => n,System(${lib.getExe createContactListTiff} ''${FAXFILE})
                      same => n,Set(FAXECHO=true)
                      same => n,Playback(vm-goodbye)
                      same => n,Wait(3)

                      exten => h,1,GotoIf($[''${FAXECHO}]?sendfax)
                      same => n,Hangup()
                      same => n(sendfax),Originate(PJSIP/00,app,SendFAX,''${FAXFILE})
                      same => n,Set(FAXECHO=false)

                    ''
                    + (genLocalExtenConf {
                      localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value;
                    })
                    + serverConf;

                  "rtp.conf" = ''
                    [general]
                    rtpstart=${builtins.toString rtpPortFrom}
                    rtpend=${builtins.toString rtpPortTo}
                  '';

                  "pjsip.conf" =
                    let
                      serverConf = builtins.foldl' (
                        conf: node:
                        conf
                        + (genServerSIPEndpoint {
                          hostname = node.hostname;
                          address = node.address;
                        })
                      ) "" nodes;
                    in
                    ''
                      [transport-udp]
                      type=transport
                      protocol=udp
                      bind=0.0.0.0
                      [transport-udp6]
                      type=transport
                      protocol=udp
                      bind=::

                      [base_endpoint](!)
                      type=endpoint
                      disallow=all
                      allow=ulaw,alaw,g722,gsm
                      direct_media=no

                      [internal_endpoint](!,base_endpoint)
                      context=from-internal

                      [userpass_auth](!)
                      type=auth
                      auth_type=userpass

                      [ip_auth](!)
                      type=identify
                      endpoint=external

                      [dynamiic_aor](!)
                      type=aor

                    ''
                    + (genLocalSIPEndpoint {
                      localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value;
                    })
                    + serverConf;
                };
            };

            environment.systemPackages = [
              createContactListTiff
            ];

            systemd.tmpfiles.rules = [
              "d ${faxDir}                            0755 ${user} ${user} - -"
            ];

            systemd.services.asterisk-watcher = {
              enable = true;
              description = "Asterisk Configuration files watcher";

              requires = [ "asterisk.service" ];
              after = [ "network.target" ];
              wantedBy = [ "multi-user.target" ];
              path = with pkgs; [
                inotify-tools
                asterisk
              ];
              script = ''
                inotifywait -m -e move /etc/asterisk |
                while read path action file; do
                  case "$file" in
                    pjsip.conf)
                      echo "restarting pjsip"
                      asterisk -rx "pjsip reload"
                      ;;
                  esac
                  case "$file" in
                    extensions.conf)
                      echo "restarting core"
                      asterisk -rx "core restart now"
                      ;;
                  esac
                done
              '';
            };
          };
      };
  };
}
