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

with lib;

let

  dag = config.lib.dag;

  cfg = config.programs.mbsync;

  # Accounts for which mbsync is enabled.
  mbsyncAccounts =
    filter (a: a.mbsync.enable) (attrValues config.accounts.email.accounts);

  genTlsConfig = tls: {
      SSLType =
        if !tls.enable          then "None"
        else if tls.useStartTls then "STARTTLS"
        else                         "IMAPS";
    }
    //
    optionalAttrs (tls.enable && tls.certificatesFile != null) {
      CertificateFile = tls.certificatesFile;
    };

  masterSlaveMapping = {
    none = "None";
    imap = "Master";
    maildir = "Slave";
    both = "Both";
  };

  genSection = header: entries:
    let
      escapeValue = escape [ "\"" ];
      hasSpace = v: builtins.match ".* .*" v != null;
      genValue = v:
        if isList v
        then concatMapStringsSep " " genValue v
        else if isBool v then (if v then "yes" else "no")
        else if isInt v then toString v
        else if hasSpace v then "\"${escapeValue v}\""
        else v;
    in
      ''
        ${header}
        ${concatStringsSep "\n"
          (mapAttrsToList (n: v: "${n} ${genValue v}") entries)}
      '';

  genAccountConfig = account: with account;
    if (imap == null || maildir == null)
    then ""
    else
      genSection "IMAPAccount ${name}" (
        {
          Host = imap.host;
          User = userName;
          PassCmd = toString passwordCommand;
        }
        // genTlsConfig imap.tls
        // optionalAttrs (imap.port != null) { Port = toString imap.port; }
        // mbsync.extraConfig.account
      )
      + "\n"
      + genSection "IMAPStore ${name}-remote" (
        {
          Account = name;
        }
        // mbsync.extraConfig.remote
      )
      + "\n"
      + genSection "MaildirStore ${name}-local" (
        {
          Path = "${maildir.absPath}/";
          Inbox = "${maildir.absPath}/${folders.inbox}";
          SubFolders = "Verbatim";
        }
        // optionalAttrs (mbsync.flatten != null) { Flatten = mbsync.flatten; }
        // mbsync.extraConfig.local
      )
      + "\n"
      + genSection "Channel ${name}" (
        {
          Master = ":${name}-remote:";
          Slave = ":${name}-local:";
          Patterns = mbsync.patterns;
          Create = masterSlaveMapping.${mbsync.create};
          Remove = masterSlaveMapping.${mbsync.remove};
          Expunge = masterSlaveMapping.${mbsync.expunge};
          SyncState = "*";
        }
        // mbsync.extraConfig.channel
      )
      + "\n";

  genGroupConfig = name: channels:
    let
      genGroupChannel = n: boxes: "Channel ${n}:${concatStringsSep "," boxes}";
    in
      concatStringsSep "\n" (
        [ "Group ${name}" ] ++ mapAttrsToList genGroupChannel channels
      );

in

{
  options = {
    programs.mbsync = {
      enable = mkEnableOption "mbsync IMAP4 and Maildir mailbox synchronizer";

      package = mkOption {
        type = types.package;
        default = pkgs.isync;
        defaultText = "pkgs.isync";
        example = literalExample "pkgs.isync";
        description = "The package to use for the mbsync binary.";
      };

      groups = mkOption {
        type = types.attrsOf (types.attrsOf (types.listOf types.str));
        default = {};
        example = literalExample ''
          {
            inboxes = {
              account1 = [ "Inbox" ];
              account2 = [ "Inbox" ];
            };
          }
        '';
        description = ''
          Definition of groups.
        '';
      };

      extraConfig = mkOption {
        type = types.lines;
        default = "";
        description = ''
          Extra configuration lines to add to the mbsync configuration.
        '';
      };
    };
  };

  config = mkIf cfg.enable {
    assertions = [
      (
        let
          badAccounts = filter (a: a.maildir == null) mbsyncAccounts;
        in
          {
            assertion = badAccounts == [];
            message = "mbsync: Missing maildir configuration for accounts: "
              + concatMapStringsSep ", " (a: a.name) badAccounts;
          }
      )

      (
        let
          badAccounts = filter (a: a.imap == null) mbsyncAccounts;
        in
          {
            assertion = badAccounts == [];
            message = "mbsync: Missing IMAP configuration for accounts: "
              + concatMapStringsSep ", " (a: a.name) badAccounts;
          }
      )
    ];

    home.packages = [ cfg.package ];

    programs.notmuch.new.ignore = [ ".uidvalidity" ".mbsyncstate" ];

    home.file.".mbsyncrc".text =
      let
        accountsConfig = map genAccountConfig mbsyncAccounts;
        groupsConfig = mapAttrsToList genGroupConfig cfg.groups;
      in
        concatStringsSep "\n" (
          [ "# Generated by Home Manager.\n" ]
          ++ accountsConfig
          ++ groupsConfig
          ++ optional (cfg.extraConfig != "") cfg.extraConfig
        );

    home.activation.createMaildir =
      dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] ''
        $DRY_RUN_CMD mkdir -m700 -p $VERBOSE_ARG ${
          concatMapStringsSep " " (a: a.maildir.absPath) mbsyncAccounts
        }
      '';
  };
}