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

with lib;
let
  hostOpts = { hostname, ... }: {
    options = with types; {
      domain = mkOption {
        type = str;
        description =
          "Primary domain to which the host belongs, in the form of a domain name.";
        default = "fudo.org";
      };

      extra-domains = mkOption {
        type = listOf str;
        description = "Extra domain in which this host is reachable.";
        default = [ ];
      };

      aliases = mkOption {
        type = listOf str;
        description =
          "Host aliases used by the current host. Note this will be multiplied with extra-domains.";
        default = [ ];
      };

      site = mkOption {
        type = str;
        description = "Site at which the host is located.";
      };

      local-networks = mkOption {
        type = listof str;
        description =
          "A list of networks to be considered trusted by this host.";
        default = [ "127.0.0.0/8" ];
      };

      profile = mkOption {
        # FIXME: get this list from profiles directly
        type = listof (enum "desktop" "laptop" "server");
        description =
          "The profile to be applied to the host, determining what software is included.";
      };

      admin-email = mkOption {
        type = nullOr str;
        description = "Email for the administrator of this host.";
        default = null;
      };

      local-users = mkOption {
        type = listOf str;
        description =
          "List of users who should have local (i.e. login) access to the host.";
        default = [ ];
      };

      description = mkOption {
        type = str;
        description = "Description of this host.";
        default = "Another Fudo Host.";
      };

      local-admins = mkOption {
        type = listOf str;
        description =
          "A list of users who should have admin access to this host.";
        default = [ ];
      };

      local-groups = mkOption {
        type = listOf str;
        description = "List of groups which should exist on this host.";
        default = [ ];
      };

      ssh-fingerprints = mkOption {
        type = listOf str;
        description = ''
          A list of DNS SSHFP records for this host. Get with `ssh-keygen -r <hostname>`
        '';
        default = [ ];
      };

      rp = mkOption {
        type = nullOr str;
        description = "Responsible person.";
        default = null;
      };

      enable-gui = mkEnableOption "Install desktop GUI software.";

      docker-server = mkEnableOption "Enable Docker on the current host.";

      kerberos-services = mkOption {
        type = listOf str;
        description =
          "List of services which should exist for this host, if it belongs to a realm.";
        default = [ "ssh" "host" ];
      };

      ssh-pubkey = mkOption {
        type = nullOr str;
        description =
          "SSH key of the host. Find with `ssh-keyscan`. Skip the hostname, just type and key.";
        default = null;
      };

      build-pubkeys = mkOption {
        type = listOf str;
        description = "SSH public keys used to access the build server.";
        default = [ ];
      };
    };
  };

in {
  options.fudo.hosts = with types;
    mkOption {
      type = attrsOf (submodule hostOpts);
      description = "Host configurations for all hosts known to the system.";
      default = { };
    };

  config = let
    hostname = config.instance.hostname;
    host-cfg = config.fudo.hosts.${hostname};
    site-name = host-cfg.site;
    site = config.fudo.sites.${site-name};
    domain-name = host-cfg.domain;
    domain = config.fudo.domains.${domain-name};
    has-build-servers = (length (attrNames site.build-servers)) > 0;
    has-build-keys = (length host-cfg.build-pubkeys) > 0;

  in {
    networking = {
      hostName = config.instance.hostname;
      nameservers = site.nameservers;
      # This will cause a loop on the gateway itself
      #defaultGateway = site.gateway-v4;
      #defaultGateway6 = site.gateway-v6;

      # Necessary to ensure that Kerberos and Avahi both work. Kerberos needs
      # the fqdn of the host, whereas Avahi wants just the simple hostname.`
      hosts = { "127.0.0.1" = [ "${hostname}.${domain-name}" "${hostname}" ]; };
    };

    nix = mkIf
      (has-build-servers && has-build-keys && site.enable-distributed-builds) {
        buildMachines = mapAttrsToList (hostname: buildOpts: {
          hostName = "${hostname}.${domain}";
          maxJobs = buildOpts.max-jobs;
          speedFactor = buildOpts.speed-factor;
          supportedFeatures = buildOpts.supported-features;
        }) site.build-servers;
        distributedBuilds = true;
      };

    time.timeZone = site.timezone;

    krb5.libdefaults.default_realm = domain.gssapi-realm;

    services.cron.mailto = domain.admin-email;

    environment.systemPackages = with pkgs;
      mkIf (host-cfg.docker-server) [ docker nix-prefetch-docker ];

    virtualisation.docker = mkIf (host-cfg.docker-server) {
      enable = true;
      enableOnBoot = true;
      autoPrune.enable = true;
    };

    programs.ssh.knownHosts = let
      keyed-hosts =
        filterAttrs (host: opts: opts.ssh-pubkey != null) config.fudo.hosts;

      traceOut = obj: builtins.trace obj obj;

      crossProduct = f: list0: list1:
        concatMap (el0: map (el1: f el0 el1) list1) list0;

      getHostnames = hostOpts:
        [ hostOpts.hostname ]
        ++ (crossProduct (host: domain: "${host}.${domain}")
          ([ hostOpts.hostname ] ++ hostOpts.aliases)
          ([ hostOpts.domain ] ++ hostOpts.extra-domains));

      getHostEntryPairs = host:
        map (hostname: nameValuePair hostname { publicKey = host.ssh-pubkey; })
        (getHostnames host);

      hostAttrsToList = hostAttrs:
        mapAttrsToList (hostname: opts: { hostname = hostname; } // opts)
        hostAttrs;

      getKnownHosts = hosts:
        concatMap getHostEntryPairs (hostAttrsToList hosts);
    in listToAttrs (getKnownHosts keyed-hosts);
  };
}