{ lib
, stdenv
, callPackage
, dpkg
, fetchurl
, recurseIntoAttrs

, sqlite
, elfutils
}:

let
  inherit (builtins)
    elem attrNames attrValues concatMap filter fromJSON getAttr groupBy head isNull listToAttrs map mapAttrs readFile replaceStrings splitVersion ;
  inherit (lib)
    converge findFirst groupBy' hasPrefix optional pipe take toInt toList versionAtLeast versionOlder ;

  aptData = fromJSON (readFile ./packages.json);

  # Compare versions in debian control file syntax
  # See: https://www.debian.org/doc/debian-policy/ch-relationships.html#syntax-of-relationship-fields
  #
  # NOTE: this is not a proper version comparison
  #
  # A proper version solver, should aggregate dependencies with the same name
  # and compute the constraint (e.g. a (>= 2) a (<< 5) -> 2 <= a << 5)
  #
  # But in the intel repo, there are no such "duplicated" dependencies to specify
  # upper limits, which leads to issues when intel-hpckit-2021 depends on things
  # like intel-basekit >= 2021.1.0-2403 and we end up installing the newest
  # basekit instead of the one from 2021.
  #
  # To mitigate this, >= is set to take the latest version with matching major
  # and minor (only revision and patch are allowed to change)
  compareVersions = got: kind: want:
    let
      g0 = take 2 (splitVersion got);
      w0 = take 2 (splitVersion want);
    in if isNull want then true
    else if kind == "=" then got == want
    else if kind == "<<" then versionOlder got want
    else if kind == "<=" then versionAtLeast want got
    else if kind == ">>" then versionOlder want got
    else if kind == ">=" then (g0 == w0) && versionAtLeast got want # always match major version
    else throw "unknown operation: ${kind}";

  findMatching = { pname, kind, version }:
    findFirst (x: pname == x.pname && compareVersions x.version kind version) null aptData;

  isIntel = pkg: (hasPrefix "intel-" pkg.pname);

  expandDeps = pkg: (map findMatching (filter isIntel pkg.dependencies)) ++ (optional (pkg.size != 0) pkg);

  # get the oldest by major version. If they have the same major version, take
  # the newest. This prevents most issues with resolutions
  # versionOlder b a -> true if b is older than a (b `older` a)
  getNewerInMajor = a: b: let
    va = a.version;
    vb = b.version;
    va0 = head (splitVersion va);
    vb0 = head (splitVersion vb);
  in
  if isNull a then b
  else if va0 != vb0 then
    if va0 > vb0 then b else a
  else if versionOlder vb va then a else b;
  removeDups = l: attrValues (groupBy' getNewerInMajor null (getAttr "provides") l);

  _resolveDeps = converge (l: removeDups (concatMap expandDeps l));
  resolveDeps = pkg: let deps = _resolveDeps (toList pkg);
      namedDeps = (map (x: "${x.pname}-${x.version}") deps);
    in builtins.traceVerbose (builtins.deepSeq namedDeps namedDeps) deps;

  blacklist = [
    "intel-basekit-env"
    "intel-basekit-getting-started"
    "intel-hpckit-env"
    "intel-hpckit-getting-started"
    "intel-oneapi-advisor"
    "intel-oneapi-common-licensing"
    "intel-oneapi-common-oneapi-vars"
    "intel-oneapi-common-vars"
    "intel-oneapi-compiler-cpp-eclipse-cfg"
    "intel-oneapi-compiler-dpcpp-eclipse-cfg"
    "intel-oneapi-condaindex"
    "intel-oneapi-dev-utilities-eclipse-cfg"
    "intel-oneapi-dpcpp-ct-eclipse-cfg"
    "intel-oneapi-eclipse-ide"
    "intel-oneapi-hpc-toolkit-getting-started"
    "intel-oneapi-icc-eclipse-plugin-cpp"
    "intel-oneapi-vtune"
    "intel-oneapi-vtune-eclipse-plugin-vtune"
  ];

  isInBlacklist = pkg: elem pkg.provides blacklist;
  removeBlacklist = filter (e: !(isInBlacklist e));

  dpkgExtractAll = pname: version: {srcs, deps}: stdenv.mkDerivation {
    inherit pname version srcs;

    nativeBuildInputs = [ dpkg ];
    phases = [ "installPhase" ];

    passthru = { inherit deps; };

    installPhase = ''
      mkdir -p $out
      for src in $srcs; do
        echo "Unpacking $src"
        dpkg -x $src $out
      done
    '';
  };

  apthost = "https://apt.repos.intel.com/oneapi/";
  fetchDeb = p: fetchurl { url = apthost + p.filename; inherit (p) sha256; };

  buildIntel = pkg: pipe pkg [
    resolveDeps
    removeBlacklist
    (l: {srcs = map fetchDeb l; deps = l; })
    (dpkgExtractAll "${pkg.provides}-extracted" pkg.version)
  ];

  findHpcKit = year: findMatching { pname = "intel-hpckit"; kind = "<<"; version = toString (year+1); };
  years = map toInt (attrNames components);

  patchIntel = callPackage ./patch_intel.nix { };

  # Version information for each hpckit. This is used to normalize the paths
  # so that files are in $out/{bin,lib,include...} instead of all over the place
  # in $out/opt/intel/oneapi/*/*/{...}.
  #
  # The most important is the compiler component, which is used to build the
  # stdenv for the hpckit.
  #
  # NOTE: this have to be manually specified, so we can avoid IFD. To add a
  # new version, add a new field with an empty attrset, (e.g. "2026" = {}; ),
  # build hpckit_2026.unpatched and use the values from
  # result/opt/intel/oneapi/* to populate the attrset.
  #
  # WARN: if there are more than one version in the folders of the unpatched
  # components, our dependency resolution hacks have probably failed and the
  # package set may be broken.
  components = {
    "2025" = {
      ishmem = "1.3";
      pti = "0.12";
      tcm = "1.3";
      umf = "0.10";

      ccl = "2021.15";
      compiler = "2025.1";
      dal = "2025.5";
      debugger = "2025.1";
      dev-utilities = "2025.1";
      dnnl = "2025.1";
      dpcpp-ct = "2025.1";
      dpl = "2022.8";
      ipp = "2022.1";
      ippcp = "2025.1";
      mkl = "2025.1";
      mpi = "2021.15";
      tbb = "2022.1";
    };
    "2024" = {
      tcm = "1.1";

      ccl = "2021.13";
      compiler = "2024.2";
      dal = "2024.6";
      debugger = "2024.2";
      dev-utilities = "2024.2";
      diagnostics = "2024.2";
      dnnl = "2024.2";
      dpcpp-ct = "2024.2";
      dpl = "2022.6";
      ipp = "2021.12";
      ippcp = "2021.12";
      mkl = "2024.2";
      mpi = "2021.13";
      tbb = "2021.13";

      extraPackages = [
        sqlite
        elfutils
      ];
    };
  };

  replaceDots = replaceStrings ["."] ["_"];

in recurseIntoAttrs (listToAttrs (map (year: let
      year_str = toString year;
    in {
      name = "hpckit_${year_str}";
      value = patchIntel {unpatched = buildIntel (findHpcKit year); components = components.${year_str}; };
  }) years)) // {
  apt = pipe aptData [
    (groupBy (p: replaceDots p.provides))
    (mapAttrs (_: l: listToAttrs (map (pkg: { name = replaceDots ("v" + pkg.version); value = pkg; }) l)))
  ] ;

  inherit resolveDeps patchIntel buildIntel;
}
