{
  lib,
  config,
  pkgs,
  ...
}:
let
  cfg = config.programs.anki;

  # Convert Nix `nullOr bool` to Python types.
  pyOptionalBool =
    val:
    if val == null then
      "None"
    else if val then
      "True"
    else
      "False";
  # This script generates the Anki SQLite settings DB using the Anki Python API.
  # The configuration options in the SQLite database take the form of Python
  # Pickle data.
  # A simple "gldriver6" file is also generated for the `videoDriver` option.
  buildAnkiConfig = pkgs.writers.writeText "buildAnkiConfig" ''
    import sys

    from aqt.profiles import ProfileManager, VideoDriver
    from aqt.theme import Theme, WidgetStyle, theme_manager
    from aqt.toolbar import HideMode

    profile_manager = ProfileManager(
      ProfileManager.get_created_base_folder(sys.argv[1])
    )
    _ = profile_manager.setupMeta()
    profile_manager.meta["firstRun"] = False

    # Video driver. Option is stored in a separate file from other options.
    video_driver_str: str = "${toString cfg.videoDriver}"
    if video_driver_str:
      # The enum value for OpenGL isn't "opengl"
      if video_driver_str == "opengl":
        video_driver = VideoDriver.OpenGL
      else:
        video_driver = VideoDriver(video_driver_str)
      profile_manager.set_video_driver(video_driver)


    # Shared options

    profile_manager.setLang("${cfg.language}")

    theme_str: str = "${toString cfg.theme}"
    if theme_str:
      theme: Theme = {
        "followSystem": Theme.FOLLOW_SYSTEM,
        "light": Theme.LIGHT,
        "dark": Theme.DARK
      }[theme_str]
      profile_manager.set_theme(theme)

    style_str: str = "${toString cfg.style}"
    if style_str:
      style: WidgetStyle = {
        "anki": WidgetStyle.ANKI, "native": WidgetStyle.NATIVE
      }[style_str]
      # Fix error from there being no main window to update the style of
      theme_manager.apply_style = lambda: None
      profile_manager.set_widget_style(style)

    ui_scale_str: str = "${toString cfg.uiScale}"
    if ui_scale_str:
      profile_manager.setUiScale(float(ui_scale_str))

    hide_top_bar: bool | None = ${pyOptionalBool cfg.hideTopBar}
    if hide_top_bar is not None:
      profile_manager.set_hide_top_bar(hide_top_bar)

    hide_top_bar_mode_str: str = "${toString cfg.hideTopBarMode}"
    if hide_top_bar_mode_str:
      hide_mode: HideMode = {
        "fullscreen": HideMode.FULLSCREEN,
        "always": HideMode.ALWAYS,
      }[hide_top_bar_mode_str]
      profile_manager.set_top_bar_hide_mode(hide_mode)

    hide_bottom_bar: bool | None = ${pyOptionalBool cfg.hideBottomBar}
    if hide_bottom_bar is not None:
      profile_manager.set_hide_bottom_bar(hide_bottom_bar)

    hide_bottom_bar_mode_str: str = "${toString cfg.hideBottomBarMode}"
    if hide_bottom_bar_mode_str:
      hide_mode: HideMode = {
        "fullscreen": HideMode.FULLSCREEN,
        "always": HideMode.ALWAYS,
      }[hide_bottom_bar_mode_str]
      profile_manager.set_bottom_bar_hide_mode(hide_mode)

    reduce_motion: bool | None = ${pyOptionalBool cfg.reduceMotion}
    if reduce_motion is not None:
      profile_manager.set_reduce_motion(reduce_motion)

    minimalist_mode: bool | None = ${pyOptionalBool cfg.minimalistMode}
    if minimalist_mode is not None:
      profile_manager.set_minimalist_mode(minimalist_mode)

    spacebar_rates_card: bool | None = ${pyOptionalBool cfg.spacebarRatesCard}
    if spacebar_rates_card is not None:
      profile_manager.set_spacebar_rates_card(spacebar_rates_card)

    legacy_import_export: bool | None = ${pyOptionalBool cfg.legacyImportExport}
    if legacy_import_export is not None:
      profile_manager.set_legacy_import_export(legacy_import_export)

    answer_keys: tuple[tuple[int, str], ...] = (${
      lib.strings.concatMapStringsSep ", " (val: "(${toString val.ease}, '${val.key}')") cfg.answerKeys
    })
    for ease, key in answer_keys:
      profile_manager.set_answer_key(ease, key)

    # Profile specific options

    profile_manager.create("User 1")
    profile_manager.openProfile("User 1")

    # Without this, the collection DB won't get automatically optimized.
    profile_manager.profile["lastOptimize"] = None

    auto_sync: bool | None = ${pyOptionalBool cfg.sync.autoSync}
    if auto_sync is not None:
      profile_manager.profile["autoSync"] = auto_sync

    sync_media: bool | None = ${pyOptionalBool cfg.sync.syncMedia}
    if sync_media is not None:
      profile_manager.profile["syncMedia"] = sync_media

    media_sync_minutes_str: str = "${toString cfg.sync.autoSyncMediaMinutes}"
    if media_sync_minutes_str:
      profile_manager.set_periodic_sync_media_minutes = int(media_sync_minutes_str)

    network_timeout_str: str = "${toString cfg.sync.networkTimeout}"
    if network_timeout_str:
      profile_manager.set_network_timeout = int(network_timeout_str)

    profile_manager.save()
  '';
in
{
  ankiConfig =
    let
      cfgAnkiPython = (
        lib.lists.findSingle (x: x.isPy3 or false) null null (cfg.package.nativeBuildInputs or [ ])
      );
      ankiPackage = if cfgAnkiPython == null then pkgs.anki else cfg.package;
      ankiPython = if cfgAnkiPython == null then pkgs.python3 else cfgAnkiPython;
    in
    pkgs.runCommand "ankiConfig"
      {
        nativeBuildInputs = [ ankiPackage ];
      }
      ''
        ${ankiPython.interpreter} ${buildAnkiConfig} $out
      '';

  # An Anki add-on is used for sync settings, so the secrets can be
  # retrieved at runtime.
  syncConfigAnkiAddon = pkgs.anki-utils.buildAnkiAddon {
    pname = "hm-sync-config";
    version = "1.0";
    src = pkgs.writeTextDir "__init__.py" ''
      import aqt
      from pathlib import Path

      username: str | None = ${if cfg.sync.username == null then "None" else "'${cfg.sync.username}'"}
      username_file: Path | None = ${
        if cfg.sync.usernameFile == null then "None" else "Path('${cfg.sync.usernameFile}')"
      }
      key_file: Path | None = ${
        if cfg.sync.keyFile == null then "None" else "Path('${cfg.sync.keyFile}')"
      }
      custom_sync_url: str | None = ${if cfg.sync.url == null then "None" else "'${cfg.sync.url}'"}

      def set_server() -> None:
          if custom_sync_url:
            aqt.mw.pm.set_custom_sync_url(custom_sync_url)
          if username:
            aqt.mw.pm.set_sync_username(username)
          elif username_file and username_file.exists():
              aqt.mw.pm.set_sync_username(username_file.read_text().strip())
          if key_file and key_file.exists():
              aqt.mw.pm.set_sync_key(key_file.read_text().strip())

      aqt.gui_hooks.profile_did_open.append(set_server)
    '';
  };

  # Make Anki work better with declarative settings. See script for specific changes.
  homeManagerAnkiAddon = pkgs.anki-utils.buildAnkiAddon {
    pname = "home-manager";
    version = "1.0";
    src = pkgs.writeTextDir "__init__.py" ''
      import aqt
      from aqt.qt import QWidget, QMessageBox
      from anki.hooks import wrap
      from typing import Any

      def make_config_differences_str(initial_config: dict[str, Any],
                                      new_config: dict[str, Any]) -> str:
        details = ""
        for key, val in new_config.items():
            initial_val = initial_config.get(key)
            if val != initial_val:
              details += f"{key} changed from `{initial_val}` to `{val}`\n"
        return details

      def dialog_did_open(dialog_manager: aqt.DialogManager,
                          dialog_name: str,
                          dialog_instance: QWidget) -> None:
        if dialog_name != "Preferences":
          return

        # Make sure defaults are loaded before copying the initial configs
        dialog_instance.update_global()
        dialog_instance.update_profile()
        initial_meta = aqt.mw.pm.meta.copy()
        initial_profile_conf = aqt.mw.pm.profile.copy()

        def on_preferences_save() -> None:
          aqt.mw.pm.save = lambda: None

          details = make_config_differences_str(initial_meta, aqt.mw.pm.meta)
          details += make_config_differences_str(initial_profile_conf,
                                                 aqt.mw.pm.profile)
          if not details:
            return

          message_box = QMessageBox(
            QMessageBox.Icon.Warning,
            "NixOS Info",
            ("Anki settings are currently being managed by Home Manager.<br>"
             "Changes to certain settings won't be saved.")
          )
          message_box.setDetailedText(details)
          message_box.exec()

        aqt.mw.pm.save = on_preferences_save

      def state_will_change(new_state: aqt.main.MainWindowState,
                            old_state: aqt.main.MainWindowState):
        if new_state != "profileManager":
          return

        QMessageBox.warning(
          aqt.mw,
          "NixOS Info",
          ("Profiles cannot be changed or added while settings are managed with "
           "Home Manager.")
        )


      # Ensure Anki doesn't try to save to the read-only DB settings file.
      aqt.mw.pm.save = lambda: None

      # Tell the user when they try to change settings that won't be persisted.
      aqt.gui_hooks.dialog_manager_did_open_dialog.append(dialog_did_open)

      # Show warning when users try to switch or customize profiles.
      aqt.gui_hooks.state_will_change.append(state_will_change)
    '';
  };
}
