{ config, lib, pkgs, ... }:
with lib;
let
  cfg = config.services.graphite;
  writeTextOrNull = f: t: if t == null then null else pkgs.writeTextDir f t;
  dataDir = cfg.dataDir;
  graphiteApiConfig = pkgs.writeText "graphite-api.yaml" ''
    time_zone: ${config.time.timeZone}
    search_index: ${dataDir}/index
    ${optionalString (cfg.api.finders != []) ''finders:''}
    ${concatMapStringsSep "\n" (f: "  - " + f.moduleName) cfg.api.finders}
    ${optionalString (cfg.api.functions != []) ''functions:''}
    ${concatMapStringsSep "\n" (f: "  - " + f) cfg.api.functions}
    ${cfg.api.extraConfig}
  '';
  seyrenConfig = {
    SEYREN_URL = cfg.seyren.seyrenUrl;
    MONGO_URL = cfg.seyren.mongoUrl;
    GRAPHITE_URL = cfg.seyren.graphiteUrl;
  } // cfg.seyren.extraConfig;
  pagerConfig = pkgs.writeText "alarms.yaml" cfg.pager.alerts;
  configDir = pkgs.buildEnv {
    name = "graphite-config";
    paths = lists.filter (el: el != null) [
      (writeTextOrNull "carbon.conf" cfg.carbon.config)
      (writeTextOrNull "storage-aggregation.conf" cfg.carbon.storageAggregation)
      (writeTextOrNull "storage-schemas.conf" cfg.carbon.storageSchemas)
      (writeTextOrNull "blacklist.conf" cfg.carbon.blacklist)
      (writeTextOrNull "whitelist.conf" cfg.carbon.whitelist)
      (writeTextOrNull "rewrite-rules.conf" cfg.carbon.rewriteRules)
      (writeTextOrNull "relay-rules.conf" cfg.carbon.relayRules)
      (writeTextOrNull "aggregation-rules.conf" cfg.carbon.aggregationRules)
    ];
  };
  carbonOpts = name: with config.ids; ''
    --nodaemon --syslog --prefix=${name} --pidfile /run/${name}/${name}.pid ${name}
  '';
  mkPidFileDir = name: ''
    mkdir -p /run/${name}
    chmod 0700 /run/${name}
    chown -R graphite:graphite /run/${name}
  '';
  carbonEnv = {
    PYTHONPATH = let
      cenv = pkgs.python.buildEnv.override {
        extraLibs = [ pkgs.python27Packages.carbon ];
      };
      cenvPack =  "${cenv}/${pkgs.python.sitePackages}";
    # opt/graphite/lib contains twisted.plugins.carbon-cache
    in "${cenvPack}/opt/graphite/lib:${cenvPack}";
    GRAPHITE_ROOT = dataDir;
    GRAPHITE_CONF_DIR = configDir;
    GRAPHITE_STORAGE_DIR = dataDir;
  };
in {
  ###### interface
  options.services.graphite = {
    dataDir = mkOption {
      type = types.path;
      default = "/var/db/graphite";
      description = ''
        Data directory for graphite.
      '';
    };
    web = {
      enable = mkOption {
        description = "Whether to enable graphite web frontend.";
        default = false;
        type = types.bool;
      };
      listenAddress = mkOption {
        description = "Graphite web frontend listen address.";
        default = "127.0.0.1";
        type = types.str;
      };
      port = mkOption {
        description = "Graphite web frontend port.";
        default = 8080;
        type = types.int;
      };
    };
    api = {
      enable = mkOption {
        description = ''
          Whether to enable graphite api. Graphite api is lightweight alternative
          to graphite web, with api and without dashboard. It's advised to use
          grafana as alternative dashboard and influxdb as alternative to
          graphite carbon.
          For more information visit
          
        '';
        default = false;
        type = types.bool;
      };
      finders = mkOption {
        description = "List of finder plugins to load.";
        default = [];
        example = literalExample "[ pkgs.python27Packages.graphite_influxdb ]";
        type = types.listOf types.package;
      };
      functions = mkOption {
        description = "List of functions to load.";
        default = [
          "graphite_api.functions.SeriesFunctions"
          "graphite_api.functions.PieFunctions"
        ];
        type = types.listOf types.str;
      };
      listenAddress = mkOption {
        description = "Graphite web service listen address.";
        default = "127.0.0.1";
        type = types.str;
      };
      port = mkOption {
        description = "Graphite api service port.";
        default = 8080;
        type = types.int;
      };
      package = mkOption {
        description = "Package to use for graphite api.";
        default = pkgs.python27Packages.graphite_api;
        defaultText = "pkgs.python27Packages.graphite_api";
        type = types.package;
      };
      extraConfig = mkOption {
        description = "Extra configuration for graphite api.";
        default = ''
          whisper:
            directories:
                - ${dataDir}/whisper
        '';
        example = ''
          allowed_origins:
            - dashboard.example.com
          cheat_times: true
          influxdb:
            host: localhost
            port: 8086
            user: influxdb
            pass: influxdb
            db: metrics
          cache:
            CACHE_TYPE: 'filesystem'
            CACHE_DIR: '/tmp/graphite-api-cache'
        '';
        type = types.str;
      };
    };
    carbon = {
      config = mkOption {
        description = "Content of carbon configuration file.";
        default = ''
          [cache]
          # Listen on localhost by default for security reasons
          UDP_RECEIVER_INTERFACE = 127.0.0.1
          PICKLE_RECEIVER_INTERFACE = 127.0.0.1
          LINE_RECEIVER_INTERFACE = 127.0.0.1
          CACHE_QUERY_INTERFACE = 127.0.0.1
          # Do not log every update
          LOG_UPDATES = False
          LOG_CACHE_HITS = False
        '';
        type = types.str;
      };
      enableCache = mkOption {
        description = "Whether to enable carbon cache, the graphite storage daemon.";
        default = false;
        type = types.bool;
      };
      storageAggregation = mkOption {
        description = "Defines how to aggregate data to lower-precision retentions.";
        default = null;
        type = types.uniq (types.nullOr types.string);
        example = ''
          [all_min]
          pattern = \.min$
          xFilesFactor = 0.1
          aggregationMethod = min
        '';
      };
      storageSchemas = mkOption {
        description = "Defines retention rates for storing metrics.";
        default = "";
        type = types.uniq (types.nullOr types.string);
        example = ''
          [apache_busyWorkers]
          pattern = ^servers\.www.*\.workers\.busyWorkers$
          retentions = 15s:7d,1m:21d,15m:5y
        '';
      };
      blacklist = mkOption {
        description = "Any metrics received which match one of the experssions will be dropped.";
        default = null;
        type = types.uniq (types.nullOr types.string);
        example = "^some\.noisy\.metric\.prefix\..*";
      };
      whitelist = mkOption {
        description = "Only metrics received which match one of the experssions will be persisted.";
        default = null;
        type = types.uniq (types.nullOr types.string);
        example = ".*";
      };
      rewriteRules = mkOption {
        description = ''
          Regular expression patterns that can be used to rewrite metric names
          in a search and replace fashion.
        '';
        default = null;
        type = types.uniq (types.nullOr types.string);
        example = ''
          [post]
          _sum$ =
          _avg$ =
        '';
      };
      enableRelay = mkOption {
        description = "Whether to enable carbon relay, the carbon replication and sharding service.";
        default = false;
        type = types.bool;
      };
      relayRules = mkOption {
        description = "Relay rules are used to send certain metrics to a certain backend.";
        default = null;
        type = types.uniq (types.nullOr types.string);
        example = ''
          [example]
          pattern = ^mydata\.foo\..+
          servers = 10.1.2.3, 10.1.2.4:2004, myserver.mydomain.com
        '';
      };
      enableAggregator = mkOption {
        description = "Whether to enable carbon aggregator, the carbon buffering service.";
        default = false;
        type = types.bool;
      };
      aggregationRules = mkOption {
        description = "Defines if and how received metrics will be aggregated.";
        default = null;
        type = types.uniq (types.nullOr types.string);
        example = ''
          .applications..all.requests (60) = sum .applications..*.requests
          .applications..all.latency (60) = avg .applications..*.latency
        '';
      };
    };
    seyren = {
      enable = mkOption {
        description = "Whether to enable seyren service.";
        default = false;
        type = types.bool;
      };
      port = mkOption {
        description = "Seyren listening port.";
        default = 8081;
        type = types.int;
      };
      seyrenUrl = mkOption {
        default = "http://localhost:${toString cfg.seyren.port}/";
        description = "Host where seyren is accessible.";
        type = types.str;
      };
      graphiteUrl = mkOption {
        default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}";
        description = "Host where graphite service runs.";
        type = types.str;
      };
      mongoUrl = mkOption {
        default = "mongodb://${config.services.mongodb.bind_ip}:27017/seyren";
        description = "Mongodb connection string.";
        type = types.str;
      };
      extraConfig = mkOption {
        default = {};
        description = ''
          Extra seyren configuration. See
          
        '';
        type = types.attrsOf types.str;
        example = literalExample ''
          {
            GRAPHITE_USERNAME = "user";
            GRAPHITE_PASSWORD = "pass";
          }
        '';
      };
    };
    pager = {
      enable = mkOption {
        description = ''
          Whether to enable graphite-pager service. For more information visit
          
        '';
        default = false;
        type = types.bool;
      };
      redisUrl = mkOption {
        description = "Redis connection string.";
        default = "redis://localhost:${toString config.services.redis.port}/";
        type = types.str;
      };
      graphiteUrl = mkOption {
        description = "URL to your graphite service.";
        default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}";
        type = types.str;
      };
      alerts = mkOption {
        description = "Alerts configuration for graphite-pager.";
        default = ''
          alerts:
            - target: constantLine(100)
              warning: 90
              critical: 200
              name: Test
        '';
        example = ''
          pushbullet_key: pushbullet_api_key
          alerts:
            - target: stats.seatgeek.app.deal_quality.venue_info_cache.hit
              warning: .5
              critical: 1
              name: Deal quality venue cache hits
        '';
        type = types.lines;
      };
    };
    beacon = {
      enable = mkEnableOption "graphite beacon";
      config = mkOption {
        description = "Graphite beacon configuration.";
        default = {};
        type = types.attrs;
      };
    };
  };
  ###### implementation
  config = mkMerge [
    (mkIf cfg.carbon.enableCache {
      systemd.services.carbonCache = let name = "carbon-cache"; in {
        description = "Graphite Data Storage Backend";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        environment = carbonEnv;
        serviceConfig = {
          ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
          User = "graphite";
          Group = "graphite";
          PermissionsStartOnly = true;
          PIDFile="/run/${name}/${name}.pid";
        };
        preStart = mkPidFileDir name + ''
          mkdir -p ${cfg.dataDir}/whisper
          chmod 0700 ${cfg.dataDir}/whisper
          chown -R graphite:graphite ${cfg.dataDir}
        '';
      };
    })
    (mkIf cfg.carbon.enableAggregator {
      systemd.services.carbonAggregator = let name = "carbon-aggregator"; in {
        enable = cfg.carbon.enableAggregator;
        description = "Carbon Data Aggregator";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        environment = carbonEnv;
        serviceConfig = {
          ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
          User = "graphite";
          Group = "graphite";
          PIDFile="/run/${name}/${name}.pid";
        };
        preStart = mkPidFileDir name;
      };
    })
    (mkIf cfg.carbon.enableRelay {
      systemd.services.carbonRelay = let name = "carbon-relay"; in {
        description = "Carbon Data Relay";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        environment = carbonEnv;
        serviceConfig = {
          ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
          User = "graphite";
          Group = "graphite";
          PIDFile="/run/${name}/${name}.pid";
        };
        preStart = mkPidFileDir name;
      };
    })
    (mkIf (cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay) {
      environment.systemPackages = [
        pkgs.pythonPackages.carbon
      ];
    })
    (mkIf cfg.web.enable {
      systemd.services.graphiteWeb = {
        description = "Graphite Web Interface";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        path = [ pkgs.perl ];
        environment = {
          PYTHONPATH = let
              penv = pkgs.python.buildEnv.override {
                extraLibs = [
                  pkgs.python27Packages.graphite_web
                  pkgs.python27Packages.pysqlite
                ];
              };
              penvPack = "${penv}/${pkgs.python.sitePackages}";
              # opt/graphite/webapp contains graphite/settings.py
              # explicitly adding pycairo in path because it cannot be imported via buildEnv
            in "${penvPack}/opt/graphite/webapp:${penvPack}:${pkgs.pythonPackages.pycairo}/${pkgs.python.sitePackages}";
          DJANGO_SETTINGS_MODULE = "graphite.settings";
          GRAPHITE_CONF_DIR = configDir;
          GRAPHITE_STORAGE_DIR = dataDir;
          LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
        };
        serviceConfig = {
          ExecStart = ''
            ${pkgs.python27Packages.waitress}/bin/waitress-serve \
            --host=${cfg.web.listenAddress} --port=${toString cfg.web.port} \
            --call django.core.handlers.wsgi:WSGIHandler'';
          User = "graphite";
          Group = "graphite";
          PermissionsStartOnly = true;
        };
        preStart = ''
          if ! test -e ${dataDir}/db-created; then
            mkdir -p ${dataDir}/{whisper/,log/webapp/}
            chmod 0700 ${dataDir}/{whisper/,log/webapp/}
            # populate database
            ${pkgs.python27Packages.graphite_web}/bin/manage-graphite.py syncdb --noinput
            # create index
            ${pkgs.python27Packages.graphite_web}/bin/build-index.sh
            touch ${dataDir}/db-created
            chown -R graphite:graphite ${cfg.dataDir}
          fi
        '';
      };
      environment.systemPackages = [ pkgs.python27Packages.graphite_web ];
    })
    (mkIf cfg.api.enable {
      systemd.services.graphiteApi = {
        description = "Graphite Api Interface";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        environment = {
          PYTHONPATH = let
              aenv = pkgs.python.buildEnv.override {
                extraLibs = [ cfg.api.package pkgs.cairo ] ++ cfg.api.finders;
              };
            in "${aenv}/${pkgs.python.sitePackages}";
          GRAPHITE_API_CONFIG = graphiteApiConfig;
          LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
        };
        serviceConfig = {
          ExecStart = ''
            ${pkgs.python27Packages.waitress}/bin/waitress-serve \
            --host=${cfg.api.listenAddress} --port=${toString cfg.api.port} \
            graphite_api.app:app
          '';
          User = "graphite";
          Group = "graphite";
          PermissionsStartOnly = true;
        };
        preStart = ''
          if ! test -e ${dataDir}/db-created; then
            mkdir -p ${dataDir}/cache/
            chmod 0700 ${dataDir}/cache/
            touch ${dataDir}/db-created
            chown -R graphite:graphite ${cfg.dataDir}
          fi
        '';
      };
    })
    (mkIf cfg.seyren.enable {
      systemd.services.seyren = {
        description = "Graphite Alerting Dashboard";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" "mongodb.service" ];
        environment = seyrenConfig;
        serviceConfig = {
          ExecStart = "${pkgs.seyren}/bin/seyren -httpPort ${toString cfg.seyren.port}";
          WorkingDirectory = dataDir;
          User = "graphite";
          Group = "graphite";
        };
        preStart = ''
          if ! test -e ${dataDir}/db-created; then
            mkdir -p ${dataDir}
            chown -R graphite:graphite ${dataDir}
          fi
        '';
      };
      services.mongodb.enable = mkDefault true;
    })
    (mkIf cfg.pager.enable {
      systemd.services.graphitePager = {
        description = "Graphite Pager Alerting Daemon";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" "redis.service" ];
        environment = {
          REDIS_URL = cfg.pager.redisUrl;
          GRAPHITE_URL = cfg.pager.graphiteUrl;
        };
        serviceConfig = {
          ExecStart = "${pkgs.pythonPackages.graphite_pager}/bin/graphite-pager --config ${pagerConfig}";
          User = "graphite";
          Group = "graphite";
        };
      };
      services.redis.enable = mkDefault true;
      environment.systemPackages = [ pkgs.pythonPackages.graphite_pager ];
    })
    (mkIf cfg.beacon.enable {
      systemd.services.graphite-beacon = {
        description = "Grpahite Beacon Alerting Daemon";
        wantedBy = [ "multi-user.target" ];
        serviceConfig = {
          ExecStart = ''
            ${pkgs.pythonPackages.graphite_beacon}/bin/graphite-beacon \
              --config ${pkgs.writeText "graphite-beacon.json" (builtins.toJSON cfg.beacon.config)}
          '';
          User = "graphite";
          Group = "graphite";
        };
      };
    })
    (mkIf (
      cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay ||
      cfg.web.enable || cfg.api.enable ||
      cfg.seyren.enable || cfg.pager.enable || cfg.beacon.enable
     ) {
      users.extraUsers = singleton {
        name = "graphite";
        uid = config.ids.uids.graphite;
        description = "Graphite daemon user";
        home = dataDir;
      };
      users.extraGroups.graphite.gid = config.ids.gids.graphite;
    })
  ];
}