# Figure out which maintainers (users/teams) are relevant for a PR:
# - All maintainers that can be linked directly to changedFiles
# - Maintainers of affectedAttrPaths if a file directly related to the attribute is in changedFiles
#
# Files and attributes are linked in various ways:
# - pkgs/by-name/<attr>/* is linked to pkgs.<attr>
# - The file position of various attributes of pkgs.<attr>
# - Explicitly specified file positions in derivations
#
# Test with
#   nix-instantiate --eval --strict --json test.nix -A result | jq
#
# Empty list as an output means success

# Dependencies coming from the CI-pinned Nixpkgs
{
  lib,
}:
# Function arguments
{
  # Files that were changed
  # Type: ListOf (Nixpkgs-root-relative path)
  changedFiles,
  # Attributes whose value was affected by the change
  # Type: ListOf (ListOf String)
  affectedAttrPaths,
  # Nixpkgs used to check maintainers. Customisable for testing
  pkgs ? import ../../.. {
    system = "x86_64-linux";
    # We should never try to ping maintainers through package aliases, this can only lead to errors.
    # One example case is, where an attribute is a throw alias, but then re-introduced in a PR.
    # This would trigger the throw. By disabling aliases, we can fallback gracefully below.
    config.allowAliases = false;
    overlays = [ ];
  },
}:
let
  nixpkgsRoot = toString ../../.. + "/";
  stripNixpkgsRootFromKeys = lib.mapAttrs' (
    file: value: lib.nameValuePair (lib.removePrefix nixpkgsRoot file) value
  );

  moduleMeta = (pkgs.nixos { }).config.meta;

  # Currently just nixos module maintainers, but in the future we can use this for code owners too
  fileUsers = stripNixpkgsRootFromKeys moduleMeta.maintainers;
  fileTeams = stripNixpkgsRootFromKeys moduleMeta.teams;

  anyMatchingFile = filename: lib.any (lib.hasPrefix filename) changedFiles;

  anyMatchingFiles = files: lib.any anyMatchingFile files;

  relevantFilenames =
    drv:
    (lib.unique (
      map (pos: lib.removePrefix nixpkgsRoot pos.file) (
        lib.filter (x: x != null) [
          (drv.meta.maintainersPosition or null)
          (drv.meta.teamsPosition or null)
          (lib.unsafeGetAttrPos "src" drv)
          (lib.unsafeGetAttrPos "pname" drv)
          (lib.unsafeGetAttrPos "version" drv)
        ]
        ++ lib.optionals (drv ? meta.position) [
          # Use ".meta.position" for cases when most of the package is
          # defined in a "common" section and the only place where
          # reference to the file with a derivation the "pos"
          # attribute.
          #
          # ".meta.position" has the following form:
          #   "pkgs/tools/package-management/nix/default.nix:155"
          # We transform it to the following:
          #   { file = "pkgs/tools/package-management/nix/default.nix"; }
          { file = lib.head (lib.splitString ":" drv.meta.position); }
        ]
      )
    ));

  relevantAffectedAttrPaths = lib.filter (
    attrPath:
    # Some packages might be reported as changed on a different platform, but
    # not even have an attribute on the platform the maintainers are requested on.
    # Fallback to `null` for these to filter them out
    let
      package = lib.attrByPath attrPath null pkgs;
    in
    package != null && anyMatchingFiles (relevantFilenames package)
  ) affectedAttrPaths;

  # Extract attributes that changed from by-name paths.
  # This allows pinging reviewers for pure refactors.
  changedByNameAttrPaths = lib.pipe changedFiles [
    (lib.filter (changed: lib.hasPrefix "pkgs/by-name/" changed))
    (map (lib.splitString "/"))
    # Filters out e.g. pkgs/by-name/README.md
    (lib.filter (path: lib.length path > 3))
    (map (path: lib.elemAt path 3))
    (map lib.singleton)
    # Filter out new packages
    (lib.filter (attrPath: lib.hasAttrByPath attrPath pkgs))
  ];

  # An attribute can appear in affected *and* touched
  attrPathsToGetMaintainersFor = lib.unique (relevantAffectedAttrPaths ++ changedByNameAttrPaths);

  attrPathEntities = lib.concatMap (
    attrPath:
    let
      package = lib.getAttrFromPath attrPath pkgs;
    in
    # meta.maintainers also contains all individual team members.
    # We only want to ping individuals if they're added individually as maintainers, not via teams.
    userPings { inherit attrPath; } (package.meta.nonTeamMaintainers or [ ])
    ++ lib.concatMap (teamPings { inherit attrPath; }) (package.meta.teams or [ ])
  ) attrPathsToGetMaintainersFor;

  changedFileEntities = lib.concatMap (
    file:
    userPings { inherit file; } (fileUsers.${file} or [ ])
    ++ lib.concatMap (teamPings { inherit file; }) (fileTeams.${file} or [ ])
  ) changedFiles;

  userPings =
    context:
    map (maintainer: {
      type = "user";
      userId = maintainer.githubId;
      inherit context;
    });

  teamPings =
    context: team:
    if team ? githubId then
      [
        {
          type = "team";
          teamId = team.githubId;
          inherit context;
        }
      ]
    else
      userPings context team.members;

  byType = lib.groupBy (ping: ping.type) (attrPathEntities ++ changedFileEntities);

  byUser = lib.pipe (byType.user or [ ]) [
    (lib.groupBy (ping: toString ping.userId))
    (lib.mapAttrs (_user: lib.map (pkg: pkg.context)))
  ];
  byTeam = lib.pipe (byType.team or [ ]) [
    (lib.groupBy (ping: toString ping.teamId))
    (lib.mapAttrs (_team: lib.map (pkg: pkg.context)))
  ];
in
{
  users = byUser;
  teams = byTeam;
  packages = attrPathsToGetMaintainersFor;
}
