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

let
  cfg = config.services.librenms;
  settingsFormat = pkgs.formats.json { };
  configJson = settingsFormat.generate "librenms-config.json" cfg.settings;

  package = pkgs.librenms.override {
    logDir = cfg.logDir;
    dataDir = cfg.dataDir;
  };

  phpOptions = ''
    log_errors = on
    post_max_size = 100M
    upload_max_filesize = 100M
    date.timezone = "${config.time.timeZone}"
  '';
  phpIni = pkgs.runCommand "php.ini"
    {
      inherit (package) phpPackage;
      inherit phpOptions;
      preferLocalBuild = true;
      passAsFile = [ "phpOptions" ];
    } ''
    cat $phpPackage/etc/php.ini $phpOptionsPath > $out
  '';

  artisanWrapper = pkgs.writeShellScriptBin "librenms-artisan" ''
    cd ${package}
    sudo=exec
    if [[ "$USER" != ${cfg.user} ]]; then
      sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
    fi
    $sudo ${package}/artisan "$@"
  '';

  lnmsWrapper = pkgs.writeShellScriptBin "lnms" ''
    cd ${package}
    sudo=exec
    if [[ "$USER" != ${cfg.user} ]]; then
    sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
    fi
    $sudo ${package}/lnms "$@"
  '';



  configFile = pkgs.writeText "config.php" ''
    <?php
    $new_config = json_decode(file_get_contents("${cfg.dataDir}/config.json"), true);
    $config = ($config == null) ? $new_config : array_merge($config, $new_config);

    ${lib.optionalString (cfg.extraConfig != null) cfg.extraConfig}
  '';

in
{
  options.services.librenms = with lib; {
    enable = mkEnableOption "LibreNMS network monitoring system";

    user = mkOption {
      type = types.str;
      default = "librenms";
      description = ''
        Name of the LibreNMS user.
      '';
    };

    group = mkOption {
      type = types.str;
      default = "librenms";
      description = ''
        Name of the LibreNMS group.
      '';
    };

    hostname = mkOption {
      type = types.str;
      default = config.networking.fqdnOrHostName;
      defaultText = literalExpression "config.networking.fqdnOrHostName";
      description = ''
        The hostname to serve LibreNMS on.
      '';
    };

    pollerThreads = mkOption {
      type = types.int;
      default = 16;
      description = ''
        Amount of threads of the cron-poller.
      '';
    };

    enableOneMinutePolling = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Enables the [1-Minute Polling](https://docs.librenms.org/Support/1-Minute-Polling/).
        Changing this option will automatically convert your existing rrd files.
      '';
    };

    useDistributedPollers = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Enables (distributed pollers)[https://docs.librenms.org/Extensions/Distributed-Poller/]
        for this LibreNMS instance. This will enable a local `rrdcached` and `memcached` server.

        To use this feature, make sure to configure your firewall that the distributed pollers
        can reach the local `mysql`, `rrdcached` and `memcached` ports.
      '';
    };

    distributedPoller = {
      enable = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Configure this LibreNMS instance as a (distributed poller)[https://docs.librenms.org/Extensions/Distributed-Poller/].
          This will disable all web features and just configure the poller features.
          Use the `mysql` database of your main LibreNMS instance in the database settings.
        '';
      };

      name = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = ''
          Custom name of this poller.
        '';
      };

      group = mkOption {
        type = types.str;
        default = "0";
        example = "1,2";
        description = ''
          Group(s) of this poller.
        '';
      };

      distributedBilling = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Enable distributed billing on this poller.
        '';
      };

      memcachedHost = mkOption {
        type = types.str;
        description = ''
          Hostname or IP of the `memcached` server.
        '';
      };

      memcachedPort = mkOption {
        type = types.port;
        default = 11211;
        description = ''
          Port of the `memcached` server.
        '';
      };

      rrdcachedHost = mkOption {
        type = types.str;
        description = ''
          Hostname or IP of the `rrdcached` server.
        '';
      };

      rrdcachedPort = mkOption {
        type = types.port;
        default = 42217;
        description = ''
          Port of the `memcached` server.
        '';
      };
    };

    poolConfig = mkOption {
      type = with types; attrsOf (oneOf [ str int bool ]);
      default = {
        "pm" = "dynamic";
        "pm.max_children" = 32;
        "pm.start_servers" = 2;
        "pm.min_spare_servers" = 2;
        "pm.max_spare_servers" = 4;
        "pm.max_requests" = 500;
      };
      description = ''
        Options for the LibreNMS PHP pool. See the documentation on `php-fpm.conf`
        for details on configuration directives.
      '';
    };

    nginx = mkOption {
      type = types.submodule (
        recursiveUpdate
          (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
          { }
      );
      default = { };
      example = literalExpression ''
        {
          serverAliases = [
            "librenms.''${config.networking.domain}"
          ];
          # To enable encryption and let let's encrypt take care of certificate
          forceSSL = true;
          enableACME = true;
          # To set the LibreNMS virtualHost as the default virtualHost;
          default = true;
        }
      '';
      description = ''
        With this option, you can customize the nginx virtualHost settings.
      '';
    };

    dataDir = mkOption {
      type = types.path;
      default = "/var/lib/librenms";
      description = ''
        Path of the LibreNMS state directory.
      '';
    };

    logDir = mkOption {
      type = types.path;
      default = "/var/log/librenms";
      description = ''
        Path of the LibreNMS logging directory.
      '';
    };

    database = {
      createLocally = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Whether to create a local database automatically.
        '';
      };

      host = mkOption {
        default = "localhost";
        description = ''
          Hostname or IP of the MySQL/MariaDB server.
          Ignored if 'socket' is defined.
        '';
      };

      port = mkOption {
        type = types.port;
        default = 3306;
        description = ''
          Port of the MySQL/MariaDB server.
          Ignored if 'socket' is defined.
        '';
      };

      database = mkOption {
        type = types.str;
        default = "librenms";
        description = ''
          Name of the database on the MySQL/MariaDB server.
        '';
      };

      username = mkOption {
        type = types.str;
        default = "librenms";
        description = ''
          Name of the user on the MySQL/MariaDB server.
          Ignored if 'socket' is defined.
        '';
      };

      passwordFile = mkOption {
        type = types.nullOr types.path;
        default = null;
        example = "/run/secrets/mysql.pass";
        description = ''
          A file containing the password for the user of the MySQL/MariaDB server.
          Must be readable for the LibreNMS user.
          Ignored if 'socket' is defined, mandatory otherwise.
        '';
      };

      socket = mkOption {
        type = types.nullOr types.str;
        default = null;
        example = "/run/mysqld/mysqld.sock";
        description = ''
          A unix socket to mysql, accessible by the librenms user.
          Useful when mysql is on the localhost.
        '';
      };
    };

    environmentFile = mkOption {
      type = types.nullOr types.str;
      default = null;
      description = ''
        File containing env-vars to be substituted into the final config. Useful for secrets.
        Does not apply to settings defined in `extraConfig`.
      '';
    };

    settings = mkOption {
      type = types.submodule {
        freeformType = settingsFormat.type;
        options = { };
      };
      description = ''
        Attrset of the LibreNMS configuration.
        See https://docs.librenms.org/Support/Configuration/ for reference.
        All possible options are listed [here](https://github.com/librenms/librenms/blob/master/misc/config_definitions.json).
        See https://docs.librenms.org/Extensions/Authentication/ for setting other authentication methods.
      '';
      default = { };
      example = {
        base_url = "/librenms/";
        top_devices = true;
        top_ports = false;
      };
    };

    extraConfig = mkOption {
      type = types.nullOr types.str;
      default = null;
      description = ''
        Additional config for LibreNMS that will be appended to the `config.php`. See
        https://github.com/librenms/librenms/blob/master/misc/config_definitions.json
        for possible options. Useful if you want to use PHP-Functions in your config.
      '';
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = config.time.timeZone != null;
        message = "You must set `time.timeZone` to use the LibreNMS module.";
      }
      {
        assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
        message = "The database host must be \"localhost\" if services.librenms.database.createLocally is set to true.";
      }
      {
        assertion = !(cfg.useDistributedPollers && cfg.distributedPoller.enable);
        message = "The LibreNMS instance can't be a distributed poller and a full instance at the same time.";
      }
    ];

    users.users.${cfg.user} = {
      group = "${cfg.group}";
      isSystemUser = true;
    };

    users.groups.${cfg.group} = { };

    services.librenms.settings = {
      # basic configs
      "user" = cfg.user;
      "own_hostname" = cfg.hostname;
      "base_url" = lib.mkDefault "/";
      "auth_mechanism" = lib.mkDefault "mysql";

      # disable auto update function (won't work with NixOS)
      "update" = false;

      # enable fast ping by default
      "ping_rrd_step" = 60;

      # one minute polling
      "rrd.step" = if cfg.enableOneMinutePolling then 60 else 300;
      "rrd.heartbeat" = if cfg.enableOneMinutePolling then 120 else 600;
    } // (lib.optionalAttrs cfg.distributedPoller.enable {
      "distributed_poller" = true;
      "distributed_poller_name" = lib.mkIf (cfg.distributedPoller.name != null) cfg.distributedPoller.name;
      "distributed_poller_group" = cfg.distributedPoller.group;
      "distributed_billing" = cfg.distributedPoller.distributedBilling;
      "distributed_poller_memcached_host" = cfg.distributedPoller.memcachedHost;
      "distributed_poller_memcached_port" = cfg.distributedPoller.memcachedPort;
      "rrdcached" = "${cfg.distributedPoller.rrdcachedHost}:${toString cfg.distributedPoller.rrdcachedPort}";
    }) // (lib.optionalAttrs cfg.useDistributedPollers {
      "distributed_poller" = true;
      # still enable a local poller with distributed polling
      "distributed_poller_group" = lib.mkDefault "0";
      "distributed_billing" = lib.mkDefault true;
      "distributed_poller_memcached_host" = "localhost";
      "distributed_poller_memcached_port" = 11211;
      "rrdcached" = "localhost:42217";
    });

    services.memcached = lib.mkIf cfg.useDistributedPollers {
      enable = true;
      listen = "0.0.0.0";
    };

    systemd.services.rrdcached = lib.mkIf cfg.useDistributedPollers {
      description = "rrdcached";
      after = [ "librenms-setup.service" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        Type = "forking";
        User = cfg.user;
        Group = cfg.group;
        LimitNOFILE = 16384;
        RuntimeDirectory = "rrdcached";
        PidFile = "/run/rrdcached/rrdcached.pid";
        # rrdcached params from https://docs.librenms.org/Extensions/Distributed-Poller/#config-sample
        ExecStart = "${pkgs.rrdtool}/bin/rrdcached -l 0:42217 -R -j ${cfg.dataDir}/rrdcached-journal/ -F -b ${cfg.dataDir}/rrd -B -w 1800 -z 900 -p /run/rrdcached/rrdcached.pid";
      };
    };

    services.mysql = lib.mkIf cfg.database.createLocally {
      enable = true;
      package = lib.mkDefault pkgs.mariadb;
      settings.mysqld = {
        innodb_file_per_table = 1;
        lower_case_table_names = 0;
      } // (lib.optionalAttrs cfg.useDistributedPollers {
        bind-address = "0.0.0.0";
      });
      ensureDatabases = [ cfg.database.database ];
      ensureUsers = [
        {
          name = cfg.database.username;
          ensurePermissions = {
            "${cfg.database.database}.*" = "ALL PRIVILEGES";
          };
        }
      ];
      initialScript = lib.mkIf cfg.useDistributedPollers (pkgs.writeText "mysql-librenms-init" ''
        CREATE USER IF NOT EXISTS '${cfg.database.username}'@'%';
        GRANT ALL PRIVILEGES ON ${cfg.database.database}.* TO '${cfg.database.username}'@'%';
      '');
    };

    services.nginx = lib.mkIf (!cfg.distributedPoller.enable) {
      enable = true;
      virtualHosts."${cfg.hostname}" = lib.mkMerge [
        cfg.nginx
        {
          root = lib.mkForce "${package}/html";
          locations."/" = {
            index = "index.php";
            tryFiles = "$uri $uri/ /index.php?$query_string";
          };
          locations."~ .php$".extraConfig = ''
            fastcgi_pass unix:${config.services.phpfpm.pools."librenms".socket};
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
          '';
        }
      ];
    };

    services.phpfpm.pools.librenms = lib.mkIf (!cfg.distributedPoller.enable) {
      user = cfg.user;
      group = cfg.group;
      inherit (package) phpPackage;
      inherit phpOptions;
      settings = {
        "listen.mode" = "0660";
        "listen.owner" = config.services.nginx.user;
        "listen.group" = config.services.nginx.group;
      } // cfg.poolConfig;
    };

    systemd.services.librenms-scheduler = {
      description = "LibreNMS Scheduler";
      path = [ pkgs.unixtools.whereis ];
      serviceConfig = {
        Type = "oneshot";
        WorkingDirectory = package;
        User = cfg.user;
        Group = cfg.group;
        ExecStart = "${artisanWrapper}/bin/librenms-artisan schedule:run";
      };
    };

    systemd.timers.librenms-scheduler = {
      description = "LibreNMS Scheduler";
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnCalendar = "minutely";
        AccuracySec = "1second";
      };
    };

    systemd.services.librenms-setup = {
      description = "Preparation tasks for LibreNMS";
      before = [ "phpfpm-librenms.service" ];
      after = [ "systemd-tmpfiles-setup.service" ]
        ++ (lib.optional (cfg.database.host == "localhost") "mysql.service");
      wantedBy = [ "multi-user.target" ];
      restartTriggers = [ package configFile ];
      path = [ pkgs.mariadb pkgs.unixtools.whereis pkgs.gnused ];
      serviceConfig = {
        Type = "oneshot";
        RemainAfterExit = true;
        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
        User = cfg.user;
        Group = cfg.group;
        ExecStartPre = lib.mkIf cfg.database.createLocally [
          "!${pkgs.writeShellScript "librenms-db-init" ''
          DB_PASSWORD=$(cat ${cfg.database.passwordFile} | tr -d '\n')
          echo "ALTER USER '${cfg.database.username}'@'localhost' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql
          ${lib.optionalString cfg.useDistributedPollers ''
            echo "ALTER USER '${cfg.database.username}'@'%' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql
          ''}
        ''}"
        ];
      };
      script = ''
        set -euo pipefail

        # config setup
        ln -sf ${configFile} ${cfg.dataDir}/config.php
        ${pkgs.envsubst}/bin/envsubst -i ${configJson} -o ${cfg.dataDir}/config.json
        export PHPRC=${phpIni}

        if [[ ! -s ${cfg.dataDir}/.env ]]; then
          # init .env file
          echo "APP_KEY=" > ${cfg.dataDir}/.env
          ${artisanWrapper}/bin/librenms-artisan key:generate --ansi
          ${artisanWrapper}/bin/librenms-artisan webpush:vapid
          echo "" >> ${cfg.dataDir}/.env
          echo -n "NODE_ID=" >> ${cfg.dataDir}/.env
          ${package.phpPackage}/bin/php -r "echo uniqid();" >> ${cfg.dataDir}/.env
          echo "" >> ${cfg.dataDir}/.env
        else
          # .env file already exists --> only update database and cache config
          ${pkgs.gnused}/bin/sed -i /^DB_/d ${cfg.dataDir}/.env
          ${pkgs.gnused}/bin/sed -i /^CACHE_DRIVER/d ${cfg.dataDir}/.env
        fi
        ${lib.optionalString (cfg.useDistributedPollers || cfg.distributedPoller.enable) ''
          echo "CACHE_DRIVER=memcached" >> ${cfg.dataDir}/.env
        ''}
        echo "DB_DATABASE=${cfg.database.database}" >> ${cfg.dataDir}/.env
      ''
      + (
        if ! isNull cfg.database.socket
        then ''
          # use socket connection
          echo "DB_SOCKET=${cfg.database.socket}" >> ${cfg.dataDir}/.env
        ''
        else ''
          # use TCP connection
          echo "DB_HOST=${cfg.database.host}" >> ${cfg.dataDir}/.env
          echo "DB_PORT=${toString cfg.database.port}" >> ${cfg.dataDir}/.env
          echo "DB_USERNAME=${cfg.database.username}" >> ${cfg.dataDir}/.env
          echo -n "DB_PASSWORD=" >> ${cfg.dataDir}/.env
          cat ${cfg.database.passwordFile} >> ${cfg.dataDir}/.env
        ''
      )
      + ''
        # clear cache after update
        OLD_VERSION=$(cat ${cfg.dataDir}/version)
        if [[ $OLD_VERSION != "${package.version}" ]]; then
          rm -r ${cfg.dataDir}/cache/*
          echo "${package.version}" > ${cfg.dataDir}/version
        fi

        # convert rrd files when the oneMinutePolling option is changed
        OLD_ENABLED=$(cat ${cfg.dataDir}/one_minute_enabled)
        if [[ $OLD_ENABLED != "${lib.boolToString cfg.enableOneMinutePolling}" ]]; then
          ${package}/scripts/rrdstep.php -h all
          echo "${lib.boolToString cfg.enableOneMinutePolling}" > ${cfg.dataDir}/one_minute_enabled
        fi

        # migrate db
        ${artisanWrapper}/bin/librenms-artisan migrate --force --no-interaction
      '';
    };

    programs.mtr.enable = true;

    services.logrotate = {
      enable = true;
      settings."${cfg.logDir}/librenms.log" = {
        su = "${cfg.user} ${cfg.group}";
        create = "0640 ${cfg.user} ${cfg.group}";
        rotate = 6;
        frequency = "weekly";
        compress = true;
        delaycompress = true;
        missingok = true;
        notifempty = true;
      };
    };

    services.cron = {
      enable = true;
      systemCronJobs =
        let
          env = "PHPRC=${phpIni}";
        in
        [
          # based on crontab provided by LibreNMS
          "33 */6 * * * ${cfg.user} ${env} ${package}/cronic ${package}/discovery-wrapper.py 1"
          "*/5 * * * * ${cfg.user} ${env} ${package}/discovery.php -h new >> /dev/null 2>&1"

          "${if cfg.enableOneMinutePolling then "*" else "*/5"} * * * * ${cfg.user} ${env} ${package}/cronic ${package}/poller-wrapper.py ${toString cfg.pollerThreads}"
          "* * * * * ${cfg.user} ${env} ${package}/alerts.php >> /dev/null 2>&1"

          "*/5 * * * * ${cfg.user} ${env} ${package}/poll-billing.php >> /dev/null 2>&1"
          "01 * * * * ${cfg.user} ${env} ${package}/billing-calculate.php >> /dev/null 2>&1"
          "*/5 * * * * ${cfg.user} ${env} ${package}/check-services.php >> /dev/null 2>&1"

          # extra: fast ping
          "* * * * * ${cfg.user} ${env} ${package}/ping.php >> /dev/null 2>&1"

          # daily.sh tasks are split to exclude update
          "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh cleanup >> /dev/null 2>&1"
          "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh notifications >> /dev/null 2>&1"
          "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh peeringdb >> /dev/null 2>&1"
          "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh mac_oui >> /dev/null 2>&1"
        ];
    };

    security.wrappers = {
      fping = {
        setuid = true;
        owner = "root";
        group = "root";
        source = "${pkgs.fping}/bin/fping";
      };
    };

    environment.systemPackages = [ artisanWrapper lnmsWrapper ];

    systemd.tmpfiles.rules = [
      "d ${cfg.logDir}                               0750 ${cfg.user} ${cfg.group} - -"
      "f ${cfg.logDir}/librenms.log                  0640 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}                              0750 ${cfg.user} ${cfg.group} - -"
      "f ${cfg.dataDir}/.env                         0600 ${cfg.user} ${cfg.group} - -"
      "f ${cfg.dataDir}/version                      0600 ${cfg.user} ${cfg.group} - -"
      "f ${cfg.dataDir}/one_minute_enabled           0600 ${cfg.user} ${cfg.group} - -"
      "f ${cfg.dataDir}/config.json                  0600 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage                      0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage/app                  0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage/debugbar             0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage/framework            0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage/framework/cache      0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage/framework/sessions   0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage/framework/views      0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/storage/logs                 0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/rrd                          0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.dataDir}/cache                        0700 ${cfg.user} ${cfg.group} - -"
    ] ++ lib.optionals cfg.useDistributedPollers [
      "d ${cfg.dataDir}/rrdcached-journal            0700 ${cfg.user} ${cfg.group} - -"
    ];

  };

  meta.maintainers = lib.teams.wdz.members;
}
