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

with lib;
let
  hostname = config.instance.hostname;
  
  siteOpts = { ... }: with types; {
    options = {
      enableACME = mkOption {
        type = bool;
        description = "Use ACME to get SSL certificates for this site.";
        default = true;
      };
        
      site-config = mkOption {
        type = attrs;
        description = "Site-specific configuration.";
      };
    };
  };

  site-copy = site: "ejabberd-${site}";

  concatMapAttrs = f: attrs:
    foldr (a: b: a // b) {} (mapAttrs f attrs);

  concatMapAttrsToList = f: attr:
    attrValues (concatMapAttrs f attr);

  host-domains = config.fudo.acme.host-domains.${hostname};

  siteCerts = site: let
    certPath = config.fudo.acme.local-copies.${site-copy site}.path;
  in [
    "${certPath}/fullchain.pem"
    "${certPath}/privkey.pem"
    "${certPath}/chain.pem"
  ];

  siteCertService = site:
    config.fudo.acme.local-copies.${site-copy site}.service;

  config-file-template = let
    jabber-config = {
      loglevel = cfg.log-level;

      access_rules = {
        c2s = { allow = "all"; };
        announce = { allow = "admin"; };
        configure = { allow = "admin"; };
        pubsub_createnode = { allow = "local"; };
      };

      acl = {
        admin = {
          user = concatMap
            (admin: map (site: "${admin}@${site}")
              (attrNames cfg.sites))
            cfg.admins;
        };
      };

      hosts = attrNames cfg.sites;

      listen = [{
        port = cfg.port;
        module = "ejabberd_c2s";
        ip = cfg.listen-ip;
        starttls = true;
        starttls_required = true;
      }];

      certfiles = concatMapAttrsToList
        (site: siteOpts:
          if (siteOpts.enableACME) then
            (siteCerts site)
          else [])
        cfg.sites;

      host_config =
        mapAttrs (site: siteOpts: siteOpts.site-config)
          cfg.sites;
    };
    
    config-file = builtins.toJSON jabber-config;
  in pkgs.writeText "ejabberd.config.yml.template" config-file;

  enter-secrets = template: secrets: target: let
    secret-readers = concatStringsSep "\n"
      (mapAttrsToList
        (secret: file: "${secret}=$(cat ${file})")
        secrets);
    secret-swappers = map
      (secret: "sed s/${secret}/\$${secret}/g")
      (attrNames secrets);
    swapper = concatStringsSep " | " secret-swappers;
  in pkgs.writeShellScript "ejabberd-generate-config.sh" ''
    cat ${template} | ${swapper} > ${target}
    chown ${cfg.user}:${cfg.group} ${target}
  '';

  cfg = config.fudo.jabber;
  
in {
  options.fudo.jabber = with types; {
    enable = mkEnableOption "Enable ejabberd server.";
    
    port = mkOption {
      type = port;
      description = "Port on which to listen for Jabber connections.";
      default = 5222;
    };

    user = mkOption {
      type = str;
      description = "User as which to run the ejabberd server.";
      default = "ejabberd";
    };

    group = mkOption {
      type = str;
      description = "Group as which to run the ejabberd server.";
      default = "ejabberd";
    };

    admins = mkOption {
      type = str;
      description = "List of admin users for the server.";
      default = [];
    };

    sites = mkOption {
      type = attrsOf (submodule siteOpts);
      description = "List of sites on which to listen for Jabber connections.";
    };

    secret-files = mkOption {
      type = attrsOf str;
      description = "Map of secret-name to file. File contents will be subbed for the name in the config.";
      default = {};
    };

    config-file = mkOption {
      type = str;
      description = "Location at which to generate the configuration file.";
      default = "/var/run/ejabberd/ejabberd.yaml";
    };
  };
  
  config = mkIf cfg.enable {
    users = {
      users.${cfg.user} = {
        isSystemUser = true;
      };

      groups.${cfg.group} = {
        members = [ cfg.user ];
      };
    };

    fudo.acme.local-copies = mapAttrs' (site: siteCfg:
      nameValuePair (site-copy site)
        mkif siteCfg.enableACME {
          domain = site;
          user = cfg.user;
          group = cfg.group;
        }) cfg.sites;

    systemd = {
      tmpfiles.rules = [
        "D '${dirOf cfg.config-file}' 0550 ${cfg.user} ${cfg.group} - -"
      ];
      
      services.ejabberd = let
        config-generator = enter-secrets config-file-template cfg.secret-files cfg.config-file;
      in {
        wants = map (site: siteCertService site) (attrNames cfg.sites);
        environment = cfg.secret-files;
        serviceConfig = {
          ExecStartPre = mkAfter "${config-generator}";
        };
      };
    };
    
    services.ejabberd = {
      enable = true;

      user = cfg.user;
      group = cfg.group;

      configFile = cfg.config-file;
    };
  };
}