private static boolean matchCallableParameters(@NotNull List<PyCallableParameter> expectedParameters,
                                                 @NotNull List<PyCallableParameter> actualParameters,
                                                 @NotNull MatchContext matchContext) {
    final TypeEvalContext context = matchContext.context;

    if (expectedParameters.size() == 1) {
      final var firstExpectedParam = expectedParameters.get(0);
      final var expectedParamType = firstExpectedParam.getType(context);
      if (expectedParamType instanceof final PyParamSpecType expectedParamSpecType) {
        matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, expectedParamSpecType.withParameters(actualParameters, context));
        return true;
      }
      else if (expectedParamType instanceof final PyConcatenateType expectedConcatenateType) {
        if (actualParameters.isEmpty()) {
          return true;
        }
        final var actualParamType = actualParameters.get(0).getType(context);
        final var expectedFirstTypes = expectedConcatenateType.getFirstTypes();

        if (actualParamType instanceof final PyConcatenateType actualConcatenateType) {
          final var actualFirstType = actualConcatenateType.getFirstTypes();
          if (!match(expectedFirstTypes, actualFirstType, matchContext)) {
            return false;
          }
        }
        else {
          final var actualParamRightBound = Math.min(expectedFirstTypes.size(), actualParameters.size());
          final var actualFirstParamTypes = ContainerUtil
            .map(actualParameters.subList(0, actualParamRightBound), it -> it.getType(context));

          if (!match(expectedFirstTypes, actualFirstParamTypes, matchContext)) {
            return false;
          }

          if (actualParamRightBound < actualParameters.size()) {
            final var expectedParamSpecType = expectedConcatenateType.getParamSpec();
            final var restActualParameters = actualParameters.subList(actualParamRightBound, actualParameters.size());
            final var parametersSubst = expectedParamSpecType.withParameters(restActualParameters, context);
            matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, parametersSubst);
            return true;
          }
        }

        return true;
      }
    }

    int startIndex = 0;
    if (!expectedParameters.isEmpty() && !actualParameters.isEmpty()) {
      var firstExpectedParam = expectedParameters.get(0);
      var firstActualParam = actualParameters.get(0);
      if (firstExpectedParam.isSelf() && firstActualParam.isSelf()) {
        if (!match(firstExpectedParam.getType(context), firstActualParam.getType(context), matchContext).orElse(true)) {
          return false;
        }
        startIndex = 1;
      }
    }

    // TODO Implement proper compatibility check for callable signatures, including positional- and keyword-only arguments, defaults, etc.
    boolean shouldAcceptUnlimitedPositionalArgs = ContainerUtil.exists(expectedParameters, PyCallableParameter::isPositionalContainer);
    boolean canAcceptUnlimitedPositionalArgs = ContainerUtil.exists(actualParameters, PyCallableParameter::isPositionalContainer);
    if (shouldAcceptUnlimitedPositionalArgs && !canAcceptUnlimitedPositionalArgs) return false;

    boolean shouldAcceptArbitraryKeywordArgs = ContainerUtil.exists(expectedParameters, PyCallableParameter::isKeywordContainer);
    boolean canAcceptArbitraryKeywordArgs = ContainerUtil.exists(actualParameters, PyCallableParameter::isKeywordContainer);
    if (shouldAcceptArbitraryKeywordArgs && !canAcceptArbitraryKeywordArgs) return false;

    List<PyType> expectedElementTypes = StreamEx.of(expectedParameters)
      .filter(cp -> !(cp.getParameter() instanceof PySlashParameter || cp.getParameter() instanceof PySingleStarParameter))
      .map(cp -> {
        PyType argType = cp.getArgumentType(context);
        if (cp.isPositionalContainer() && !(argType instanceof PyVariadicType)) {
          return PyUnpackedTupleTypeImpl.createUnbound(argType);
        }
        return argType;
      })
      .toList();
    PyTypeParameterMapping mapping = PyTypeParameterMapping.mapWithParameterList(ContainerUtil.subList(expectedElementTypes, startIndex),
                                                                                 ContainerUtil.subList(actualParameters, startIndex),
                                                                                 context);
    if (mapping == null) {
      return false;
    }
    // actual callable type could accept more general parameter type
    for (Couple<PyType> pair : mapping.getMappedTypes()) {
      Optional<Boolean> matched = matchContext.reverseSubstitutions().reversedSubstitutions
                                  ? match(pair.getSecond(), pair.getFirst(), matchContext.reverseSubstitutions())
                                  : match(pair.getFirst(), pair.getSecond(), matchContext.reverseSubstitutions());
      if (!matched.orElse(true)) {
        return false;
      }
    }
    return true;
  }
private static boolean matchCallableParameters(@NotNull List<PyCallableParameter> expectedParameters,
                                                 @NotNull List<PyCallableParameter> actualParameters,
                                                 @NotNull MatchContext matchContext)