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

let
  inherit (lib)
    any
    attrValues
    boolToString
    concatMapStringsSep
    concatStrings
    concatStringsSep
    escapeShellArg
    flatten
    isBool
    isInt
    isList
    isString
    literalExpression
    mapAttrs'
    mapAttrsToList
    mkDefault
    mkEnableOption
    mkIf
    mkMerge
    mkOption
    nameValuePair
    optionalString
    types
    ;

  cfg = config.services.invoiceplane;
  eachSite = cfg.sites;
  user = "invoiceplane";
  webserver = config.services.${cfg.webserver};
  #backupDir = "/var/lib/invoiceplane/backup";

  invoiceplane-config =
    hostName: cfg:
    pkgs.writeText "ipconfig.php" ''
      IP_URL=http://${hostName}
      ENABLE_DEBUG=false
      DISABLE_SETUP=false
      REMOVE_INDEXPHP=false
      DB_HOSTNAME=${cfg.database.host}
      DB_USERNAME=${cfg.database.user}
      # NOTE: file_get_contents adds newline at the end of returned string
      DB_PASSWORD=${
        optionalString (
          cfg.database.passwordFile != null
        ) "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")"
      }
      DB_DATABASE=${cfg.database.name}
      DB_PORT=${toString cfg.database.port}
      SESS_EXPIRATION=864000
      ENABLE_INVOICE_DELETION=false
      DISABLE_READ_ONLY=false
      ENCRYPTION_KEY=
      ENCRYPTION_CIPHER=AES-256
      SETUP_COMPLETED=false
      REMOVE_INDEXPHP=true
    '';

  mkPhpValue =
    v:
    if isString v then
      escapeShellArg v
    # NOTE: If any value contains a , (comma) this will not get escaped
    else if isList v && any lib.strings.isCoercibleToString v then
      escapeShellArg (concatMapStringsSep "," toString v)
    else if isInt v then
      toString v
    else if isBool v then
      boolToString v
    else
      abort "The Invoiceplane config value ${lib.generators.toPretty { } v} can not be encoded.";

  extraConfig =
    hostName: cfg:
    let
      settings = mapAttrsToList (k: v: "${k}=${mkPhpValue v}") cfg.settings;
    in
    pkgs.writeText "extraConfig.php" (concatStringsSep "\n" settings);

  pkg =
    hostName: cfg:
    pkgs.stdenv.mkDerivation rec {
      pname = "invoiceplane-${hostName}";
      version = src.version;
      src = pkgs.invoiceplane;

      postPatch = ''
        # Patch index.php file to load additional config file
        substituteInPlace index.php \
          --replace-fail "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'extraConfig.php'); \$dotenv->load();";
      '';

      installPhase = ''
        mkdir -p $out
        cp -r * $out/

        # symlink uploads and log directories
        rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp
        ln -sf ${cfg.stateDir}/uploads $out/
        ln -sf ${cfg.stateDir}/logs $out/application/
        ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/

        # symlink the InvoicePlane config
        ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php

        # symlink the extraConfig file
        ln -s ${extraConfig hostName cfg} $out/extraConfig.php

        # symlink additional templates
        ${concatMapStringsSep "\n" (
          template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/"
        ) cfg.invoiceTemplates}
      '';
    };

  siteOpts =
    { name, ... }:
    {
      options = {

        enable = mkEnableOption "InvoicePlane web application";

        stateDir = mkOption {
          type = types.path;
          default = "/var/lib/invoiceplane/${name}";
          description = ''
            This directory is used for uploads of attachments and cache.
            The directory passed here is automatically created and permissions
            adjusted as required.
          '';
        };

        database = {
          host = mkOption {
            type = types.str;
            default = "localhost";
            description = "Database host address.";
          };

          port = mkOption {
            type = types.port;
            default = 3306;
            description = "Database host port.";
          };

          name = mkOption {
            type = types.str;
            default = "invoiceplane";
            description = "Database name.";
          };

          user = mkOption {
            type = types.str;
            default = "invoiceplane";
            description = "Database user.";
          };

          passwordFile = mkOption {
            type = types.nullOr types.path;
            default = null;
            example = "/run/keys/invoiceplane-dbpassword";
            description = ''
              A file containing the password corresponding to
              {option}`database.user`.
            '';
          };

          createLocally = mkOption {
            type = types.bool;
            default = true;
            description = "Create the database and database user locally.";
          };
        };

        invoiceTemplates = mkOption {
          type = types.listOf types.path;
          default = [ ];
          description = ''
            List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.

            ::: {.note}
            These templates need to be packaged before use, see example.
            :::
          '';
          example = literalExpression ''
            let
              # Let's package an example template
              template-vtdirektmarketing = pkgs.stdenv.mkDerivation {
                name = "vtdirektmarketing";
                # Download the template from a public repository
                src = pkgs.fetchgit {
                  url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git";
                  sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z";
                };
                sourceRoot = ".";
                # Installing simply means copying template php file to the output directory
                installPhase = ""
                  mkdir -p $out
                  cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/
                "";
              };
            # And then pass this package to the template list like this:
            in [ template-vtdirektmarketing ]
          '';
        };

        poolConfig = mkOption {
          type =
            with types;
            attrsOf (oneOf [
              str
              int
              bool
            ]);
          default = {
            "pm" = "dynamic";
            "pm.max_children" = 32;
            "pm.start_servers" = 2;
            "pm.min_spare_servers" = 2;
            "pm.max_spare_servers" = 4;
            "pm.max_requests" = 500;
          };
          description = ''
            Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf`
            for details on configuration directives.
          '';
        };

        settings = mkOption {
          type = types.attrsOf types.anything;
          default = { };
          description = ''
            Structural InvoicePlane configuration. Refer to
            <https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example>
            for details and supported values.
          '';
          example = literalExpression ''
            {
              SETUP_COMPLETED = true;
              DISABLE_SETUP = true;
              IP_URL = "https://invoice.example.com";
            }
          '';
        };

        cron = {
          enable = mkOption {
            type = types.bool;
            default = false;
            description = ''
              Enable cron service which periodically runs Invoiceplane tasks.
              Requires key taken from the administration page. Refer to
              <https://wiki.invoiceplane.com/en/1.0/modules/recurring-invoices>
              on how to configure it.
            '';
          };
          key = mkOption {
            type = types.str;
            description = "Cron key taken from the administration page.";
          };
        };

      };

    };
in
{
  disabledModules = [ "services/web-apps/invoiceplane.nix" ];

  # interface
  options = {
    services.invoiceplane = mkOption {
      type = types.submodule {

        options.sites = mkOption {
          type = types.attrsOf (types.submodule siteOpts);
          default = { };
          description = "Specification of one or more WordPress sites to serve";
        };

        options.webserver = mkOption {
          type = types.enum [
            "caddy"
            "nginx"
          ];
          default = "caddy";
          example = "nginx";
          description = ''
            Which webserver to use for virtual host management.
          '';
        };
      };
      default = { };
      description = "InvoicePlane configuration.";
    };

  };

  # implementation
  config = mkIf (eachSite != { }) (mkMerge [
    {

      assertions = flatten (
        mapAttrsToList (hostName: cfg: [
          {
            assertion = cfg.database.createLocally -> cfg.database.user == user;
            message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
          }
          {
            assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
            message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.'';
          }
          {
            assertion = cfg.cron.enable -> cfg.cron.key != null;
            message = ''services.invoiceplane.sites."${hostName}".cron.key must be set in order to use cron service.'';
          }
        ]) eachSite
      );

      services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
        enable = true;
        package = mkDefault pkgs.mariadb;
        ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
        ensureUsers = mapAttrsToList (hostName: cfg: {
          name = cfg.database.user;
          ensurePermissions = {
            "${cfg.database.name}.*" = "ALL PRIVILEGES";
          };
        }) eachSite;
      };

      services.phpfpm = {
        phpPackage = pkgs.php81;
        pools = mapAttrs' (
          hostName: cfg:
          (nameValuePair "invoiceplane-${hostName}" {
            inherit user;
            group = webserver.group;
            settings = {
              "listen.owner" = webserver.user;
              "listen.group" = webserver.group;
            } // cfg.poolConfig;
          })
        ) eachSite;
      };

    }

    {

      systemd.tmpfiles.rules = flatten (
        mapAttrsToList (hostName: cfg: [
          "d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -"
          "f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -"
          "d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -"
          "d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -"
          "d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -"
          "d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -"
          "d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -"
          "d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -"
          "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -"
        ]) eachSite
      );

      systemd.services.invoiceplane-config = {
        serviceConfig.Type = "oneshot";
        script = concatStrings (
          mapAttrsToList (hostName: cfg: ''
            mkdir -p ${cfg.stateDir}/logs \
                     ${cfg.stateDir}/uploads
            if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then
              cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php"
            fi
          '') eachSite
        );
        wantedBy = [ "multi-user.target" ];
      };

      users.users.${user} = {
        group = webserver.group;
        isSystemUser = true;
      };

    }
    {

      # Cron service implementation

      systemd.timers = mapAttrs' (
        hostName: cfg:
        (nameValuePair "invoiceplane-cron-${hostName}" (
          mkIf cfg.cron.enable {
            wantedBy = [ "timers.target" ];
            timerConfig = {
              OnBootSec = "5m";
              OnUnitActiveSec = "5m";
              Unit = "invoiceplane-cron-${hostName}.service";
            };
          }
        ))
      ) eachSite;

      systemd.services = mapAttrs' (
        hostName: cfg:
        (nameValuePair "invoiceplane-cron-${hostName}" (
          mkIf cfg.cron.enable {
            serviceConfig = {
              Type = "oneshot";
              User = user;
              ExecStart = "${pkgs.curl}/bin/curl --header 'Host: ${hostName}' http://localhost/invoices/cron/recur/${cfg.cron.key}";
            };
          }
        ))
      ) eachSite;

    }

    (mkIf (cfg.webserver == "caddy") {
      services.caddy = {
        enable = true;
        virtualHosts = mapAttrs' (
          hostName: cfg:
          (nameValuePair "http://${hostName}" {
            extraConfig = ''
              root * ${pkg hostName cfg}
              file_server
              php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}
            '';
          })
        ) eachSite;
      };
    })

    (mkIf (cfg.webserver == "nginx") {
      services.nginx = {
        enable = true;
        virtualHosts = mapAttrs' (
          hostName: cfg:
          (nameValuePair hostName {
            root = pkg hostName cfg;
            extraConfig = ''
              index index.php index.html index.htm;

              if (!-e $request_filename){
                rewrite ^(.*)$ /index.php break;
              }
            '';

            locations = {
              "/setup".extraConfig =
                let
                  scheme = if config.services.nginx.virtualHosts.${hostName}.forceSSL then "https" else "http";
                in
                ''
                  rewrite ^(.*)$ ${scheme}://${hostName}/ redirect;
                '';

              "~ .php$" = {
                extraConfig = ''
                  fastcgi_split_path_info ^(.+\.php)(/.+)$;
                  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                  fastcgi_pass unix:${config.services.phpfpm.pools."invoiceplane-${hostName}".socket};
                  include ${config.services.nginx.package}/conf/fastcgi_params;
                  include ${config.services.nginx.package}/conf/fastcgi.conf;
                '';
              };
            };
          })
        ) eachSite;
      };
    })
  ]);
}