/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module RpcInterface
 */
import { BeEvent, BentleyStatus, Guid } from "@itwin/core-bentley";
import { BackendError, IModelError, NoContentError } from "../../IModelError";
import { RpcConfiguration } from "./RpcConfiguration";
import { RpcProtocolEvent, RpcRequestEvent, RpcRequestStatus } from "./RpcConstants";
import { RpcMarshaling } from "./RpcMarshaling";
import { RpcOperation } from "./RpcOperation";
import { RpcProtocol, RpcProtocolVersion } from "./RpcProtocol";
import { CURRENT_REQUEST } from "./RpcRegistry";
/* eslint-disable @typescript-eslint/naming-convention */
// cspell:ignore csrf
/* eslint-disable @typescript-eslint/no-deprecated */
/** @internal */
export const aggregateLoad = { lastRequest: 0, lastResponse: 0 };
/** @internal */
export class ResponseLike {
    get body() { return null; }
    async arrayBuffer() { return this._data; }
    async blob() { throw new IModelError(BentleyStatus.ERROR, "Not implemented."); }
    async formData() { throw new IModelError(BentleyStatus.ERROR, "Not implemented."); }
    async json() { return this._data; }
    async text() { return this._data; }
    get bodyUsed() { return false; }
    get headers() { throw new IModelError(BentleyStatus.ERROR, "Not implemented."); }
    get ok() { return this.status >= 200 && this.status <= 299; }
    get redirected() { return false; }
    get status() { return 200; }
    get statusText() { return ""; }
    get trailer() { throw new IModelError(BentleyStatus.ERROR, "Not implemented."); }
    get type() { return "basic"; }
    get url() { return ""; }
    clone() { return { ...this }; }
    constructor(data) {
        this._data = Promise.resolve(data);
    }
}
class Cancellable {
    cancel() { }
    constructor(task) {
        this.promise = new Promise((resolve, reject) => {
            this.cancel = () => resolve(undefined);
            task.then(resolve, reject);
        });
    }
}
/** A RPC operation request.
 * @internal
 */
export class RpcRequest {
    /** All RPC requests that are currently in flight. */
    static get activeRequests() { return this._activeRequests; }
    /** The aggregate operations profile of all active RPC interfaces. */
    static get aggregateLoad() { return aggregateLoad; }
    /**
     * The request for the current RPC operation.
     * @note The return value of this function is only reliable if program control was received from a RPC interface class member function that directly returns the result of calling RpcInterface.forward.
     */
    static current(context) {
        return context[CURRENT_REQUEST];
    }
    /** The status of this request. */
    get status() { return this._status; }
    /** Extended status information for this request (if available). */
    get extendedStatus() { return this._extendedStatus; }
    /** The last submission for this request. */
    get lastSubmitted() { return this._lastSubmitted; }
    /** The last status update received for this request. */
    get lastUpdated() { return this._lastUpdated; }
    /** Whether a connection is active for this request. */
    get connecting() { return this._connecting; }
    /** Whether this request is pending. */
    get pending() {
        switch (this.status) {
            case RpcRequestStatus.Submitted:
            case RpcRequestStatus.Pending: {
                return true;
            }
            default: {
                return false;
            }
        }
    }
    /** The elapsed time for this request. */
    get elapsed() {
        return this._lastUpdated - this._created;
    }
    /** An attempt-specific value for when to next retry this request. */
    get retryAfter() { return this._retryAfter; }
    /** Finds the first parameter of a given structural type if present. */
    findParameterOfType(requiredProperties) {
        for (const param of this.parameters) {
            if (typeof (param) === "object" && param !== null) {
                for (const prop of Object.getOwnPropertyNames(requiredProperties)) {
                    if (prop in param && typeof (param[prop]) === requiredProperties[prop]) {
                        return param;
                    }
                }
            }
        }
        return undefined;
    }
    /** Finds the first IModelRpcProps parameter if present. */
    findTokenPropsParameter() {
        return this.findParameterOfType({ iModelId: "string" });
    }
    /** The raw implementation response for this request. */
    get rawResponse() {
        this._hasRawListener = true;
        return this._rawPromise;
    }
    /** Constructs an RPC request. */
    constructor(client, operation, parameters) {
        this._resolve = () => undefined;
        this._resolveRaw = () => undefined;
        this._reject = () => undefined;
        this._rejectRaw = () => undefined;
        this._created = 0;
        this._lastSubmitted = 0;
        this._lastUpdated = 0;
        /** @internal */
        this._status = RpcRequestStatus.Unknown;
        /** @internal */
        this._extendedStatus = "";
        this._connecting = false;
        this._active = true;
        this._hasRawListener = false;
        this._raw = undefined;
        this._attempts = 0;
        this._retryAfter = null;
        this._transientFaults = 0;
        this._response = undefined;
        this.responseProtocolVersion = RpcProtocolVersion.None;
        this._created = new Date().getTime();
        this.path = "";
        this.method = "";
        this.client = client;
        this.protocol = client.configuration.protocol;
        this.operation = RpcOperation.lookup(client.constructor, operation);
        this.parameters = parameters;
        this.retryInterval = this.operation.policy.retryInterval(client.configuration);
        this.response = new Promise((resolve, reject) => {
            this._resolve = resolve;
            this._reject = reject;
        });
        this._rawPromise = new Promise((resolve, reject) => {
            this._resolveRaw = resolve;
            this._rejectRaw = reject;
        });
        this.id = RpcConfiguration.requestContext.getId(this) || Guid.createValue();
        this.setStatus(RpcRequestStatus.Created);
        this.operation.policy.requestCallback(this);
    }
    /** Sets the last updated time for the request. */
    setLastUpdatedTime() {
        this._lastUpdated = new Date().getTime();
    }
    computeRetryAfter(attempts) {
        return (((Math.pow(2, attempts) - 1) / 2) * 500) + 500;
    }
    recordTransientFault() {
        ++this._transientFaults;
    }
    resetTransientFaultCount() {
        this._transientFaults = 0;
    }
    supportsStatusCategory() {
        if (!this.protocol.supportsStatusCategory) {
            return false;
        }
        return RpcProtocol.protocolVersion >= RpcProtocolVersion.IntroducedStatusCategory && this.responseProtocolVersion >= RpcProtocolVersion.IntroducedStatusCategory;
    }
    /* @internal */
    cancel() {
        if (typeof (this._sending) === "undefined") {
            return;
        }
        this._sending.cancel();
        this._sending = undefined;
        this._connecting = false;
        RpcRequest._activeRequests.delete(this.id);
        this.setStatus(RpcRequestStatus.Cancelled);
    }
    /* @internal */
    async submit() {
        if (!this._active)
            return;
        this._lastSubmitted = new Date().getTime();
        this._retryAfter = null;
        ++this._attempts;
        if (this.status === RpcRequestStatus.Created || this.status === RpcRequestStatus.NotFound || this.status === RpcRequestStatus.Cancelled) {
            this.setStatus(RpcRequestStatus.Submitted);
        }
        try {
            this._connecting = true;
            RpcRequest._activeRequests.set(this.id, this);
            this.protocol.events.raiseEvent(RpcProtocolEvent.RequestCreated, this);
            this._sending = new Cancellable(this.setHeaders().then(async () => this.send()));
            this.operation.policy.sentCallback(this);
            const response = await this._sending.promise;
            if (typeof (response) === "undefined") {
                return;
            }
            this._sending = undefined;
            const status = this.protocol.getStatus(response);
            if (this._hasRawListener && status === RpcRequestStatus.Resolved && typeof (this._response) !== "undefined") {
                this._connecting = false;
                RpcRequest._activeRequests.delete(this.id);
                this.resolveRaw();
            }
            else {
                this.protocol.events.raiseEvent(RpcProtocolEvent.ResponseLoading, this);
                if (status === RpcRequestStatus.Unknown) {
                    this._connecting = false;
                    RpcRequest._activeRequests.delete(this.id);
                    this.handleUnknownResponse(response);
                    return;
                }
                const value = await this.load();
                this.protocol.events.raiseEvent(RpcProtocolEvent.ResponseLoaded, this);
                RpcRequest._activeRequests.delete(this.id);
                this._connecting = false;
                this.handleResponse(response, value);
            }
        }
        catch (err) {
            this.protocol.events.raiseEvent(RpcProtocolEvent.ConnectionErrorReceived, this, err);
            RpcRequest._activeRequests.delete(this.id);
            this._connecting = false;
            this.reject(err);
        }
    }
    handleUnknownResponse(code) {
        this.reject(new IModelError(BentleyStatus.ERROR, `Unknown response ${code}.`));
    }
    handleResponse(code, value) {
        const protocolStatus = this.protocol.getStatus(code);
        const status = this.transformResponseStatus(protocolStatus, value);
        if (RpcRequestStatus.isTransientError(status)) {
            return this.handleTransientError(status);
        }
        switch (status) {
            case RpcRequestStatus.Resolved: {
                return this.handleResolved(value);
            }
            case RpcRequestStatus.Rejected: {
                return this.handleRejected(value);
            }
            case RpcRequestStatus.Pending: {
                return this.setPending(status, value.objects);
            }
            case RpcRequestStatus.NotFound: {
                return this.handleNotFound(status, value);
            }
            case RpcRequestStatus.NoContent: {
                return this.handleNoContent();
            }
        }
    }
    transformResponseStatus(protocolStatus, value) {
        if (!this.supportsStatusCategory()) {
            return protocolStatus;
        }
        let status = protocolStatus;
        if (protocolStatus === RpcRequestStatus.Pending) {
            status = RpcRequestStatus.Rejected;
        }
        else if (protocolStatus === RpcRequestStatus.NotFound) {
            status = RpcRequestStatus.Rejected;
        }
        else if (protocolStatus === RpcRequestStatus.Unknown) {
            status = RpcRequestStatus.Rejected;
        }
        if (value.objects.indexOf("iTwinRpcCoreResponse") !== -1 && value.objects.indexOf("managedStatus") !== -1) {
            const managedStatus = JSON.parse(value.objects);
            value.objects = managedStatus.responseValue;
            if (managedStatus.managedStatus === "pending") {
                status = RpcRequestStatus.Pending;
            }
            else if (managedStatus.managedStatus === "notFound") {
                status = RpcRequestStatus.NotFound;
            }
        }
        return status;
    }
    handleResolved(value) {
        try {
            this._raw = value.objects;
            const result = RpcMarshaling.deserialize(this.protocol, value);
            if (ArrayBuffer.isView(result)) {
                this._raw = result.buffer;
            }
            return this.resolve(result);
        }
        catch (err) {
            return this.reject(err);
        }
    }
    handleRejected(value) {
        this.protocol.events.raiseEvent(RpcProtocolEvent.BackendErrorReceived, this);
        try {
            const error = RpcMarshaling.deserialize(this.protocol, value);
            const hasInfo = error && typeof (error) === "object" && error.hasOwnProperty("name") && error.hasOwnProperty("message");
            const name = hasInfo ? error.name : "";
            const message = hasInfo ? error.message : "";
            const errorNumber = (hasInfo && error.hasOwnProperty("errorNumber")) ? error.errorNumber : BentleyStatus.ERROR;
            return this.reject(new BackendError(errorNumber, name, message, () => error));
        }
        catch (err) {
            return this.reject(err);
        }
    }
    handleNoContent() {
        return this.reject(new NoContentError());
    }
    handleNotFound(status, value) {
        if (RpcRequest.notFoundHandlers.numberOfListeners === 0)
            this.handleRejected(value);
        const response = RpcMarshaling.deserialize(this.protocol, value);
        this.setStatus(status);
        let resubmitted = false;
        RpcRequest.notFoundHandlers.raiseEvent(this, response, () => {
            if (resubmitted)
                throw new IModelError(BentleyStatus.ERROR, `Already resubmitted using this handler.`);
            resubmitted = true;
            void this.submit();
        }, (reason) => reason ? this.reject(reason) : this.handleRejected(value));
        return;
    }
    resolve(result) {
        if (!this._active)
            return;
        this._active = false;
        this.setLastUpdatedTime();
        this._resolve(result);
        if (this._hasRawListener) {
            if (typeof (this._raw) === "undefined") {
                throw new IModelError(BentleyStatus.ERROR, "Cannot access raw response.");
            }
            this._resolveRaw(new ResponseLike(this._raw));
        }
        this.setStatus(RpcRequestStatus.Resolved);
        this.dispose();
    }
    resolveRaw() {
        if (typeof (this._response) === "undefined") {
            throw new IModelError(BentleyStatus.ERROR, "Cannot access raw response.");
        }
        this._active = false;
        this.setLastUpdatedTime();
        this._resolveRaw(this._response);
        this.setStatus(RpcRequestStatus.Resolved);
        this.dispose();
    }
    reject(reason) {
        if (!this._active)
            return;
        this._active = false;
        this.setLastUpdatedTime();
        this._reject(reason);
        if (this._hasRawListener) {
            this._rejectRaw(reason);
        }
        this.setStatus(RpcRequestStatus.Rejected);
        this.dispose();
    }
    /** @internal */
    dispose() {
        this.setStatus(RpcRequestStatus.Disposed);
        this._raw = undefined;
        this._response = undefined;
        const client = this.client;
        if (client[CURRENT_REQUEST] === this) {
            client[CURRENT_REQUEST] = undefined;
        }
    }
    setPending(status, extendedStatus) {
        if (!this._active)
            return;
        this.setLastUpdatedTime();
        this._extendedStatus = extendedStatus;
        this.setStatus(status);
        RpcRequest.events.raiseEvent(RpcRequestEvent.PendingUpdateReceived, this);
    }
    handleTransientError(status) {
        if (!this._active)
            return;
        this.setLastUpdatedTime();
        this._retryAfter = this.computeRetryAfter(this._attempts - 1);
        if (this._transientFaults > this.protocol.configuration.transientFaultLimit) {
            this.reject(new IModelError(BentleyStatus.ERROR, `Exceeded transient fault limit.`));
        }
        else {
            this.setStatus(status);
            RpcRequest.events.raiseEvent(RpcRequestEvent.TransientErrorReceived, this);
        }
    }
    async setHeaders() {
        const versionHeader = this.protocol.protocolVersionHeaderName;
        if (versionHeader && RpcProtocol.protocolVersion)
            this.setHeader(versionHeader, RpcProtocol.protocolVersion.toString());
        const headerNames = this.protocol.serializedClientRequestContextHeaderNames;
        const headerValues = await RpcConfiguration.requestContext.serialize(this);
        if (headerNames.id)
            this.setHeader(headerNames.id, headerValues.id || this.id); // Cannot be empty
        if (headerNames.applicationVersion)
            this.setHeader(headerNames.applicationVersion, headerValues.applicationVersion);
        if (headerNames.applicationId)
            this.setHeader(headerNames.applicationId, headerValues.applicationId);
        if (headerNames.sessionId)
            this.setHeader(headerNames.sessionId, headerValues.sessionId);
        if (headerNames.authorization && headerValues.authorization)
            this.setHeader(headerNames.authorization, headerValues.authorization);
        if (headerValues.csrfToken)
            this.setHeader(headerValues.csrfToken.headerName, headerValues.csrfToken.headerValue);
    }
    setStatus(status) {
        if (this._status === status)
            return;
        this._status = status;
        RpcRequest.events.raiseEvent(RpcRequestEvent.StatusChanged, this);
    }
}
RpcRequest._activeRequests = new Map();
/** Events raised by RpcRequest. See [[RpcRequestEvent]] */
RpcRequest.events = new BeEvent();
/** Resolvers for "not found" requests. See [[RpcRequestNotFoundHandler]] */
RpcRequest.notFoundHandlers = new BeEvent();
/** @internal */
export const initializeRpcRequest = (() => {
    let initialized = false;
    return () => {
        if (initialized) {
            return;
        }
        initialized = true;
        RpcRequest.events.addListener((type, request) => {
            if (type !== RpcRequestEvent.StatusChanged)
                return;
            switch (request.status) {
                case RpcRequestStatus.Submitted: {
                    aggregateLoad.lastRequest = request.lastSubmitted;
                    break;
                }
                case RpcRequestStatus.Pending:
                case RpcRequestStatus.Resolved:
                case RpcRequestStatus.Rejected: {
                    aggregateLoad.lastResponse = request.lastUpdated;
                    break;
                }
            }
        });
        RpcProtocol.events.addListener((type) => {
            const now = new Date().getTime();
            switch (type) {
                case RpcProtocolEvent.RequestReceived: {
                    aggregateLoad.lastRequest = now;
                    break;
                }
                case RpcProtocolEvent.BackendReportedPending:
                case RpcProtocolEvent.BackendErrorOccurred:
                case RpcProtocolEvent.BackendResponseCreated: {
                    aggregateLoad.lastResponse = now;
                    break;
                }
            }
        });
    };
})();
//# sourceMappingURL=RpcRequest.js.map