# build/nix/nixos-module.nix
#
# NixOS module for Performance Co-Pilot.
# Mirrors a standard RHEL/Debian standalone PCP deployment.
#
# Design notes:
# - Uses systemd.packages to pull in upstream unit files from the PCP build
# - Applies hardening overlays rather than rewriting units from scratch
# - This preserves upstream ExecStartPre, EnvironmentFile, and other settings
#
# Cache optimization:
# - The `pcp` argument should be passed via specialArgs from the flake
# - This ensures all derivations use the same PCP package for cache reuse
# - See microvm.nix line 88: specialArgs = { inherit pcp; };
#
{ config, pkgs, lib, pcp ? null, ... }:
with lib;
let
  cfg = config.services.pcp;
  constants = import ./constants.nix;

  pcpConf = "${cfg.package}/share/pcp/etc/pcp.conf";
  pcpDir = "${cfg.package}/share/pcp";

  # ─── PMDA Configuration ─────────────────────────────────────────────────
  # Generate pmcd.conf entries for additional PMDAs
  pmdaEntries = concatStringsSep "\n" (mapAttrsToList (name: pmda:
    if pmda.type == "dso" then
      "${name}\t${toString pmda.domain}\tdso\t${pmda.init}\t${pmda.path}"
    else
      "${name}\t${toString pmda.domain}\t${pmda.type}\t${pmda.flags}\t${pmda.command}"
  ) cfg.pmdas);

  # Generate pmcd.conf with base PMDAs + additional ones
  # On Linux, the base is: linux (60) + pmcd (2), both as DSO
  pmcdConf = pkgs.writeText "pmcd.conf" ''
    #
    # Performance Co-Pilot PMDA Configuration
    # Generated by NixOS - do not edit directly
    #
    # Format: name domain_id type init_func/flags path/command
    #

    # ─── Base Platform PMDAs ──────────────────────────────────────────────
    # Linux kernel metrics (DSO for performance)
    linux	${toString constants.pmdaDomains.linux}	dso	linux_init	${cfg.package}/var/lib/pcp/pmdas/linux/pmda_linux.so

    # PMCD internal metrics (DSO)
    pmcd	${toString constants.pmdaDomains.pmcd}	dso	pmcd_init	${cfg.package}/var/lib/pcp/pmdas/pmcd/pmda_pmcd.so

    ${optionalString (pmdaEntries != "") ''
    # ─── Additional PMDAs ─────────────────────────────────────────────────
    ${pmdaEntries}
    ''}
  '';

  # Environment variables for PCP services
  # Override PCP_*_DIR variables to use mutable system paths instead of
  # immutable Nix store paths (required for pmlogger, pmie, pmcd, etc.)
  pcpEnv = {
    PCP_CONF = pcpConf;
    PCP_DIR = pcpDir;
    # Point to generated pmcd.conf (overrides default location)
    PCP_PMCDCONF_PATH = "/etc/pcp/pmcd/pmcd.conf";
    # Mutable runtime paths (from constants.nix - Nix store versions are read-only)
    PCP_LOG_DIR = constants.paths.logDir;
    PCP_VAR_DIR = constants.paths.varDir;
    PCP_TMP_DIR = constants.paths.tmpDir;
    PCP_RUN_DIR = constants.paths.runDir;
    # pmlogger archive directory (must be writable)
    PCP_ARCHIVE_DIR = constants.paths.archiveDir;
    # Override hardcoded /bin/pwd path in shell scripts (see shell-portable-pwd.patch)
    PWDCMND = "pwd";
  };

  # ─── Shared systemd hardening ────────────────────────────────────────
  # Applied to network-facing PCP services (pmcd, pmproxy).
  # Note: Some hardening is relaxed because pmcd needs to:
  # - Access /proc for metric collection
  # - Use various system calls for performance monitoring
  baseHardening = {
    PrivateTmp = true;
    ProtectHome = true;
    ProtectControlGroups = true;
    ProtectKernelModules = true;
    NoNewPrivileges = true;
    RestrictRealtime = true;
    LockPersonality = true;
  };

  # pmcd hardening: optionally add capabilities for PMDAs that need elevated access
  # - relaxedPermissions: DAC and ptrace for pmdaproc
  # - BCC PMDA: BPF syscall access for eBPF programs
  hasBccPmda = cfg.pmdas ? bcc;
  pmcdHardening = recursiveUpdate baseHardening (
    optionalAttrs (cfg.relaxedPermissions || hasBccPmda) ({
      CapabilityBoundingSet = []
        ++ optionals cfg.relaxedPermissions [
          "CAP_DAC_READ_SEARCH"  # For reading /proc of other users' processes
          "CAP_SYS_PTRACE"       # For pmdaproc process inspection
        ]
        ++ optionals hasBccPmda [
          "CAP_SYS_ADMIN"        # BPF syscall (kernel < 5.8)
          "CAP_BPF"              # BPF syscall (kernel >= 5.8)
          "CAP_PERFMON"          # Perf events (kernel >= 5.8)
          "CAP_SYS_PTRACE"       # Read /proc of other processes
          "CAP_DAC_READ_SEARCH"  # Access kernel symbols
        ];
    } // optionalAttrs hasBccPmda {
      AmbientCapabilities = [
        "CAP_SYS_ADMIN"
        "CAP_BPF"
        "CAP_PERFMON"
        "CAP_SYS_PTRACE"
        "CAP_DAC_READ_SEARCH"
      ];
      # BCC PMDA needs to disable NoNewPrivileges to use ambient capabilities
      NoNewPrivileges = false;
    })
  );

  pmproxyHardening = baseHardening;

  # IP filtering when allowedNetworks is specified
  ipFiltering = optionalAttrs (cfg.allowedNetworks != []) {
    IPAddressAllow = cfg.allowedNetworks;
    IPAddressDeny = "any";
  };

in
{
  # ═══════════════════════════════════════════════════════════════════════
  # Options interface
  # ═══════════════════════════════════════════════════════════════════════

  options.services.pcp = {
    enable = mkEnableOption "Performance Co-Pilot monitoring services";

    package = mkOption {
      type = types.package;
      default = if pcp != null then pcp
                else throw "PCP package not found. Pass 'pcp' via specialArgs or set services.pcp.package explicitly.";
      defaultText = lib.literalExpression "pcp (from specialArgs)";
      description = "The PCP package to use. Passed via specialArgs from flake.nix for cache consistency.";
    };

    preset = mkOption {
      type = types.enum [ "standalone" "minimal" "custom" ];
      default = "standalone";
      description = ''
        Deployment preset:
        - standalone: Full PCP suite (pmcd + pmlogger + pmie + pmproxy)
        - minimal: Only pmcd (for remote collection targets)
        - custom: Manually configure each component
      '';
    };

    pmlogger.enable = mkOption {
      type = types.bool;
      default = cfg.preset == "standalone";
      defaultText = literalExpression ''config.services.pcp.preset == "standalone"'';
      description = "Enable pmlogger (archive logger).";
    };

    pmie.enable = mkOption {
      type = types.bool;
      default = cfg.preset == "standalone";
      defaultText = literalExpression ''config.services.pcp.preset == "standalone"'';
      description = "Enable pmie (inference engine).";
    };

    pmproxy.enable = mkOption {
      type = types.bool;
      default = cfg.preset == "standalone";
      defaultText = literalExpression ''config.services.pcp.preset == "standalone"'';
      description = "Enable pmproxy (REST API gateway).";
    };

    allowedNetworks = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [ "10.177.0.0/24" "127.0.0.0/8" ];
      description = "IP ranges allowed to connect to pmcd/pmproxy.";
    };

    openFirewall = mkOption {
      type = types.bool;
      default = true;
      description = "Automatically open firewall ports for enabled services.";
    };

    relaxedPermissions = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Enable additional capabilities for pmcd to collect more metrics.
        When true, grants CAP_DAC_READ_SEARCH and CAP_SYS_PTRACE for
        pmdaproc and other introspective PMDAs.
        When false (default), pmcd runs with minimal privileges.
      '';
    };

    pmdas = mkOption {
      type = types.attrsOf (types.submodule {
        options = {
          domain = mkOption {
            type = types.int;
            description = "PMDA domain number (unique identifier, 0-511)";
          };
          type = mkOption {
            type = types.enum [ "dso" "pipe" "socket" ];
            default = "pipe";
            description = "PMDA type: dso (shared library), pipe (spawned process), socket (daemon)";
          };
          flags = mkOption {
            type = types.str;
            default = "binary";
            description = "PMDA flags (e.g., 'binary', 'binary notready')";
          };
          command = mkOption {
            type = types.str;
            description = "For pipe/socket PMDAs: command to execute";
          };
          init = mkOption {
            type = types.nullOr types.str;
            default = null;
            description = "For DSO PMDAs: init function name";
          };
          path = mkOption {
            type = types.nullOr types.str;
            default = null;
            description = "For DSO PMDAs: path to shared library";
          };
        };
      });
      default = {};
      description = ''
        Additional PMDAs to register with pmcd.
        These are added to pmcd.conf alongside the base platform PMDAs.
      '';
      example = literalExpression ''
        {
          bcc = {
            domain = 149;
            type = "pipe";
            flags = "binary notready";
            command = "pmpython /path/to/pmdabcc.python -d 149";
          };
        }
      '';
    };
  };

  # ═══════════════════════════════════════════════════════════════════════
  # Implementation
  # ═══════════════════════════════════════════════════════════════════════

  config = mkIf cfg.enable {
    environment.systemPackages = [ cfg.package ];

    # Note: We define our own service units rather than using upstream units
    # because NixOS requires specific service configurations for proper systemd
    # integration (RuntimeDirectory, StateDirectory, etc.)

    users.users.pcp = {
      isSystemUser = true;
      group = "pcp";
      description = "Performance Co-Pilot daemon user";
    };
    users.groups.pcp = {};

    environment.variables = pcpEnv;

    systemd.tmpfiles.rules = [
      # Runtime directories
      "d /var/lib/pcp          0755 pcp pcp -"
      "d /var/lib/pcp/tmp      0775 pcp pcp -"
      "d /var/log/pcp          0755 pcp pcp -"
      "d /var/log/pcp/pmcd     0755 root root -"
      "d /var/log/pcp/pmlogger 0775 pcp pcp -"
      "d /var/log/pcp/pmie     0775 pcp pcp -"
      "d /var/log/pcp/pmproxy  0775 pcp pcp -"
      "d /run/pcp              0775 pcp pcp -"

      # Symlinks to read-only store paths (PMNS, PMDAs)
      "L+ /var/lib/pcp/pmns    - - - - ${cfg.package}/var/lib/pcp/pmns"
      "L+ /var/lib/pcp/pmdas   - - - - ${cfg.package}/var/lib/pcp/pmdas"
      "L+ /var/lib/pcp/pmcd    - - - - ${cfg.package}/var/lib/pcp/pmcd"

      # Config directory structure - mix of read-only symlinks and writable dirs
      # Note: parent /var/lib/pcp is owned by pcp, so config must also be pcp-owned
      # to avoid "unsafe path transition" errors from systemd-tmpfiles
      "d  /var/lib/pcp/config              0755 pcp pcp -"
      "L+ /var/lib/pcp/config/derived      - - - - ${cfg.package}/var/lib/pcp/config/derived"
      "L+ /var/lib/pcp/config/pmafm        - - - - ${cfg.package}/var/lib/pcp/config/pmafm"
      "L+ /var/lib/pcp/config/pmieconf     - - - - ${cfg.package}/var/lib/pcp/config/pmieconf"
      "L+ /var/lib/pcp/config/pmlogconf    - - - - ${cfg.package}/var/lib/pcp/config/pmlogconf"
      "L+ /var/lib/pcp/config/pmlogredact  - - - - ${cfg.package}/var/lib/pcp/config/pmlogredact"
      "L+ /var/lib/pcp/config/pmlogrewrite - - - - ${cfg.package}/var/lib/pcp/config/pmlogrewrite"
      # Writable directories for runtime-generated configs (pmlogconf creates config.default here)
      "d  /var/lib/pcp/config/pmda         0775 pcp pcp -"
      "d  /var/lib/pcp/config/pmie         0775 pcp pcp -"
      "d  /var/lib/pcp/config/pmlogger     0775 pcp pcp -"
      # Static pmlogger configs as symlinks
      "L+ /var/lib/pcp/config/pmlogger/config.pmstat  - - - - ${cfg.package}/var/lib/pcp/config/pmlogger/config.pmstat"
      "L+ /var/lib/pcp/config/pmlogger/options.pmstat - - - - ${cfg.package}/var/lib/pcp/config/pmlogger/options.pmstat"

      # FHS-compatible symlinks for users familiar with Fedora/RHEL
      # These make standard paths work: cat /etc/pcp.conf, source /etc/pcp.env
      "L+ /etc/pcp.conf        - - - - ${cfg.package}/share/pcp/etc/pcp.conf"
      "L+ /etc/pcp.env         - - - - ${cfg.package}/share/pcp/etc/pcp.env"

      # Create /etc/pcp directory structure (not a symlink, so we can have writable pmcd.conf)
      "d  /etc/pcp             0755 root root -"
      "d  /etc/pcp/pmcd        0755 root root -"
      # Copy generated pmcd.conf (needs to be a real file, not symlink, for pmcd to modify)
      "C  /etc/pcp/pmcd/pmcd.conf 0644 root root - ${pmcdConf}"
      # Symlink other config dirs from package
      "L+ /etc/pcp/labels      - - - - ${cfg.package}/share/pcp/etc/pcp/labels"
      "L+ /etc/pcp/pmie        - - - - ${cfg.package}/share/pcp/etc/pcp/pmie"
      "L+ /etc/pcp/pmlogger    - - - - ${cfg.package}/share/pcp/etc/pcp/pmlogger"
      "L+ /etc/pcp/pmproxy     - - - - ${cfg.package}/share/pcp/etc/pcp/pmproxy"
      "L+ /etc/pcp/derived     - - - - ${cfg.package}/share/pcp/etc/pcp/derived"
    ];

    # ─── pmcd service overlay ──────────────────────────────────────────
    systemd.services.pmcd = {
      description = "Performance Co-Pilot Collector Daemon";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
      wants = [ "network.target" ];
      environment = pcpEnv;
      serviceConfig = {
        # When BCC PMDA is enabled, use Type=simple because pmcd doesn't write
        # its PID file until all PMDAs are initialized, but BCC compilation
        # takes 2-3 minutes. With Type=forking, systemd expects the PID file
        # immediately after the fork completes, causing restart loops.
        Type = if hasBccPmda then "simple" else "forking";
        # When BCC PMDA is enabled, increase PMDA negotiation timeout (-q)
        # since eBPF compilation takes several seconds even with BTF enabled.
        # -q = initial negotiation timeout (default 3s)
        # -t = response timeout (default 5s)
        # -f = run in foreground (required for Type=simple)
        ExecStart = "${cfg.package}/libexec/pcp/bin/pmcd"
          + optionalString hasBccPmda " -f -q 60 -t 30";
        Restart = "on-failure";
        RuntimeDirectory = "pcp";
        StateDirectory = "pcp";
        # Note: Don't use LogsDirectory here - it creates dirs as root:root
        # since pmcd has no User=. tmpfiles.rules creates /var/log/pcp/* with pcp:pcp.
      } // optionalAttrs (!hasBccPmda) {
        # Only use PIDFile for non-BCC variants (where pmcd forks quickly)
        PIDFile = "/run/pcp/pmcd.pid";
      } // optionalAttrs hasBccPmda {
        # BCC PMDA compiles eBPF programs at startup which can take 2-3 minutes.
        TimeoutStartSec = "300";  # 5 minutes for BCC eBPF compilation
      } // pmcdHardening // ipFiltering;
    };

    # ─── pmlogger service overlay ──────────────────────────────────────
    # pmlogger_check is a shell script that:
    # 1. Reads the control file to find pmlogger specifications
    # 2. Generates config.default using pmlogconf if needed
    # 3. Starts pmlogger processes as background daemons
    # 4. Exits after starting all pmloggers
    #
    # We use Type=oneshot because pmlogger_check starts pmloggers and exits.
    # RemainAfterExit=yes makes systemd consider the service "active" after exit.
    # The actual pmlogger processes run independently as daemons.
    systemd.services.pmlogger = mkIf cfg.pmlogger.enable {
      description = "Performance Co-Pilot Archive Logger";
      wantedBy = [ "multi-user.target" ];
      after = [ "pmcd.service" ];
      bindsTo = [ "pmcd.service" ];
      environment = pcpEnv;
      path = with pkgs; [ coreutils gawk gnused gnugrep hostname findutils util-linux procps cfg.package ];
      serviceConfig = {
        Type = "oneshot";
        RemainAfterExit = true;
        # Start pmloggers based on control file
        # -V: verbose output for debugging in journal
        ExecStart = "${cfg.package}/libexec/pcp/bin/pmlogger_check -V";
        # Stop all pmloggers managed by this control file
        # -s: stop mode (sets STOP_PMLOGGER=true)
        ExecStop = "${cfg.package}/libexec/pcp/bin/pmlogger_check -s -V";
        User = "pcp";
        Group = "pcp";
        StateDirectory = "pcp";
        LogsDirectory = "pcp/pmlogger";
        # Give pmlogger_check time to generate config.default and start pmloggers
        TimeoutStartSec = "120";
      };
    };

    # ─── pmie service overlay ──────────────────────────────────────────
    # pmie_check is a shell script that needs awk, sed, grep, etc.
    systemd.services.pmie = mkIf cfg.pmie.enable {
      description = "Performance Co-Pilot Inference Engine";
      wantedBy = [ "multi-user.target" ];
      after = [ "pmcd.service" ];
      bindsTo = [ "pmcd.service" ];
      environment = pcpEnv;
      path = with pkgs; [ coreutils gawk gnused gnugrep hostname findutils util-linux procps cfg.package ];
      serviceConfig = {
        Type = "forking";
        ExecStart = "${cfg.package}/libexec/pcp/bin/pmie_check";
        Restart = "on-failure";
        User = "pcp";
        Group = "pcp";
        StateDirectory = "pcp";
        LogsDirectory = "pcp/pmie";
      };
    };

    # ─── pmproxy service overlay ───────────────────────────────────────
    systemd.services.pmproxy = mkIf cfg.pmproxy.enable {
      description = "Performance Co-Pilot REST API Proxy";
      wantedBy = [ "multi-user.target" ];
      after = [ "pmcd.service" ];
      bindsTo = [ "pmcd.service" ];
      environment = pcpEnv;
      serviceConfig = {
        Type = "forking";
        ExecStart = "${cfg.package}/libexec/pcp/bin/pmproxy";
        PIDFile = "/run/pcp/pmproxy.pid";
        Restart = "on-failure";
      } // pmproxyHardening // ipFiltering;
    };

    # ─── Maintenance timers ────────────────────────────────────────────
    systemd.timers.pmlogger-daily = mkIf cfg.pmlogger.enable {
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnCalendar = "*-*-* 00:10:00";
        Persistent = true;
      };
    };
    systemd.services.pmlogger-daily = mkIf cfg.pmlogger.enable {
      description = "PCP Logger Daily Maintenance";
      environment = pcpEnv;
      path = with pkgs; [ coreutils gawk gnused gnugrep hostname findutils util-linux procps cfg.package ];
      serviceConfig = {
        Type = "oneshot";
        ExecStart = "${cfg.package}/libexec/pcp/bin/pmlogger_daily";
      };
    };

    systemd.timers.pmie-daily = mkIf cfg.pmie.enable {
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnCalendar = "*-*-* 00:08:00";
        Persistent = true;
      };
    };
    systemd.services.pmie-daily = mkIf cfg.pmie.enable {
      description = "PCP Inference Engine Daily Maintenance";
      environment = pcpEnv;
      path = with pkgs; [ coreutils gawk gnused gnugrep hostname findutils util-linux procps cfg.package ];
      serviceConfig = {
        Type = "oneshot";
        ExecStart = "${cfg.package}/libexec/pcp/bin/pmie_daily";
      };
    };

    # Dynamic firewall based on enabled services
    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall (
      [ constants.ports.pmcd ] ++
      optional cfg.pmproxy.enable constants.ports.pmproxy
    );
  };
}
