{ config, lib, pkgs, ... }:
with lib;
let
  cfg = config.security.grsecurity;
  mkKernel = kernel: patch:
    assert patch.kversion == kernel.version;
      { inherit kernel patch;
        inherit (patch) grversion revision;
      };
  stable-patch = with pkgs.kernelPatches;
    if cfg.vserver then grsecurity_vserver else grsecurity_stable;
  stableKernel = mkKernel pkgs.linux_3_2  stable-patch;
  testKernel   = mkKernel pkgs.linux_3_13 pkgs.kernelPatches.grsecurity_unstable;
  ## -- grsecurity configuration -----------------------------------------------
  grsecPrioCfg =
    if cfg.config.priority == "security" then
      "GRKERNSEC_CONFIG_PRIORITY_SECURITY y"
    else
      "GRKERNSEC_CONFIG_PRIORITY_PERF y";
  grsecSystemCfg =
    if cfg.config.system == "desktop" then
      "GRKERNSEC_CONFIG_DESKTOP y"
    else
      "GRKERNSEC_CONFIG_SERVER y";
  grsecVirtCfg =
    if cfg.config.virtualisationConfig == "none" then
      "GRKERNSEC_CONFIG_VIRT_NONE y"
    else if cfg.config.virtualisationConfig == "host" then
      "GRKERNSEC_CONFIG_VIRT_HOST y"
    else
      "GRKERNSEC_CONFIG_VIRT_GUEST y";
  grsecHwvirtCfg = if cfg.config.virtualisationConfig == "none" then "" else
    if cfg.config.hardwareVirtualisation == true then
      "GRKERNSEC_CONFIG_VIRT_EPT y"
    else
      "GRKERNSEC_CONFIG_VIRT_SOFT y";
  grsecVirtswCfg =
    let virtCfg = opt: "GRKERNSEC_CONFIG_VIRT_"+opt+" y";
    in
      if cfg.config.virtualisationConfig == "none" then ""
      else if cfg.config.virtualisationSoftware == "xen"    then virtCfg "XEN"
      else if cfg.config.virtualisationSoftware == "kvm"    then virtCfg "KVM"
      else if cfg.config.virtualisationSoftware == "vmware" then virtCfg "VMWARE"
      else                                                       virtCfg "VIRTUALBOX";
  grsecMainConfig = if cfg.config.mode == "custom" then "" else ''
    GRKERNSEC_CONFIG_AUTO y
    ${grsecPrioCfg}
    ${grsecSystemCfg}
    ${grsecVirtCfg}
    ${grsecHwvirtCfg}
    ${grsecVirtswCfg}
  '';
  grsecConfig =
    let boolToKernOpt = b: if b then "y" else "n";
        # Disable RANDSTRUCT under virtualbox, as it has some kind of
        # breakage with the vbox guest drivers
        randstruct = optionalString config.services.virtualbox.enable
          "GRKERNSEC_RANDSTRUCT n";
        # Disable restricting links under the testing kernel, as something
        # has changed causing it to fail miserably during boot.
        restrictLinks = optionalString cfg.testing
          "GRKERNSEC_LINK n";
    in ''
      SECURITY_APPARMOR y
      DEFAULT_SECURITY_APPARMOR y
      GRKERNSEC y
      ${grsecMainConfig}
      ${if cfg.config.restrictProc then
          "GRKERNSEC_PROC_USER y"
        else
          optionalString cfg.config.restrictProcWithGroup ''
            GRKERNSEC_PROC_USERGROUP y
            GRKERNSEC_PROC_GID ${toString cfg.config.unrestrictProcGid}
          ''
      }
      GRKERNSEC_SYSCTL ${boolToKernOpt cfg.config.sysctl}
      GRKERNSEC_CHROOT_CHMOD ${boolToKernOpt cfg.config.denyChrootChmod}
      GRKERNSEC_NO_RBAC ${boolToKernOpt cfg.config.disableRBAC}
      ${randstruct}
      ${restrictLinks}
      ${cfg.config.kernelExtraConfig}
    '';
  ## -- grsecurity kernel packages ---------------------------------------------
  localver = grkern:
    "-grsec" + optionalString cfg.config.verboseVersion
       "-${grkern.grversion}-${grkern.revision}";
  grsecurityOverrider = args: grkern: {
    # Apparently as of gcc 4.6, gcc-plugin headers (which are needed by PaX plugins)
    # include libgmp headers, so we need these extra tweaks
    buildInputs = args.buildInputs ++ [ pkgs.gmp ];
    preConfigure = ''
      ${args.preConfigure or ""}
      sed -i 's|-I|-I${pkgs.gmp}/include -I|' scripts/gcc-plugin.sh
      sed -i 's|HOST_EXTRACFLAGS +=|HOST_EXTRACFLAGS += -I${pkgs.gmp}/include|' tools/gcc/Makefile
      sed -i 's|HOST_EXTRACXXFLAGS +=|HOST_EXTRACXXFLAGS += -I${pkgs.gmp}/include|' tools/gcc/Makefile
      rm localversion-grsec
      echo ${localver grkern} > localversion-grsec
    '';
  };
  mkGrsecPkg = grkern:
    let kernelPkg = lowPrio (overrideDerivation (grkern.kernel.override (args: {
          kernelPatches = args.kernelPatches ++ [ grkern.patch pkgs.kernelPatches.grsec_fix_path ];
          argsOverride = {
            modDirVersion = "${grkern.kernel.modDirVersion}${localver grkern}";
          };
          extraConfig = grsecConfig;
        })) (args: grsecurityOverrider args grkern));
    in pkgs.linuxPackagesFor kernelPkg (mkGrsecPkg grkern);
  grsecPackage = mkGrsecPkg (if cfg.stable then stableKernel else testKernel);
in
{
  options = {
    security.grsecurity = {
      enable = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Enable grsecurity support. This enables advanced exploit
          hardening for the Linux kernel, and adds support for
          administrative Role-Based Acess Control (RBAC) via
          gradm. It also includes traditional
          utilities for PaX.
        '';
      };
      stable = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Enable the stable grsecurity patch, based on Linux 3.2.
        '';
      };
      vserver = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Enable the stable grsecurity/vserver patches, based on Linux 3.2.
        '';
      };
      testing = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Enable the testing grsecurity patch, based on Linux 3.13.
        '';
      };
      config = {
        mode = mkOption {
          type = types.str;
          default = "auto";
          example = "custom";
          description = ''
            grsecurity configuration mode. This specifies whether
            grsecurity is auto-configured or otherwise completely
            manually configured. Can either by
            custom or auto.
            auto is recommended.
          '';
        };
        priority = mkOption {
          type = types.str;
          default = "security";
          example = "performance";
          description = ''
            grsecurity configuration priority. This specifies whether
            the kernel configuration should emphasize speed or
            security. Can either by security or
            performance.
          '';
        };
        system = mkOption {
          type = types.str;
          default = "";
          example = "desktop";
          description = ''
            grsecurity system configuration. This specifies whether
            the kernel configuration should be suitable for a Desktop
            or a Server. Can either by server or
            desktop.
          '';
        };
        virtualisationConfig = mkOption {
          type = types.str;
          default = "none";
          example = "host";
          description = ''
            grsecurity virtualisation configuration. This specifies
            the virtualisation role of the machine - that is, whether
            it will be a virtual machine guest, a virtual machine
            host, or neither. Can be one of none,
            host, or guest.
          '';
        };
        hardwareVirtualisation = mkOption {
          type = types.nullOr types.bool;
          default = null;
          example = true;
          description = ''
            grsecurity hardware virtualisation configuration. Set to
            true if your machine supports hardware
            accelerated virtualisation.
          '';
        };
        virtualisationSoftware = mkOption {
          type = types.str;
          default = "";
          example = "kvm";
          description = ''
            grsecurity virtualisation software. Set this to the
            specified virtual machine technology if the machine is
            running as a guest, or a host.
            Can be one of kvm,
            xen, vmware or
            virtualbox.
          '';
        };
        sysctl = mkOption {
          type = types.bool;
          default = false;
          description = ''
            If true, then set GRKERN_SYSCTL y. If
            enabled then grsecurity can be controlled using sysctl
            (and turned off). You are advised to *never* enable this,
            but if you do, make sure to always set the sysctl
            kernel.grsecurity.grsec_lock to
            non-zero as soon as all sysctl options are set. *THIS IS
            EXTREMELY IMPORTANT*!
            If disabled, this also turns off the
            systemd-sysctl service.
          '';
        };
        denyChrootChmod = mkOption {
          type = types.bool;
          default = false;
          description = ''
            If true, then set GRKERN_CHROOT_CHMOD
            y. If enabled, this denies processes inside a
            chroot from setting the suid or sgid bits using
            chmod or fchmod.
            By default this protection is disabled - it makes it
            impossible to use Nix to build software on your system,
            which is what most users want.
            If you are using NixOps to deploy your software to a
            remote machine, you're encouraged to enable this as you
            won't need to compile code.
          '';
        };
        restrictProc = mkOption {
          type = types.bool;
          default = false;
          description = ''
            If true, then set GRKERN_PROC_USER
            y. This restricts non-root users to only viewing
            their own processes and restricts network-related
            information, kernel symbols, and module information.
          '';
        };
        restrictProcWithGroup = mkOption {
          type = types.bool;
          default = true;
          description = ''
            If true, then set GRKERN_PROC_USERGROUP
            y. This is similar to
            restrictProc except it allows a special
            group (specified by unrestrictProcGid)
            to still access otherwise classified information in
            /proc.
          '';
        };
        unrestrictProcGid = mkOption {
          type = types.int;
          default = config.ids.gids.grsecurity;
          description = ''
            If set, specifies a GID which is exempt from
            /proc restrictions (set by
            GRKERN_PROC_USERGROUP). By default,
            this is set to the GID for grsecurity,
            a predefined NixOS group, which the
            root account is a member of. You may
            conveniently add other users to this group if you need
            access to /proc
          '';
        };
        disableRBAC = mkOption {
          type = types.bool;
          default = false;
          description = ''
            If true, then set GRKERN_NO_RBAC
            y. This disables the
            /dev/grsec device, which in turn
            disables the RBAC system (and gradm).
          '';
        };
        verboseVersion = mkOption {
          type = types.bool;
          default = false;
          description = "Use verbose version in kernel localversion.";
        };
        kernelExtraConfig = mkOption {
          type = types.str;
          default = "";
          description = "Extra kernel configuration parameters.";
        };
      };
    };
  };
  config = mkIf cfg.enable {
    assertions =
      [ { assertion = cfg.stable || cfg.testing;
          message   = ''
            If grsecurity is enabled, you must select either the
            stable patch (with kernel 3.2), or the testing patch (with
            kernel 3.13) to continue.
          '';
        }
        { assertion = (cfg.stable -> !cfg.testing) || (cfg.testing -> !cfg.stable);
          message   = ''
            You must select either the stable or testing patch, not
            both.
          '';
        }
        { assertion = (cfg.testing -> !cfg.vserver);
          message   = "The vserver patches are only supported in the stable kernel.";
        }
        { assertion = (cfg.config.restrictProc -> !cfg.config.restrictProcWithGroup) ||
                      (cfg.config.restrictProcWithGroup -> !cfg.config.restrictProc);
          message   = "You cannot enable both restrictProc and restrictProcWithGroup";
        }
        { assertion = config.boot.kernelPackages.kernel.features ? grsecurity
                   && config.boot.kernelPackages.kernel.features.grsecurity;
          message = "grsecurity enabled, but kernel doesn't have grsec support";
        }
        { assertion = elem cfg.config.mode [ "auto" "custom" ];
          message = "grsecurity mode must either be 'auto' or 'custom'.";
        }
        { assertion = cfg.config.mode == "auto" -> elem cfg.config.system [ "desktop" "server" ];
          message = "when using auto grsec mode, system must be either 'desktop' or 'server'";
        }
        { assertion = cfg.config.mode == "auto" -> elem cfg.config.priority [ "performance" "security" ];
          message = "when using auto grsec mode, priority must be 'performance' or 'security'.";
        }
        { assertion = cfg.config.mode == "auto" -> elem cfg.config.virtualisationConfig [ "host" "guest" "none" ];
          message = "when using auto grsec mode, 'virt' must be 'host', 'guest' or 'none'.";
        }
        { assertion = (cfg.config.mode == "auto" && (elem cfg.config.virtualisationConfig [ "host" "guest" ])) ->
              cfg.config.hardwareVirtualisation != null;
          message   = "when using auto grsec mode with virtualisation, you must specify if your hardware has virtualisation extensions";
        }
        { assertion = (cfg.config.mode == "auto" && (elem cfg.config.virtualisationConfig [ "host" "guest" ])) ->
              elem cfg.config.virtualisationSoftware [ "kvm" "xen" "virtualbox" "vmware" ];
          message   = "virtualisation software must be 'kvm', 'xen', 'vmware' or 'virtualbox'";
        }
      ];
    systemd.services.grsec-lock = mkIf cfg.config.sysctl {
      description     = "grsecurity sysctl-lock Service";
      requires        = [ "sysctl.service" ];
      wantedBy        = [ "multi-user.target" ];
      serviceConfig.Type = "oneshot";
      serviceConfig.RemainAfterExit = "yes";
      script = ''
        locked=`cat /proc/sys/kernel/grsecurity/grsec_lock`
        if [ "$locked" == "0" ]; then
            echo 1 > /proc/sys/kernel/grsecurity/grsec_lock
            echo grsecurity sysctl lock - enabled
        else
            echo grsecurity sysctl lock already enabled - doing nothing
        fi
      '';
    };
#   systemd.services.grsec-learn = {
#     description     = "grsecurity learning Service";
#     wantedBy        = [ "local-fs.target" ];
#     serviceConfig   = {
#       Type = "oneshot";
#       RemainAfterExit = "yes";
#       ExecStart = "${pkgs.gradm}/sbin/gradm -VFL /etc/grsec/learning.logs";
#       ExecStop  = "${pkgs.gradm}/sbin/gradm -D";
#     };
#   };
    system.activationScripts.grsec =
      ''
        mkdir -p /etc/grsec
        if [ ! -f /etc/grsec/learn_config ]; then
          cp ${pkgs.gradm}/etc/grsec/learn_config /etc/grsec
        fi
        if [ ! -f /etc/grsec/policy ]; then
          cp ${pkgs.gradm}/etc/grsec/policy /etc/grsec
        fi
        chmod -R 0600 /etc/grsec
      '';
    # Enable apparmor support, gradm udev rules, and utilities
    security.apparmor.enable   = true;
    boot.kernelPackages        = grsecPackage;
    services.udev.packages     = [ pkgs.gradm ];
    environment.systemPackages = [ pkgs.gradm pkgs.paxctl pkgs.pax-utils ];
  };
}