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

  nativeMessagingHostNames = lib.flatten (
    lib.mapAttrsToList
      (_: addonOpts: lib.optionals addonOpts.enable addonOpts.nativeMessagingHosts)
      cfg.addons
  );
  nativeMessagingPrograms = lib.map (n: config.sane.programs."${n}") nativeMessagingHostNames;
  nativeMessagingHosts = lib.map (p: p.package) nativeMessagingPrograms;

  addonSuggestedProgramNames = lib.flatten (
    lib.mapAttrsToList
      (_: addonOpts: lib.optionals addonOpts.enable addonOpts.suggestedPrograms)
      cfg.addons
  );
  addonSuggestedPrograms = lib.map (n: config.sane.programs."${n}") addonSuggestedProgramNames;
  addonHomePaths = lib.concatMap (p: p.sandbox.extraHomePaths) (addonSuggestedPrograms ++ nativeMessagingPrograms);

  packageUnwrapped = let
    unwrapped = pkgs.firefox-unwrapped // {
      # i configure these post wrapping (below), but the wrapper errors if it thinks the browser was built
      # with a different config
      allowAddonSideload = true;
      requireSigning = false;
    };
  in (pkgs.wrapFirefox unwrapped {
    # inherit the default librewolf.cfg
    # it can be further customized via ~/.librewolf/librewolf.overrides.cfg
    libName = "firefox";
    inherit nativeMessagingHosts;

    nixExtensions = lib.concatMap (ext: lib.optional ext.enable ext.package) (builtins.attrValues cfg.addons);

    extraPrefsFiles = [
      "${pkgs.arkenfox-userjs}/user.cfg"
      (pkgs.writeText "mozilla.cfg" ''
        // load additional preferences from user directory; inspired by librewolf
        let home_dir;
        if (home_dir = getenv('HOME')) {
          defaultPref('autoadmin.global_config_url', `file://''${home_dir}/.mozilla/firefox/user.js`);
        }
      '')
    ]; # ++ pkgs.librewolf-pmos-mobile.extraPrefsFiles

    extraPolicies = {
      # firefox policy schema is documented in <repo:mozilla-firefox/firefox:browser/components/enterprisepolicies/schemas/policies-schema.json>
      # and <https://mozilla.github.io/policy-templates>
      # however the bulk of them seem to not actually have any effect T_T
      #
      # XXX(2024-12-02): using `nixExtensions` causes `about:debugging` to be blocked.
      # i guess this is because the page can install extensions, or something.
      # fuck that, enable it by brute force
      ExtensionSettings = {
        "*" = {
          installation_mode = "allowed";
        };
      };
      PasswordManagerEnabled = false;  #< TODO(2025-05-31): does this actually have an effect?
      # see also ~/.mozilla/firefox/default/search.json.mozlz4
      # (use mozlz4a package to view)
      # XXX(2025-05-31): the below SearchEngines policy has no observable effect.
      # SearchEngines = {
      #   Add = [];
      #   # Default = "search@kagi.comdefault";
      #   Default = "ddg";  #< builtin
      #   DefaultPrivate = "ddg";  #< builtin
      #   PreventInstalls = false;
      #   Remove = [];
      # };
    };
  }).overrideAttrs (base: {
    nativeBuildInputs = (base.nativeBuildInputs or []) ++ [
      pkgs.copyDesktopItems
      pkgs.gnused
      pkgs.unzip
      pkgs.zip
    ];
    desktopItems = (base.desktopItems or []) ++ [
      (pkgs.makeDesktopItem {
        name = "firefox-in-vpn";
        desktopName = "Firefox (VPN)";
        genericName = "Web Browser";
        # N.B.: --new-instance ensures we don't reuse an existing differenty-namespaced instance.
        # OTOH, it may error about "only one instance can run at a time": close the other instance if you see that.
        exec = "${lib.getExe pkgs.sane-scripts.vpn} do default -- firefox --new-instance";
        icon = "firefox";
        categories = [ "Network" "WebBrowser" ];
        type = "Application";
      })
      (pkgs.makeDesktopItem {
        name = "firefox-stub-dns";
        desktopName = "Firefox (Stub DNS)";
        genericName = "Web Browser";
        # N.B.: --new-instance ensures we don't reuse an existing differently-namespaced instance.
        # OTOH, it may error about "only one instance can run at a time": close the other instance if you see that.
        exec = "${lib.getExe pkgs.sane-scripts.vpn} do none -- firefox --new-instance";
        icon = "firefox";
        categories = [ "Network" "WebBrowser" ];
        type = "Application";
      })
    ];

    # TODO: could use `zip -f` to only update the one changed file, instead of rezipping everything.
    buildCommand = (base.buildCommand or "") + ''
      patchOmni() {
        local name="$1"
        local ja="$2"

        mkdir $name
        echo "$ja: BEFORE:"
        ls -l $(readlink $out/lib/firefox/$ja)

        echo "unzipping $ja"
        # N.B. `zip` exits non-zero even on successful extraction, if the file didn't 100% obey spec
        unzip $out/lib/firefox/$ja -d $name || true

        echo "removing old $ja"
        rm $out/lib/firefox/$ja

        (
          pushd $name
          echo "patching $ja"
          patch_''${name}_hook

          echo "re-zipping $ja"
          zip $out/lib/firefox/$ja -r ./*
          popd
        )

        echo "$ja: AFTER:"
        ls -l $out/lib/firefox/$ja
      }

      patch_browser_omni_hook() {
        # de-associate `ctrl+shift+c` from activating the devtools.
        # see: <https://stackoverflow.com/a/54260938>
        sed -i s'/devtools-commandkey-inspector = C/devtools-commandkey-inspector = VK_F12/' localization/en-US/devtools/startup/key-shortcuts.ftl

        # remap Close Tab shortcut from Ctrl+W to Ctrl+Shift+W
        # see: <https://www.math.cmu.edu/~gautam/sj/blog/20220329-firefox-disable-ctrl-w.html>
        sed -i s'/command="cmd_close" modifiers="accel"/command="cmd_close" modifiers="accel,shift"/' chrome/browser/content/browser/browser.xhtml
      }

      patch_toplevel_omni_hook() {
        # allow loading "unsigned" addons (i.e. the ones i fucking build FROM SOURCE).
        # required for Mozilla firefox; not for Librewolf
        # alternative implementations require a rebuild:
        # - nixpkgs `allowAddonSideload = true;`
        # - configure flag `--allow-addond-sideload`
        sed -i s'/MOZ_ALLOW_ADDON_SIDELOAD:/MOZ_ALLOW_ADDON_SIDELOAD: true, _OLD_MOZ_ALLOW_ADDON_SIDELOAD:/' modules/AppConstants.sys.mjs
        sed -i s'/MOZ_REQUIRE_SIGNING:/MOZ_REQUIRE_SIGNING: false, _OLD_MOZ_REQUIRE_SIGNING:/' modules/AppConstants.sys.mjs
      }

      patchOmni "browser_omni" "browser/omni.ja"
      patchOmni "toplevel_omni" "omni.ja"

      runHook postBuild
      runHook postInstall
      runHook postFixup
    '';
  });
in
{
  imports = [
    ./addons.nix
    ./browserpass.nix
    ./passff-host.nix
  ];

  sane.programs.firefox = {
    configOption = with lib; mkOption {
      default = {};
      type = types.submodule {
        options = {
          browser = mkOption {
            default = defaultSettings;
            type = types.anything;
          };
          persistData = mkOption {
            description = "optional store name to which persist browsing data (like history)";
            type = types.nullOr types.str;
            default = null;
          };
          persistCache = mkOption {
            description = "optional store name to which persist browser cache";
            type = types.nullOr types.str;
            default = "ephemeral";
          };
          addons = mkOption {
            default = {};
            type = types.attrsOf (types.submodule ({ name, ...}: {
              options = {
                enable = mkEnableOption "enable the ${name} Firefox addon";
                package = mkPackageOption pkgs.firefox-extensions name {};
                nativeMessagingHosts = mkOption {
                  type = types.listOf types.str;
                  default = [];
                };
                suggestedPrograms = mkOption {
                  type = types.listOf types.str;
                  default = [];
                };
              };
            }));
          };
          formFactor = mkOption {
            default = "other";
            type = types.enum [
              "desktop"
              "laptop"
              "other"
            ];
            description = ''
              tune the browser experience for a specific form factor
            '';
          };
        };
      };
    };

    inherit packageUnwrapped;

    suggestedPrograms = nativeMessagingHostNames ++ addonSuggestedProgramNames;

    sandbox.net = "all";
    sandbox.whitelistAudio = true;
    sandbox.whitelistAvDev = true;  #< it doesn't seem to use pipewire, but direct /dev/videoN (as of 2024/09/12)
    sandbox.whitelistDbus.user.own = [
      "org.mozilla.firefox.*"
      "org.mpris.MediaPlayer2.firefox.*"
    ];
    sandbox.whitelistPortal = [
      "Camera"  # not sure if used
      # "Email"  # not sure if used
      "FileChooser"
      "Location"  # not sure if used
      "OpenURI"
      "Print"  # not sure if used
      "ScreenCast"  # not sure if used
    ];
    sandbox.whitelistSecurityKeys = true;
    sandbox.whitelistSendNotifications = true;
    sandbox.whitelistWayland = true;
    sandbox.extraHomePaths = [
      "dev"  # for developing anything web-related
      # for uploads/downloads.
      # it still needs these paths despite using the portal's file-chooser :?
      "tmp"
      "Pictures/albums"
      "Pictures/cat"
      "Pictures/from"
      "Pictures/Photos"
      "Pictures/Screenshots"
      "Pictures/servo-macros"
    ] ++ addonHomePaths;

    sandbox.tmpDir = ".cache/mozilla/tmp";
    sandbox.mesaCacheDir = ".cache/mozilla/mesa";

    mime.associations = let
      desktop = "firefox.desktop";
    in {
      "text/html" = desktop;
      "x-scheme-handler/http" = desktop;
      "x-scheme-handler/https" = desktop;
      "x-scheme-handler/about" = desktop;
      "x-scheme-handler/unknown" = desktop;
    };

    env.BROWSER = "firefox";  # used by misc tools like xdg-email, as fallback

    fs.".mozilla/firefox/bookmarks.html".symlink.target = ./bookmarks.html;
    # instruct Firefox to put the profile in a predictable directory (so we can do things like persist just it).
    # XXX: the directory *must* exist, even if empty; Firefox will not create the directory itself.
    fs.".mozilla/firefox/profiles.ini".symlink.text = ''
      [Profile0]
      Name=default
      IsRelative=1
      Path=default
      Default=1

      [General]
      StartWithLastProfile=1
    '';

    fs.".mozilla/firefox/user.js".symlink.target = ./user.js;
    fs.".mozilla/firefox/default/search.json.mozlz4".symlink.target = let
      drv = pkgs.stdenvNoCC.mkDerivation {
        # Mozilla uses a custom compression scheme for `search.json` because they're ASSHOLES.
        # seriously, why do you need to compress this 2 KiB file? and then why do you need to INVENT YOUR OWN COMPRESSION FORMAT FOR THAT?
        # GET YOUR HEAD OUT OF YOUR ASS.
        #
        # search.json fields:
        # - `metaData.defaultEngineIdHash`: this is a digest of the defaultEngineId _value_, plus some legalese text;
        #   compute this either by manually updating the search engine in firefox and seeing what it generates,
        #   or, see the code for it in <repo:nix-community/home-manager>
        name = "search.json.mozlz4";
        src = ./.;
        nativeBuildInputs = with pkgs; [ mozlz4a ];
        buildPhase = ''
          mozlz4a ./search.json search.json.mozlz4
        '';
        installPhase = ''
          install -D search.json.mozlz4 "$out"/search.json.mozlz4
        '';
      };
    in "${drv}/search.json.mozlz4";

    # flush the cache to disk to avoid it taking up too much tmp.
    persist.byPath = let
      maybePersist = if (cfg.persistData != null) then
        cfg.persistData
      else
        "ephemeral"
      ;
    in {
      ".cache/mozilla".store = maybePersist;
      # storage consumes 80+ MB
      # the other profile dirs take significantly less; < 20MB total
      ".mozilla/firefox/default/storage".store = maybePersist;
      # profile files (.mozilla/firefox/default/...):
      # - addons.json
      #   - regenerated on launch
      # - addonStartup.json.lz4
      #   - regenerated on next-next launch
      # - bookmarkbackups/
      # - bounce-tracking-protection.sqlite
      #   - recreated on launch
      # - cert9.db
      #   - regenerated on launch
      # - compatibility.ini
      #   - regenerated on launch
      # - containers.json
      #   - regenerated on launch
      # - content-prefs.sqlite
      #   - recreated on launch
      # - cookies.sqlite{,-wal}
      #   - recreated on launch
      # - crashes/
      # - datareporting/
      # - domain_to_categories.sqlite{,-journal}
      #   - recreated on launch
      # - enumerate_devices.txt
      #   - recreated on launch
      # - extension-preferences.json
      #   - NOT recreated, but inconsequential to delete
      # - extension-settings.json
      #   - regenerated on launch
      # - extension-store/
      # - extension-store-menus/
      # - extensions/
      # - extensions.json
      #   - regenerated on launch
      # - failover.jsc
      #   - regenerated on launch
      # - favicons.sqlite{,-wal}
      #   - regenerated on launch
      # - formhistory.sqlite
      #   - recreated on launch
      # - handlers.json
      #   - NOT recreated, but inconsequential to delete
      # - key4.db
      #   - recreated on launch
      # - lock -> ...
      # - minidumps/
      # - permissions.sqlite
      #   - recreated on launch
      # - pkcs11.txt
      #   - regenerated on launch
      # - places.sqlite{,-wal}
      #   - recreated on launch
      # - prefs.js
      #   - recreated on launch
      # - protections.sqlite
      #   - recreated on launch
      # - search.json.mozlz4
      #   - recreated on launch; loses any customized search engines
      # - security_state/
      # - serviceworker.txt
      #   - NOT recreated, but inconsequential to delete
      # - sessionCheckpoints.json
      #   - recreated on launch
      # - shield-preference-experiments.json
      #   - NOT recreated, but inconsequential to delete
      # - storage/
      # - storage-sync-v2.sqlite
      #   - recreated on launch
      # - storage.sqlite
      # - times.json
      #   - recreated on launch
      # - xulstore.json
      #   - recreated on launch
    };
  };
}
