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

with lib;
let
  cfg = config.informis.cl-gemini;

  lisp-helper = import ../lisp.nix { inherit pkgs; };

  feedOpts = { ... }: with types; {
    options = {
      url = mkOption {
        type = str;
        description = "Base URI of the feed, i.e. the URI corresponding to the feed path.";
        example = "gemini://my.server/path/to/feedfiles";
      };

      title = mkOption {
        type = str;
        description = "Title of given feed.";
        example = "My Fancy Feed";
      };

      path = mkOption {
        type = str;
        description = "Path to Gemini files making up the feed.";
        example = "/path/to/feed";
      };
    };
  };

  ensure-certificates = hostname: user: key: cert: pkgs.writeShellScript "ensure-gemini-certificates.sh" ''
    if [[ ! -e ${key} ]]; then
      TARGET_CERT_DIR=$(${pkgs.coreutils}/bin/dirname ${cert})
      TARGET_KEY_DIR=$(${pkgs.coreutils}/bin/dirname ${key})
      if [[ ! -d $TARGET_CERT_DIR ]]; then mkdir -p $TARGET_CERT_DIR; fi
      if [[ ! -d $TARGET_KEY_DIR ]]; then mkdir -p $TARGET_KEY_DIR; fi
      ${pkgs.openssl}/bin/openssl req -new -subj "/CN=.${hostname}" -addext "subjectAltName = DNS:${hostname}, DNS:.${hostname}" -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 3650 -nodes -out ${cert} -keyout ${key}
      ${pkgs.coreutils}/bin/chown -R ${user}:nogroup ${cert}
      ${pkgs.coreutils}/bin/chown -R ${user}:nogroup ${key}
      ${pkgs.coreutils}/bin/chmod 0444 ${cert}
      ${pkgs.coreutils}/bin/chmod 0400 ${key}
    fi
  '';

  generate-feeds = feeds:
    let
      feed-strings = mapAttrsToList (feed-name: opts:
        "(cl-gemini:register-feed :name \"${feed-name}\" :title \"${opts.title}\" :path \"${opts.path}\" :base-uri \"${opts.url}\")") feeds;
    in pkgs.writeText "gemini-local-feeds.lisp" (concatStringsSep "\n" feed-strings);

in {
  options.informis.cl-gemini = with types; {
    enable = mkEnableOption "Enable the cl-gemini server.";

    port = mkOption {
      type = port;
      description = "Port on which to serve Gemini traffic.";
      default = 1965;
    };

    hostname = mkOption {
      type = str;
      description = "Hostname at which the server is available (for generating the SSL certificate).";
      example = "my.hostname.com";
    };

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

    server-ip = mkOption {
      type = str;
      description = "IP on which to serve Gemini traffic.";
      example = "1.2.3.4";
    };

    document-root = mkOption {
      type = str;
      description = "Root at which to look for gemini files.";
      example = "/my/gemini/root";
    };

    user-public = mkOption {
      type = str;
      description = "Subdirectory of user homes to check for gemini files.";
      default = "gemini-public";
    };

    ssl-private-key = mkOption {
      type = str;
      description = "Path to the pem-encoded server private key.";
      example = "/path/to/secret/key.pem";
      default = "${config.users.users.cl-gemini.home}/private/server-key.pem";
    };

    ssl-certificate = mkOption {
      type = str;
      description = "Path to the pem-encoded server public certificate.";
      example = "/path/to/cert.pem";
      default = "${config.users.users.cl-gemini.home}/private/server-cert.pem";
    };

    slynk-port = mkOption {
      type = nullOr port;
      description = "Port on which to open a slynk server, if any.";
      default = null;
    };

    feeds = mkOption {
      type = attrsOf (submodule feedOpts);
      description = "Feeds to generate and make available (as eg. /feed/name.xml).";
      example = {
        diary = {
          title = "My Diary";
          path = "/path/to/my/gemfiles/";
          url = "gemini://my.host/blog-path/";
        };
      };
      default = {};
    };

    textfiles-archive = mkOption {
      type = str;
      description = "A path containing only gemini & text files.";
      example = "/path/to/textfiles/";
    };
  };

  config = mkIf cfg.enable {

    networking.firewall.allowedTCPPorts = [ cfg.port ];

    users.users = {
      ${cfg.user} = {
        isSystemUser = true;
        group = "nogroup";
        createHome = true;
        home = "/var/lib/${cfg.user}";
      };
    };

    systemd.services = {
      cl-gemini = {
        description = "cl-gemini Gemini server (https://gemini.circumlunar.space/)";

        serviceConfig = {
          ExecStartPre = "${ensure-certificates cfg.hostname cfg.user cfg.ssl-private-key cfg.ssl-certificate}";
          ExecStart = "${pkgs.cl-gemini}/bin/launch-server.sh";
          Restart = "on-failure";
          PIDFile = "/run/cl-gemini.$USERNAME.uid";
          User = cfg.user;
        };

        environment = {
          GEMINI_SLYNK_PORT = mkIf (cfg.slynk-port != null) (toString cfg.slynk-port);
          GEMINI_LISTEN_IP = cfg.server-ip;
          GEMINI_PRIVATE_KEY = cfg.ssl-private-key;
          GEMINI_CERTIFICATE = cfg.ssl-certificate;
          GEMINI_LISTEN_PORT = toString cfg.port;
          GEMINI_DOCUMENT_ROOT = cfg.document-root;
          GEMINI_TEXTFILES_ROOT = cfg.textfiles-archive;
          GEMINI_FEEDS = "${generate-feeds cfg.feeds}";

          CL_SOURCE_REGISTRY = "${lisp-helper.lisp-source-registry pkgs.cl-gemini}";
        };

        path = with pkgs; [
          gcc
          file
          getent
        ];

        wantedBy = [ "multi-user.target" ];
      };
    };
  };
}