import { extractUmbNotificationColor } from './extractUmbNotificationColor.function.js';
import { isUmbNotifications, UMB_NOTIFICATION_HEADER } from './isUmbNotifications.function.js';
import { isProblemDetailsLike } from './apiTypeValidators.function.js';
import { UmbAuthSignalerContext } from './auth-signaler.context.js';
import { UmbControllerBase } from '../../../libs/class-api/index.js';
const MAX_RETRIES = 3;
export class UmbApiInterceptorController extends UmbControllerBase {
    /**
     * Store pending requests that received a 401 response and are waiting for re-authentication.
     * This is used to retry the requests after re-authentication.
     */
    #pending401Requests = [];
    /**
     * Store non-GET requests that received a 401 response.
     * This is used to notify the user about actions that could not be completed due to session expiration.
     * These requests will not be retried, as they are not idempotent.
     * Instead, we will notify the user about these requests after re-authentication.
     */
    #nonGet401Requests = [];
    /** Registered on the host so auth context can consume it to bridge authorization state. */
    #signaler = new UmbAuthSignalerContext(this._host);
    /**
     * Binds the default interceptors to the client.
     * This includes the auth response interceptor, the error interceptor and the umb-notifications interceptor.
     * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
     */
    bindDefaultInterceptors(client) {
        // Add the default observables to the instance
        this.handleUnauthorizedAuthRetry();
        // Add the default interceptors to the client
        this.addAuthResponseInterceptor(client);
        this.addForbiddenResponseInterceptor(client);
        this.addUmbGeneratedResourceInterceptor(client);
        this.addUmbNotificationsInterceptor(client);
        this.addErrorInterceptor(client);
    }
    /**
     * Interceptor which checks responses for 401 errors and signals the auth layer to show the login UI.
     * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
     * @internal
     */
    addAuthResponseInterceptor(client) {
        client.interceptors.response.use(async (response, request, requestConfig) => {
            if (response.status !== 401)
                return response;
            // Build a plain ProblemDetails object for the response body
            const problemDetails = {
                status: response.status,
                title: response.statusText || 'Unauthorized request, waiting for re-authentication.',
                detail: undefined,
                errors: undefined,
                type: 'Unauthorized',
                stack: undefined,
            };
            const newResponse = this.#createResponse(problemDetails, response);
            const signaler = this.#signaler;
            // Only retry for GET requests
            if (request.method !== 'GET') {
                // Collect info for later notification
                this.#nonGet401Requests.push({ request, requestConfig });
                // Signal the auth layer to show the login UI
                signaler.requestTimeout();
                return newResponse;
            }
            // Find if this request is already in the queue and increment retries
            let retries = 1;
            const existing = this.#pending401Requests.find((req) => req.request === request && req.requestConfig === requestConfig);
            if (existing) {
                retries = existing.retries + 1;
                if (retries > MAX_RETRIES) {
                    return newResponse;
                }
                existing.retries = retries;
            }
            // Return a promise that will resolve when re-auth completes
            return new Promise((resolve, reject) => {
                this.#pending401Requests.push({
                    request,
                    requestConfig,
                    retry: async () => {
                        const { data, response: retryResponse } = await client.request(requestConfig);
                        return this.#createResponse(data, retryResponse);
                    },
                    resolve,
                    reject,
                    retries,
                });
                // Signal the auth layer to show the login UI
                signaler.requestTimeout();
                console.log('[Interceptor] 401 Unauthorized - queuing request for re-authentication and have tried', retries - 1, 'times before', requestConfig);
            });
        });
    }
    /**
     * Interceptor which checks responses for 403 errors and displays them as a notification.
     * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
     * @internal
     */
    addForbiddenResponseInterceptor(client) {
        client.interceptors.response.use((response) => {
            if (response.status !== 403)
                return response;
            // Build a plain ProblemDetails object for the response body
            const problemDetails = {
                status: response.status,
                title: response.statusText ||
                    'You do not have the necessary permissions to complete the requested action. If you believe this is in error, please reach out to your administrator.',
                detail: undefined,
                errors: undefined,
                type: 'Unauthorized',
                stack: undefined,
            };
            return this.#createResponse(problemDetails, response);
        });
    }
    /**
     * Interceptor which checks responses for the Umb-Generated-Resource header and replaces the value into the response body.
     * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
     * @internal
     */
    addUmbGeneratedResourceInterceptor(client) {
        client.interceptors.response.use((response) => {
            if (!response.headers.has('Umb-Generated-Resource'))
                return response;
            const generatedResource = response.headers.get('Umb-Generated-Resource');
            if (generatedResource === null) {
                return response;
            }
            // Return a new response with the generated resource in the body (plain text)
            return this.#createResponse(generatedResource, response);
        });
    }
    /**
     * Interceptor which checks responses for 500 errors and displays them as a notification if any.
     * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
     * @internal
     */
    addErrorInterceptor(client) {
        client.interceptors.response.use(async (response) => {
            // If the response is ok, we just return the response
            if (response.ok)
                return response;
            // We will check if it is not a 401 or 403 error, as that is handled by other interceptors
            if (response.status === 401 || response.status === 403)
                return response;
            // Special handling for 404 Not Found
            if (response.status === 404) {
                const notFoundProblemDetails = {
                    status: response.status,
                    title: response.statusText || 'The requested resource was not found.',
                    detail: undefined,
                    errors: undefined,
                    type: 'NotFound',
                    stack: undefined,
                };
                return this.#createResponse(notFoundProblemDetails, response);
            }
            // For all other errors, we will build a ProblemDetails object
            let problemDetails = {
                status: response.status,
                title: response.statusText ||
                    'A fatal server error occurred. If this continues, please reach out to your administrator.',
                detail: undefined,
                errors: undefined,
                type: 'ServerError',
                stack: undefined,
            };
            try {
                // Clones the response to read the body
                const origResponse = response.clone();
                const errorBody = await origResponse.json();
                // If there is JSON in the error, we will try to parse it as a ProblemDetails object
                if (errorBody && isProblemDetailsLike(errorBody)) {
                    // Merge the parsed problem details into our default
                    problemDetails = errorBody;
                }
            }
            catch (e) {
                // Ignore JSON parse error
                console.error('[Interceptor] Caught a server error, but failed parsing error body (expected JSON)', e);
            }
            return this.#createResponse(problemDetails, response);
        });
    }
    /**
     * Interceptor which checks responses for the umb-notifications header and displays them as a notification if any. Removes the umb-notifications from the headers.
     * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
     * @internal
     */
    addUmbNotificationsInterceptor(client) {
        client.interceptors.response.use((response) => {
            // Check if the response has the umb-notifications header
            // If not, we just return the response
            const umbNotifications = response.headers.get(UMB_NOTIFICATION_HEADER);
            if (!umbNotifications)
                return response;
            // Parse the notifications from the header
            // If the header is not a valid JSON, we just return the response
            try {
                const notifications = JSON.parse(umbNotifications);
                if (!isUmbNotifications(notifications))
                    return response;
                for (const notification of notifications) {
                    this.#peekError(notification.category, notification.message, null, extractUmbNotificationColor(notification.type));
                }
            }
            catch {
                // Ignore JSON parse errors
            }
            return response;
        });
    }
    /**
     * Observes the auth signaler's authorization state to retry GET-requests that received a 401
     * Unauthorized response. Also notifies the user about non-GET requests that received a 401
     * Unauthorized response after re-authentication completes.
     * @internal
     */
    handleUnauthorizedAuthRetry() {
        this.observe(this.#signaler.isAuthorized, (isAuthorized) => {
            // Only retry when transitioning to authorized (i.e. re-authentication completed)
            if (!isAuthorized)
                return;
            // Skip if there are no pending requests to retry
            if (this.#pending401Requests.length === 0 && this.#nonGet401Requests.length === 0)
                return;
            console.log('[Interceptor] 401 Unauthorized - re-authentication completed');
            // On auth, retry all pending requests
            const requests = this.#pending401Requests.splice(0, this.#pending401Requests.length);
            requests.forEach((req) => {
                console.log('[Interceptor] 401 Unauthorized - retrying request after re-authentication', req.requestConfig);
                req.retry().then(req.resolve).catch(req.reject);
            });
            // Notify about non-GET 401s after successful re-auth
            if (this.#nonGet401Requests.length > 0) {
                const errors = {};
                this.#nonGet401Requests.forEach((req) => {
                    errors[`${req.request.method} ${req.request.url}`] = `Request failed with 401 Unauthorized.`;
                });
                this.#peekError('Some actions were not completed', 'Some actions could not be completed because your session expired. Please try again.', errors, 'warning');
                this.#nonGet401Requests.length = 0; // Clear after notifying
            }
        }, '_authClearNonGet401Requests');
    }
    /**
     * Helper to create a new Response with correct Content-Type.
     * @param {unknown} body The body of the response, can be a string or an object.
     * @param {Response} originalResponse The original response to copy status and headers from.
     * @returns {Response} The new Response object with the correct Content-Type and body.
     */
    #createResponse(body, originalResponse) {
        const isString = typeof body === 'string';
        const contentType = isString ? 'text/plain' : 'application/json';
        const responseBody = isString ? body : JSON.stringify(body);
        // Construct new headers but preserve "X-" headers from the original response
        const headersOverride = {};
        originalResponse.headers.forEach((value, key) => {
            if (key.toLowerCase().startsWith('x-')) {
                headersOverride[key] = value;
            }
        });
        return new Response(responseBody, {
            status: originalResponse.status,
            statusText: originalResponse.statusText,
            headers: {
                ...headersOverride,
                'Content-Type': contentType,
            },
        });
    }
    /**
     * Helper to show a notification error.
     * @param {string} headline The headline of the error notification.
     * @param {string} message The message of the error notification.
     * @param {unknown} details Additional details to show in the notification.
     * @param {UmbNotificationColor} [color] The color of the notification.
     */
    async #peekError(headline, message, details, color) {
        // Store the host for usage in the following async context
        const host = this._host;
        // This late importing is done to avoid circular reference [NL]
        (await import('../notification/index.js')).umbPeekError(host, {
            headline,
            message,
            details,
            color,
        });
    }
}
