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

let
  inherit (lib) mkDefault mkIf mkMerge mkOption mkOverride types;
  cfg = config.beatific;
  mkFlag = description: mkOption {
    type = types.bool;
    inherit description;
    default = true;
  };
in {
  options = {
    beatific = {
      # The host name is reused for beatific-specific configuration.
      # The bulk of common config is handled in beatific.defaults below, but
      # having one option without a default ensures that the module cannot be
      # imported accidentally.
      hostName = mkOption {
        type = types.str;
        description = "Hostname";
      };

      isLighthouse = mkOption {
        type = types.bool;
        description = "Whether this host is a Nebula lighthouse";
        default = false;
      };

      extraPrograms = mkOption {
        type = types.bool;
        description = "Additional default programs";
        default = false;
      };

      # Groups of related defaults can be disabled by flipping off the switches here:
      #   beatific.defaults.${category} = false;
      # They default to true because the point is to do these things by default.
      defaults = {
        time = mkFlag "Default time zone and NTP";
        i18n = mkFlag "Default locale settings";
        programs = mkFlag "Default installed programs";
        ssh = mkFlag "Default sshd settings";
        nebula = mkFlag "Default beatific nebula settings";
        tvb = mkFlag "Default tvb account";
        tvbSync = mkFlag "Configure system syncthing for tvb";
        hosts = mkFlag "Default 10.22.20.* DNS host entries";
      };
    };
  };

  config = mkMerge [
    {
      # Options to always set
      networking.hostName = cfg.hostName;
      networking.stevenblack = {
        enable = true;
        block = [ "fakenews" "gambling" "porn" ];
      };
      nix.extraOptions = "experimental-features = nix-command flakes";
      nix.settings.trusted-users = [ "tvb" ];
      # Link /etc/nixos to the flake source
      environment.etc.nixos.source = ./..;
      environment.etc."bashrc.local".source = ./bashrc;
      environment.shellAliases = {
        # Shortcut for nixos-rebuild
        nr = "sudo nixos-rebuild --fast --flake $HOME/nixos-configs";
        # Always preserve mode, ownership, ts with copy
        cp = "cp -rp";
        xo = "xdg-open";
        scp = "scp -p";
        smv = "rsync -av --remove-source-files";
        ffprobe = "ffprobe -hide_banner";
        ffmpeg = "ffmpeg -hide_banner";
        ".." = "cd ..";
        "..." = "cd ../..";
        "...." = "cd ../../..";
        "....." = "cd ../../../..";
      };
      security.sudo.extraRules = [{
        users = [ "tvb" ];
        commands = [ { command = "/run/current-system/sw/bin/nixos-rebuild"; options = [ "NOPASSWD" ]; } ];
      }];
    }

    (mkIf cfg.defaults.time {
      # mkDefault time zone to make it easy to configure it to non-UTC
      time.timeZone = mkDefault "UTC";
      services.ntp.enable = true;
      services.ntp.servers = [ "time.nist.gov" ];
    })

    (mkIf cfg.defaults.i18n {
      # en_US.UTF-8
      i18n.defaultLocale = "en_US.UTF-8";
      i18n.extraLocaleSettings = {
        LC_ADDRESS = "en_US.UTF-8";
        LC_IDENTIFICATION = "en_US.UTF-8";
        LC_MEASUREMENT = "en_US.UTF-8";
        LC_MONETARY = "en_US.UTF-8";
        LC_NAME = "en_US.UTF-8";
        LC_NUMERIC = "en_US.UTF-8";
        LC_PAPER = "en_US.UTF-8";
        LC_TELEPHONE = "en_US.UTF-8";
        LC_TIME = "en_US.UTF-8";
      };
    })

    (mkIf cfg.defaults.programs {
      environment.systemPackages = with pkgs; [
        bat           # colorized and numbered `less`
        bc            # Terminal calculator
        curl          # omnipotent URL tool
        difftastic    # file diff tool
        duf           # disk-free checker
        exiftool      # media tag tool
        ffmpeg        # omnipotent media tool
        file          # file type inspector
        htmlq         # jq for html
        jq            # jq for json
        nebula        # vpn
        poppler_utils # provides pdfto* utils, allows lesspipe to read pdfs
        psmisc        # provides killall
        python3       # second-best language for everything
        ripgrep       # fast file searcher
        rsync         # incremental remote copy
        sqlite        # omnipotent database
        tree          # directory tree view
        unzip         # .zip archive tool
        vim           # terminal editor
        viu           # terminal image "viewer"
        wget          # web fetcher
        zip           # .zip archive tool
        (writeShellScriptBin "clip" ''
          ${xclip}/bin/xclip -sel c < "$1"
        '')
      ];
      programs = {
        git = {
          enable = true;
          config = {
            init.defaultBranch = "master";
            merge.conflictstyle = "diff3";
            alias = {
              amend = "commit --amend";
              fixup = "commit --amend --no-edit";
              pick = "cherry-pick";
            };
          };
        };
        htop.enable = true;
      };
      # The nixpkgs default is "nano", so we go one priority higher
      environment.variables.EDITOR = mkOverride 999 "vim";
    })

    (mkIf cfg.extraPrograms {
      environment.systemPackages = with pkgs; [
        (pkgs.writeShellScriptBin "ebook-convert" ''
          exec ${pkgs.calibre}/bin/ebook-convert "$@"
        '')
        imagemagick  # image convertion cli
        puddletag  # mp3 tag editor
        tesseract  # OCR engine
      ];
    })

    (mkIf cfg.defaults.ssh {
      services.openssh.enable = true;
      services.openssh.settings.PrintMotd = true;
      environment.etc."motd".text = let
        ascii = import ./ascii.nix;
      in ascii.${cfg.hostName} or ascii.beatific;
      networking.firewall.allowPing = true;
      networking.firewall.allowedTCPPorts = [ 22 ];
    })

    (mkIf cfg.defaults.nebula {
      services.nebula.networks.beatific = let
        empyreanExternalDns = "vpn.alogoulogoi.com";
        empyreanInternalIp = "10.22.20.1";
        nebulaPort = 4242;
      in {
        enable = true;

        # The lighthouse only listens on the designated subdomain
        listen.host = if cfg.isLighthouse then empyreanExternalDns else "0.0.0.0";
        listen.port = nebulaPort;

        # Standard certificate paths
        ca = "/etc/nebula/beatific/beatific.crt";
        cert = "/etc/nebula/beatific/${cfg.hostName}.crt";
        key = "/etc/nebula/beatific/${cfg.hostName}.key";

        isLighthouse = cfg.isLighthouse;
        # Non-lighthouses connect to the lighthouse at empyrean
        # This should be a VPN address in the static host map
        lighthouses = mkIf (! cfg.isLighthouse) [ empyreanInternalIp ];

        # Currently there is no VPN-level traffic filtering
        firewall.outbound = [ { port = "any"; proto = "any"; host = "any"; } ];
        firewall.inbound  = [ { port = "any"; proto = "any"; host = "any"; } ];

        # Map the lighthouse address to its public address
        staticHostMap = { ${empyreanInternalIp} = [ "${empyreanExternalDns}:${toString nebulaPort}" ]; };

        settings = {
          # Enable UDP holepunching both ways, which allows nodes to establish more direct connections with each other
          punchy = { punch = true; response = true; };
        };
      };
    })

    (mkIf cfg.defaults.tvb {
      users.groups.tvb = {};
      users.users.tvb = {
        isNormalUser = true;
        group = "tvb";
        extraGroups = [ "wheel" ];
        initialPassword = "password";
        openssh.authorizedKeys.keyFiles = (import ../keys).tvb;
      };
    })

    (mkIf cfg.defaults.tvbSync {
      # I haven't gotten user services to work correctly yet,
      # so for now, tvb monopolizes the system syncthing instance.
      # Adding users in the future should probably involve multiple
      # system services so as not to require login to sync.
      services.syncthing = {
        enable = true;
        configDir = "/home/tvb/.config/syncthing";
        # this doesn't prevent syncthing from putting sync points in other locations, it's just a default
        # normally it would make sense to put it at ~ but see https://github.com/NixOS/nixpkgs/pull/273693
        dataDir = "/home/tvb/.config/syncthing";
        openDefaultPorts = true;
        user = "tvb";
        group = "tvb";
      };
    })

    (mkIf cfg.defaults.hosts {
      # Create *.home host entries for all the beatific members
      networking.hosts = {
        "10.22.20.1" = [
          "empyrean.home"
        ];
        "10.22.20.2" = [
          "catacomb.home"
          "mopidy.home.ktvb.site"
        ];
        "10.22.20.3" = [
          "palamas.home"
        ];
        "10.22.20.4" = [
          "stagirite.home"
        ];
        "10.22.20.5" = [
          "vagrant.home"
        ];
        "10.22.20.6" = [
          "unfolder.home"
        ];
        "10.22.20.7" = [
          "centroid.home"
        ];
        "10.22.20.8" = [
          "backyard.home"
          "pool.backyard.home"
          "mirror.backyard.home"
          "jellyfin.home.ktvb.site"
        ];
        "10.22.20.9" = [
          "imperium.home"
        ];
      };
    })
  ];
}