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

with lib;

let
  cfg = config.services.trafficserver;
  user = config.users.users.trafficserver.name;
  group = config.users.groups.trafficserver.name;

  getManualUrl = name: "https://docs.trafficserver.apache.org/en/latest/admin-guide/files/${name}.en.html";

  yaml = pkgs.formats.yaml { };

  mkYamlConf = name: cfg:
    if cfg != null then {
      "trafficserver/${name}.yaml".source = yaml.generate "${name}.yaml" cfg;
    } else {
      "trafficserver/${name}.yaml".text = "";
    };

  mkRecordLines = path: value:
    if isAttrs value then
      lib.mapAttrsToList (n: v: mkRecordLines (path ++ [ n ]) v) value
    else if isInt value then
      "CONFIG ${concatStringsSep "." path} INT ${toString value}"
    else if isFloat value then
      "CONFIG ${concatStringsSep "." path} FLOAT ${toString value}"
    else
      "CONFIG ${concatStringsSep "." path} STRING ${toString value}";

  mkRecordsConfig = cfg: concatStringsSep "\n" (flatten (mkRecordLines [ ] cfg));
  mkPluginConfig = cfg: concatStringsSep "\n" (map (p: "${p.path} ${p.arg}") cfg);
in
{
  options.services.trafficserver = {
    enable = mkEnableOption "Apache Traffic Server";

    cache = mkOption {
      type = types.lines;
      default = "";
      example = "dest_domain=example.com suffix=js action=never-cache";
      description = ''
        Caching rules that overrule the origin's caching policy.

        Consult the [upstream
        documentation](${getManualUrl "cache.config"}) for more details.
      '';
    };

    hosting = mkOption {
      type = types.lines;
      default = "";
      example = "domain=example.com volume=1";
      description = ''
        Partition the cache according to origin server or domain

        Consult the [
        upstream documentation](${getManualUrl "hosting.config"}) for more details.
      '';
    };

    ipAllow = mkOption {
      type = types.nullOr yaml.type;
      default = lib.importJSON ./ip_allow.json;
      defaultText = literalMD "upstream defaults";
      example = literalExpression ''
        {
          ip_allow = [{
            apply = "in";
            ip_addrs = "127.0.0.1";
            action = "allow";
            methods = "ALL";
          }];
        }
      '';
      description = ''
        Control client access to Traffic Server and Traffic Server connections
        to upstream servers.

        Consult the [upstream
        documentation](${getManualUrl "ip_allow.yaml"}) for more details.
      '';
    };

    logging = mkOption {
      type = types.nullOr yaml.type;
      default = lib.importJSON ./logging.json;
      defaultText = literalMD "upstream defaults";
      example = { };
      description = ''
        Configure logs.

        Consult the [upstream
        documentation](${getManualUrl "logging.yaml"}) for more details.
      '';
    };

    parent = mkOption {
      type = types.lines;
      default = "";
      example = ''
        dest_domain=. method=get parent="p1.example:8080; p2.example:8080" round_robin=true
      '';
      description = ''
        Identify the parent proxies used in an cache hierarchy.

        Consult the [upstream
        documentation](${getManualUrl "parent.config"}) for more details.
      '';
    };

    plugins = mkOption {
      default = [ ];

      description = ''
        Controls run-time loadable plugins available to Traffic Server, as
        well as their configuration.

        Consult the [upstream
        documentation](${getManualUrl "plugin.config"}) for more details.
      '';

      type = with types;
        listOf (submodule {
          options.path = mkOption {
            type = str;
            example = "xdebug.so";
            description = ''
              Path to plugin. The path can either be absolute, or relative to
              the plugin directory.
            '';
          };
          options.arg = mkOption {
            type = str;
            default = "";
            example = "--header=ATS-My-Debug";
            description = "arguments to pass to the plugin";
          };
        });
    };

    records = mkOption {
      type = with types;
        let valueType = (attrsOf (oneOf [ int float str valueType ])) // {
          description = "Traffic Server records value";
        };
        in
        valueType;
      default = { };
      example = { proxy.config.proxy_name = "my_server"; };
      description = ''
        List of configurable variables used by Traffic Server.

        Consult the [
        upstream documentation](${getManualUrl "records.config"}) for more details.
      '';
    };

    remap = mkOption {
      type = types.lines;
      default = "";
      example = "map http://from.example http://origin.example";
      description = ''
        URL remapping rules used by Traffic Server.

        Consult the [
        upstream documentation](${getManualUrl "remap.config"}) for more details.
      '';
    };

    splitDns = mkOption {
      type = types.lines;
      default = "";
      example = ''
        dest_domain=internal.corp.example named="255.255.255.255:212 255.255.255.254" def_domain=corp.example search_list="corp.example corp1.example"
        dest_domain=!internal.corp.example named=255.255.255.253
      '';
      description = ''
        Specify the DNS server that Traffic Server should use under specific
        conditions.

        Consult the [
        upstream documentation](${getManualUrl "splitdns.config"}) for more details.
      '';
    };

    sslMulticert = mkOption {
      type = types.lines;
      default = "";
      example = "dest_ip=* ssl_cert_name=default.pem";
      description = ''
        Configure SSL server certificates to terminate the SSL sessions.

        Consult the [
        upstream documentation](${getManualUrl "ssl_multicert.config"}) for more details.
      '';
    };

    sni = mkOption {
      type = types.nullOr yaml.type;
      default = null;
      example = literalExpression ''
        {
          sni = [{
            fqdn = "no-http2.example.com";
            https = "off";
          }];
        }
      '';
      description = ''
        Configure aspects of TLS connection handling for both inbound and
        outbound connections.

        Consult the [upstream
        documentation](${getManualUrl "sni.yaml"}) for more details.
      '';
    };

    storage = mkOption {
      type = types.lines;
      default = "/var/cache/trafficserver 256M";
      example = "/dev/disk/by-id/XXXXX volume=1";
      description = ''
        List all the storage that make up the Traffic Server cache.

        Consult the [
        upstream documentation](${getManualUrl "storage.config"}) for more details.
      '';
    };

    strategies = mkOption {
      type = types.nullOr yaml.type;
      default = null;
      description = ''
        Specify the next hop proxies used in an cache hierarchy and the
        algorithms used to select the next proxy.

        Consult the [
        upstream documentation](${getManualUrl "strategies.yaml"}) for more details.
      '';
    };

    volume = mkOption {
      type = types.nullOr yaml.type;
      default = "";
      example = "volume=1 scheme=http size=20%";
      description = ''
        Manage cache space more efficiently and restrict disk usage by
        creating cache volumes of different sizes.

        Consult the [
        upstream documentation](${getManualUrl "volume.config"}) for more details.
      '';
    };
  };

  config = mkIf cfg.enable {
    environment.etc = {
      "trafficserver/cache.config".text = cfg.cache;
      "trafficserver/hosting.config".text = cfg.hosting;
      "trafficserver/parent.config".text = cfg.parent;
      "trafficserver/plugin.config".text = mkPluginConfig cfg.plugins;
      "trafficserver/records.config".text = mkRecordsConfig cfg.records;
      "trafficserver/remap.config".text = cfg.remap;
      "trafficserver/splitdns.config".text = cfg.splitDns;
      "trafficserver/ssl_multicert.config".text = cfg.sslMulticert;
      "trafficserver/storage.config".text = cfg.storage;
      "trafficserver/volume.config".text = cfg.volume;
    } // (mkYamlConf "ip_allow" cfg.ipAllow)
    // (mkYamlConf "logging" cfg.logging)
    // (mkYamlConf "sni" cfg.sni)
    // (mkYamlConf "strategies" cfg.strategies);

    environment.systemPackages = [ pkgs.trafficserver ];
    systemd.packages = [ pkgs.trafficserver ];

    # Traffic Server does privilege handling independently of systemd, and
    # therefore should be started as root
    systemd.services.trafficserver = {
      enable = true;
      wantedBy = [ "multi-user.target" ];
    };

    # These directories can't be created by systemd because:
    #
    #   1. Traffic Servers starts as root and switches to an unprivileged user
    #      afterwards. The runtime directories defined below are assumed to be
    #      owned by that user.
    #   2. The bin/trafficserver script assumes these directories exist.
    systemd.tmpfiles.rules = [
      "d '/run/trafficserver' - ${user} ${group} - -"
      "d '/var/cache/trafficserver' - ${user} ${group} - -"
      "d '/var/lib/trafficserver' - ${user} ${group} - -"
      "d '/var/log/trafficserver' - ${user} ${group} - -"
    ];

    services.trafficserver = {
      records.proxy.config.admin.user_id = user;
      records.proxy.config.body_factory.template_sets_dir =
        "${pkgs.trafficserver}/etc/trafficserver/body_factory";
    };

    users.users.trafficserver = {
      description = "Apache Traffic Server";
      isSystemUser = true;
      inherit group;
    };
    users.groups.trafficserver = { };
  };
}
