{
  config,
  lib,
  pkgs,
  ...
}:
let
  cfg = config.services.castopod;
  fpm = config.services.phpfpm.pools.castopod;

  user = "castopod";

  # https://docs.castopod.org/getting-started/install.html#requirements
  phpPackage = pkgs.php82.withExtensions (
    { enabled, all }:
    with all;
    [
      intl
      curl
      mbstring
      gd
      exif
      mysqlnd
    ]
    ++ enabled
  );
in
{
  meta.doc = ./castopod.md;
  meta.maintainers = with lib.maintainers; [ alexoundos ];

  options.services = {
    castopod = {
      enable = lib.mkEnableOption "Castopod, a hosting platform for podcasters";
      package = lib.mkPackageOption pkgs "castopod" { };
      dataDir = lib.mkOption {
        type = lib.types.path;
        default = "/var/lib/castopod";
        description = ''
          The path where castopod stores all data. This path must be in sync
          with the castopod package (where it is hardcoded during the build in
          accordance with its own `dataDir` argument).
        '';
      };
      database = {
        createLocally = lib.mkOption {
          type = lib.types.bool;
          default = true;
          description = ''
            Create the database and database user locally.
          '';
        };
        hostname = lib.mkOption {
          type = lib.types.str;
          default = "localhost";
          description = "Database hostname.";
        };
        name = lib.mkOption {
          type = lib.types.str;
          default = "castopod";
          description = "Database name.";
        };
        user = lib.mkOption {
          type = lib.types.str;
          default = user;
          description = "Database user.";
        };
        passwordFile = lib.mkOption {
          type = lib.types.nullOr lib.types.path;
          default = null;
          example = "/run/keys/castopod-dbpassword";
          description = ''
            A file containing the password corresponding to
            [](#opt-services.castopod.database.user).

            This file is loaded using systemd LoadCredentials.
          '';
        };
      };
      settings = lib.mkOption {
        type =
          with lib.types;
          attrsOf (oneOf [
            str
            int
            bool
          ]);
        default = { };
        example = {
          "email.protocol" = "smtp";
          "email.SMTPHost" = "localhost";
          "email.SMTPUser" = "myuser";
          "email.fromEmail" = "castopod@example.com";
        };
        description = ''
          Environment variables used for Castopod.
          See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
          for available environment variables.
        '';
      };
      environmentFile = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        default = null;
        example = "/run/keys/castopod-env";
        description = ''
          Environment file to inject e.g. secrets into the configuration.
          See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
          for available environment variables.

          This file is loaded using systemd LoadCredentials.
        '';
      };
      configureNginx = lib.mkOption {
        type = lib.types.bool;
        default = true;
        description = "Configure nginx as a reverse proxy for CastoPod.";
      };
      localDomain = lib.mkOption {
        type = lib.types.str;
        example = "castopod.example.org";
        description = "The domain serving your CastoPod instance.";
      };
      poolSettings = lib.mkOption {
        type =
          with lib.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 Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
        '';
      };
      maxUploadSize = lib.mkOption {
        type = lib.types.str;
        default = "512M";
        description = ''
          Maximum supported size for a file upload in. Maximum HTTP body
          size is set to this value for nginx and PHP (because castopod doesn't
          support chunked uploads yet:
          https://code.castopod.org/adaures/castopod/-/issues/330).

          Note, that practical upload size limit is smaller. For example, with
          512 MiB setting - around 500 MiB is possible.
        '';
      };
    };
  };

  config = lib.mkIf cfg.enable {
    services.castopod.settings =
      let
        sslEnabled =
          with config.services.nginx.virtualHosts.${cfg.localDomain};
          addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null;
        baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}";
      in
      lib.mapAttrs (_: lib.mkDefault) {
        "app.forceGlobalSecureRequests" = sslEnabled;
        "app.baseURL" = baseURL;

        "media.baseURL" = baseURL;
        "media.root" = "media";
        "media.storage" = cfg.dataDir;

        "admin.gateway" = "admin";
        "auth.gateway" = "auth";

        "database.default.hostname" = cfg.database.hostname;
        "database.default.database" = cfg.database.name;
        "database.default.username" = cfg.database.user;
        "database.default.DBPrefix" = "cp_";

        "cache.handler" = "file";
      };

    services.phpfpm.pools.castopod = {
      inherit user;
      group = config.services.nginx.group;
      inherit phpPackage;
      phpOptions = ''
        # https://code.castopod.org/adaures/castopod/-/blob/develop/docker/production/common/uploads.template.ini
        file_uploads = On
        memory_limit = 512M
        upload_max_filesize = ${cfg.maxUploadSize}
        post_max_size = ${cfg.maxUploadSize}
        max_execution_time = 300
        max_input_time = 300
      '';
      settings = {
        "listen.owner" = config.services.nginx.user;
        "listen.group" = config.services.nginx.group;
      }
      // cfg.poolSettings;
    };

    systemd.services.castopod-setup = {
      after = lib.optional config.services.mysql.enable "mysql.service";
      requires = lib.optional config.services.mysql.enable "mysql.service";
      wantedBy = [ "multi-user.target" ];
      path = [
        pkgs.openssl
        phpPackage
      ];
      script =
        let
          envFile = "${cfg.dataDir}/.env";
          media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}";
        in
        ''
          mkdir -p ${cfg.dataDir}/writable/{cache,logs,session,temp,uploads}

          if [ ! -d ${lib.escapeShellArg media} ]; then
            cp --no-preserve=mode,ownership -r ${cfg.package}/share/castopod/public/media ${lib.escapeShellArg media}
          fi

          if [ ! -f ${cfg.dataDir}/salt ]; then
            openssl rand -base64 33 > ${cfg.dataDir}/salt
          fi

          cat <<'EOF' > ${envFile}
          ${lib.generators.toKeyValue { } cfg.settings}
          EOF

          echo "analytics.salt=$(cat ${cfg.dataDir}/salt)" >> ${envFile}

          ${
            if (cfg.database.passwordFile != null) then
              ''
                echo "database.default.password=$(<"$CREDENTIALS_DIRECTORY/dbpasswordfile")" >> ${envFile}
              ''
            else
              ''
                echo "database.default.password=" >> ${envFile}
              ''
          }

          ${lib.optionalString (cfg.environmentFile != null) ''
            cat "$CREDENTIALS_DIRECTORY/envfile" >> ${envFile}
          ''}

          php ${cfg.package}/share/castopod/spark castopod:database-update
        '';
      serviceConfig = {
        StateDirectory = "castopod";
        LoadCredential =
          lib.optional (cfg.environmentFile != null) "envfile:${cfg.environmentFile}"
          ++ (lib.optional (cfg.database.passwordFile != null) "dbpasswordfile:${cfg.database.passwordFile}");
        WorkingDirectory = "${cfg.package}/share/castopod";
        Type = "oneshot";
        RemainAfterExit = true;
        User = user;
        Group = config.services.nginx.group;
        ReadWritePaths = cfg.dataDir;
      };
    };

    systemd.services.castopod-scheduled = {
      after = [ "castopod-setup.service" ];
      wantedBy = [ "multi-user.target" ];
      path = [ phpPackage ];
      script = ''
        php ${cfg.package}/share/castopod/spark tasks:run
      '';
      serviceConfig = {
        StateDirectory = "castopod";
        WorkingDirectory = "${cfg.package}/share/castopod";
        Type = "oneshot";
        User = user;
        Group = config.services.nginx.group;
        ReadWritePaths = cfg.dataDir;
        LogLevelMax = "notice"; # otherwise periodic tasks flood the journal
      };
    };

    systemd.timers.castopod-scheduled = {
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnCalendar = "*-*-* *:*:00";
        Unit = "castopod-scheduled.service";
      };
    };

    services.mysql = lib.mkIf cfg.database.createLocally {
      enable = true;
      package = lib.mkDefault pkgs.mariadb;
      ensureDatabases = [ cfg.database.name ];
      ensureUsers = [
        {
          name = cfg.database.user;
          ensurePermissions = {
            "${cfg.database.name}.*" = "ALL PRIVILEGES";
          };
        }
      ];
    };

    services.nginx = lib.mkIf cfg.configureNginx {
      enable = true;
      virtualHosts."${cfg.localDomain}" = {
        root = lib.mkForce "${cfg.package}/share/castopod/public";

        extraConfig = ''
          try_files $uri $uri/ /index.php?$args;
          index index.php index.html;
          client_max_body_size ${cfg.maxUploadSize};
        '';

        locations."^~ /${cfg.settings."media.root"}/" = {
          root = cfg.settings."media.storage";
          extraConfig = ''
            add_header Access-Control-Allow-Origin "*";
            expires max;
            access_log off;
          '';
        };

        locations."~ \\.php$" = {
          fastcgiParams = {
            SERVER_NAME = "$host";
          };
          extraConfig = ''
            fastcgi_intercept_errors on;
            fastcgi_index index.php;
            fastcgi_pass unix:${fpm.socket};
            try_files $uri =404;
            fastcgi_read_timeout 3600;
            fastcgi_send_timeout 3600;
          '';
        };
      };
    };

    users.users.${user} = lib.mapAttrs (_: lib.mkDefault) {
      description = "Castopod user";
      isSystemUser = true;
      group = config.services.nginx.group;
    };
  };
}
