import { __awaiter, __extends, __generator, __spreadArray } from "tslib";
import { CachedSolanaRpcFactory } from "./cachedRpcFactory";
import { SolanaBaseRpcFactory } from "./baseRpcFactories";
import { isPromiseFulfilled, isPromiseRejected } from "../../utils/TypeGuards";
import { compareSvmRpcResults, createSendErrorWithMessage } from "../utils";
// This factory stores multiple Cached RPC factories so that users of this factory can specify multiple RPC providers
// and the factory will fallback through them if any RPC calls fail. This factory also implements quorum logic amongst
// the RPC providers.
var QuorumFallbackSolanaRpcFactory = /** @class */ (function (_super) {
    __extends(QuorumFallbackSolanaRpcFactory, _super);
    function QuorumFallbackSolanaRpcFactory(factoryConstructorParams, nodeQuorumThreshold, logger) {
        var _this = _super.call(this) || this;
        _this.nodeQuorumThreshold = nodeQuorumThreshold;
        _this.logger = logger;
        _this.rpcFactories = [];
        factoryConstructorParams.forEach(function (params) {
            var rpcFactory = new (CachedSolanaRpcFactory.bind.apply(CachedSolanaRpcFactory, __spreadArray([void 0], params, false)))();
            _this.rpcFactories.push({
                transport: rpcFactory.createTransport(),
                rpcClient: rpcFactory.createRpcClient(),
                rpcFactory: rpcFactory,
            });
        });
        if (_this.nodeQuorumThreshold < 1 || !Number.isInteger(_this.nodeQuorumThreshold)) {
            throw new Error("nodeQuorum,Threshold cannot be < 1 and must be an integer. Currently set to ".concat(_this.nodeQuorumThreshold));
        }
        return _this;
    }
    QuorumFallbackSolanaRpcFactory.prototype.createTransport = function () {
        var _this = this;
        return function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            return __awaiter(_this, void 0, void 0, function () {
                var _a, method, params, quorumThreshold, requiredFactories, fallbackFactories, errors, tryWithFallback, results, getErrorStrings, errorTexts, successfulProviderUrls_1, values, getHighestCountResult, logQuorumMismatchOrFailureDetails, throwQuorumError, mostFrequentResult, fallbackResults, fallbackValues, _b, quorumResult, count, mismatchedProviders, successfulProviderUrls;
                var _this = this;
                var _c;
                return __generator(this, function (_d) {
                    switch (_d.label) {
                        case 0:
                            _a = args[0].payload, method = _a.method, params = _a.params;
                            quorumThreshold = this._getQuorum(method, params !== null && params !== void 0 ? params : []);
                            requiredFactories = this.rpcFactories.slice(0, quorumThreshold);
                            fallbackFactories = __spreadArray([], this.rpcFactories.slice(quorumThreshold), true);
                            errors = [];
                            tryWithFallback = function (factory) {
                                var args = [];
                                for (var _i = 1; _i < arguments.length; _i++) {
                                    args[_i - 1] = arguments[_i];
                                }
                                return factory
                                    .transport.apply(factory, args).then(function (result) { return [factory.rpcFactory, result]; })
                                    .catch(function (error) {
                                    // Append the provider and error to the error array.
                                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                                    errors.push([factory.rpcFactory, (error === null || error === void 0 ? void 0 : error.stack) || (error === null || error === void 0 ? void 0 : error.toString())]);
                                    if (fallbackFactories.length === 0) {
                                        throw error;
                                    }
                                    var currentFactory = factory.rpcFactory.clusterUrl;
                                    var nextFactory = fallbackFactories.shift();
                                    _this.logger.debug({
                                        at: "FallbackSolanaRpcFactory#createTransport::tryWithFallback",
                                        message: "[".concat(method, "] ").concat(currentFactory, " failed, falling back to ").concat(nextFactory.rpcFactory.clusterUrl, ", new fallback providers length: ").concat(fallbackFactories.length),
                                        method: method,
                                        error: error,
                                    });
                                    return tryWithFallback.apply(void 0, __spreadArray([nextFactory], args, false));
                                });
                            };
                            return [4 /*yield*/, Promise.allSettled(requiredFactories.map(function (factory) {
                                    return tryWithFallback.apply(void 0, __spreadArray([factory], args, false));
                                }))];
                        case 1:
                            results = _d.sent();
                            getErrorStrings = function () {
                                return errors.map(function (_a) {
                                    var factory = _a[0], errorText = _a[1];
                                    return "Provider ".concat(factory.clusterUrl, " failed to call ").concat(method, " with error ").concat(errorText);
                                });
                            };
                            if (!results.every(isPromiseFulfilled)) {
                                errorTexts = getErrorStrings();
                                successfulProviderUrls_1 = results.filter(isPromiseFulfilled).map(function (result) { return result.value[0].clusterUrl; });
                                throw createSendErrorWithMessage("Not enough providers succeeded on ".concat(method, " call. Errors:\n").concat(errorTexts.join("\n"), "\n") +
                                    "Successful Providers:\n".concat(successfulProviderUrls_1.join("\n")), (_c = results.find(isPromiseRejected)) === null || _c === void 0 ? void 0 : _c.reason);
                            }
                            values = results.map(function (result) { return result.value; });
                            // Start at element 1 and begin comparing.
                            // If _all_ values are equal, we have hit quorum, so return.
                            if (values.slice(1).every(function (_a) {
                                var output = _a[1];
                                return compareSvmRpcResults(method, values[0][1], output);
                            })) {
                                return [2 /*return*/, values[0][1]];
                            }
                            getHighestCountResult = function (values) {
                                // Group the results by the count of that result.
                                var counts = __spreadArray([], values, true).reduce(function (acc, curr) {
                                    var result = curr[1];
                                    // Find the first result that matches the return value.
                                    var existingMatch = acc.find(function (_a) {
                                        var existingResult = _a[0];
                                        return compareSvmRpcResults(method, existingResult, result);
                                    });
                                    // Increment the count if a match is found, else add a new element to the match array with a count of 1.
                                    if (existingMatch) {
                                        existingMatch[1]++;
                                    }
                                    else {
                                        acc.push([result, 1]);
                                    }
                                    // Return the same acc object because it was modified in place.
                                    return acc;
                                }, [[undefined, 0]] // Initialize with [undefined, 0] as the first element so something is always returned.
                                );
                                // Sort so the result with the highest count is first.
                                counts.sort(function (_a, _b) {
                                    var a = _a[1];
                                    var b = _b[1];
                                    return b - a;
                                });
                                // Extract the result by grabbing the first element.
                                var _a = counts[0], mostFrequentResult = _a[0], count = _a[1];
                                return [mostFrequentResult, count];
                            };
                            logQuorumMismatchOrFailureDetails = function (method, params, mismatchedProviders, successfulProviders, errors, quorumResult) {
                                _this.logger.warn({
                                    at: "FallbackSolanaRpcFactory#createTransport",
                                    message: "[".concat(method, "] Some providers mismatched with the quorum result or failed \uD83D\uDEB8"),
                                    notificationPath: "across-warn",
                                    method: method,
                                    params: JSON.stringify(params),
                                    quorumResult: METHODS_RETURNING_BIGINT.includes(method) ? Number(quorumResult) : undefined,
                                    mismatchedProviders: mismatchedProviders,
                                    successfulProviders: successfulProviders,
                                    erroringProviders: errors.map(function (_a) {
                                        var factory = _a[0], errorText = _a[1];
                                        return "Provider ".concat(factory.clusterUrl, " failed with error ").concat(errorText);
                                    }),
                                });
                            };
                            throwQuorumError = function (mostFrequentResult, allValues) {
                                var errorTexts = getErrorStrings();
                                var successfulProviderUrls = values.map(function (_a) {
                                    var provider = _a[0];
                                    return provider.clusterUrl;
                                });
                                var mismatchedProviders = allValues
                                    .filter(function (_a) {
                                    var result = _a[1];
                                    return !compareSvmRpcResults(method, result, mostFrequentResult);
                                })
                                    .map(function (_a) {
                                    var factory = _a[0];
                                    return factory.clusterUrl;
                                });
                                logQuorumMismatchOrFailureDetails(method, params !== null && params !== void 0 ? params : [], mismatchedProviders, successfulProviderUrls, errors, mostFrequentResult);
                                throw new Error("Not enough providers agreed to meet quorum.\n" +
                                    "Providers that errored:\n" +
                                    "".concat(errorTexts.join("\n"), "\n") +
                                    "Providers that succeeded, but some failed to match:\n" +
                                    successfulProviderUrls.join("\n"));
                            };
                            // Exit early if there are no fallback providers left.
                            if (fallbackFactories.length === 0) {
                                mostFrequentResult = getHighestCountResult(values)[0];
                                throwQuorumError(mostFrequentResult, values);
                            }
                            return [4 /*yield*/, Promise.allSettled(fallbackFactories.map(function (factory) {
                                    return factory
                                        .transport.apply(factory, args).then(function (result) { return [factory.rpcFactory, result]; })
                                        .catch(function (err) {
                                        errors.push([factory.rpcFactory, (err === null || err === void 0 ? void 0 : err.stack) || (err === null || err === void 0 ? void 0 : err.toString())]);
                                        throw new Error("Fallback RPC call failed while trying to reach quorum", err);
                                    });
                                }))];
                        case 2:
                            fallbackResults = _d.sent();
                            fallbackValues = fallbackResults.filter(isPromiseFulfilled).map(function (promise) { return promise.value; });
                            _b = getHighestCountResult(__spreadArray(__spreadArray([], values, true), fallbackValues, true)), quorumResult = _b[0], count = _b[1];
                            // If this count is less than we need for quorum, throw the quorum error.
                            if (count < quorumThreshold) {
                                throwQuorumError(quorumResult, __spreadArray(__spreadArray([], values, true), fallbackValues, true));
                            }
                            mismatchedProviders = __spreadArray(__spreadArray([], values, true), fallbackValues, true).filter(function (_a) {
                                var result = _a[1];
                                return !compareSvmRpcResults(method, result, quorumResult);
                            })
                                .map(function (_a) {
                                var factory = _a[0];
                                return factory.clusterUrl;
                            });
                            successfulProviderUrls = __spreadArray(__spreadArray([], values, true), fallbackValues, true).map(function (_a) {
                                var provider = _a[0];
                                return provider.clusterUrl;
                            });
                            if (mismatchedProviders.length > 0 || errors.length > 0) {
                                logQuorumMismatchOrFailureDetails(method, params !== null && params !== void 0 ? params : [], mismatchedProviders, successfulProviderUrls, errors, quorumResult);
                            }
                            return [2 /*return*/, quorumResult];
                    }
                });
            });
        };
    };
    QuorumFallbackSolanaRpcFactory.prototype._getQuorum = function (method, _params) {
        // Only use quorum if this is a historical query that doesn't depend on the current block number.
        switch (method) {
            case "getBlock":
            case "getBlockTime":
                return this.nodeQuorumThreshold;
        }
        // All other calls should use quorum 1 to avoid errors due to sync differences.
        return 1;
    };
    return QuorumFallbackSolanaRpcFactory;
}(SolanaBaseRpcFactory));
export { QuorumFallbackSolanaRpcFactory };
// These methods return a bigint and their results are loggable because they are succinct and can further assist
// quorum debugging.
var METHODS_RETURNING_BIGINT = ["getBlockTime", "getSlot"];
//# sourceMappingURL=quorumFallbackRpcFactory.js.map