import { getLogger } from '../logging/logger.js';
/**
 * Converts Headers to a plain object for logging.
 */
function headersToObject(headers) {
    const result = {};
    headers.forEach((value, key) => {
        result[key] = value;
    });
    return result;
}
/**
 * Creates authentication middleware for openapi-fetch.
 *
 * This middleware intercepts requests and adds OAuth authentication headers
 * using the provided AuthStrategy.
 *
 * @param auth - The authentication strategy to use
 * @returns Middleware that adds auth headers to requests
 */
export function createAuthMiddleware(auth) {
    return {
        async onRequest({ request }) {
            if (auth.getAuthorizationHeader) {
                const authHeader = await auth.getAuthorizationHeader();
                request.headers.set('Authorization', authHeader);
            }
            return request;
        },
    };
}
/**
 * Masks specified keys in an object for logging.
 * Only masks top-level keys, replaces values with '...' placeholder.
 */
function maskBody(body, keysToMask) {
    if (!keysToMask || keysToMask.length === 0 || typeof body !== 'object' || body === null) {
        return body;
    }
    const masked = { ...body };
    for (const key of keysToMask) {
        if (key in masked) {
            masked[key] = '...';
        }
    }
    return masked;
}
/**
 * Creates logging middleware for openapi-fetch clients.
 *
 * Logs request/response details at debug and trace levels.
 *
 * @param config - Logging configuration or prefix string for backwards compatibility
 * @returns Middleware that logs requests and responses
 *
 * @example
 * // Simple usage with just a prefix
 * client.use(createLoggingMiddleware('OCAPI'));
 *
 * @example
 * // With body masking for large payloads
 * client.use(createLoggingMiddleware({
 *   prefix: 'MRT',
 *   maskBodyKeys: ['data']  // Masks base64-encoded bundle data
 * }));
 */
export function createLoggingMiddleware(config) {
    // Support both string (prefix) and config object for backwards compatibility
    const { prefix, maskBodyKeys } = typeof config === 'string' ? { prefix: config, maskBodyKeys: undefined } : (config ?? {});
    const reqTag = prefix ? `[${prefix} REQ]` : '';
    const respTag = prefix ? `[${prefix} RESP]` : '';
    return {
        async onRequest({ request }) {
            const logger = getLogger();
            const url = request.url;
            logger.debug({ method: request.method, url }, `${reqTag} ${request.method} ${url}`);
            // Read body from the request (already serialized by openapi-fetch)
            let body;
            if (request.body) {
                const clonedRequest = request.clone();
                const text = await clonedRequest.text();
                try {
                    body = JSON.parse(text);
                }
                catch {
                    body = text;
                }
            }
            // Mask sensitive/large body keys before logging
            const maskedBody = maskBody(body, maskBodyKeys);
            logger.trace({ method: request.method, url, headers: headersToObject(request.headers), body: maskedBody }, `${reqTag} ${request.method} ${url} body`);
            request._startTime = Date.now();
            return request;
        },
        async onResponse({ request, response }) {
            const logger = getLogger();
            const startTime = request._startTime ?? Date.now();
            const duration = Date.now() - startTime;
            const url = request.url;
            logger.debug({ method: request.method, url, status: response.status, duration }, `${respTag} ${request.method} ${url} ${response.status} ${duration}ms`);
            const clonedResponse = response.clone();
            let responseBody;
            // Read as text first, then try to parse as JSON.
            // This avoids a bug where json() consumes the body stream even when parsing fails,
            // making subsequent text() calls fail with "Body has already been read".
            const text = await clonedResponse.text();
            try {
                responseBody = JSON.parse(text);
            }
            catch {
                responseBody = text;
            }
            // Mask sensitive/large body keys before logging
            const maskedResponseBody = maskBody(responseBody, maskBodyKeys);
            logger.trace({ method: request.method, url, headers: headersToObject(response.headers), body: maskedResponseBody }, `${respTag} ${request.method} ${url} body`);
            return response;
        },
    };
}
/**
 * Creates middleware that sets the User-Agent header on requests.
 *
 * Sets both the standard `User-Agent` header and a custom `sfdc_user_agent` header
 * with the same value.
 *
 * @param config - Configuration with the User-Agent string
 * @returns Middleware that sets the User-Agent headers
 *
 * @example
 * ```typescript
 * const client = createOcapiClient(config, auth);
 * client.use(createUserAgentMiddleware({ userAgent: 'b2c-cli/0.1.0' }));
 * ```
 */
export function createUserAgentMiddleware(config) {
    return {
        async onRequest({ request }) {
            request.headers.set('User-Agent', config.userAgent);
            request.headers.set('sfdc_user_agent', config.userAgent);
            return request;
        },
    };
}
export function createExtraParamsMiddleware(config) {
    const logger = getLogger();
    return {
        async onRequest({ request }) {
            let modifiedRequest = request;
            // HTTP methods that don't allow a request body
            const methodsWithoutBody = ['GET', 'HEAD'];
            const canHaveBody = !methodsWithoutBody.includes(modifiedRequest.method.toUpperCase());
            // Add extra headers first (before other modifications)
            if (config.headers && Object.keys(config.headers).length > 0) {
                const newHeaders = new Headers(modifiedRequest.headers);
                for (const [key, value] of Object.entries(config.headers)) {
                    newHeaders.set(key, value);
                }
                logger.trace({ extraHeaders: config.headers }, '[ExtraParams] Adding extra headers to request');
                modifiedRequest = new Request(modifiedRequest.url, {
                    method: modifiedRequest.method,
                    headers: newHeaders,
                    ...(canHaveBody && modifiedRequest.body ? { body: modifiedRequest.body, duplex: 'half' } : {}),
                });
            }
            // Add extra query parameters
            if (config.query && Object.keys(config.query).length > 0) {
                const url = new URL(modifiedRequest.url);
                for (const [key, value] of Object.entries(config.query)) {
                    if (value !== undefined) {
                        url.searchParams.set(key, String(value));
                    }
                }
                logger.trace({ extraQuery: config.query, originalUrl: modifiedRequest.url, newUrl: url.toString() }, '[ExtraParams] Adding extra query params to URL');
                modifiedRequest = new Request(url.toString(), {
                    method: modifiedRequest.method,
                    headers: modifiedRequest.headers,
                    ...(canHaveBody && modifiedRequest.body ? { body: modifiedRequest.body, duplex: 'half' } : {}),
                });
            }
            // Merge extra body fields for JSON requests
            if (config.body && Object.keys(config.body).length > 0) {
                const contentType = modifiedRequest.headers.get('content-type');
                if (contentType?.includes('application/json') && modifiedRequest.body) {
                    const clonedRequest = modifiedRequest.clone();
                    const originalBody = await clonedRequest.text();
                    try {
                        const parsedBody = JSON.parse(originalBody);
                        const mergedBody = { ...parsedBody, ...config.body };
                        logger.trace({ originalBody: parsedBody, extraBody: config.body, mergedBody }, '[ExtraParams] Merging extra body fields into request');
                        modifiedRequest = new Request(modifiedRequest.url, {
                            method: modifiedRequest.method,
                            headers: modifiedRequest.headers,
                            body: JSON.stringify(mergedBody),
                        });
                    }
                    catch {
                        logger.warn('[ExtraParams] Could not parse request body as JSON, skipping body merge');
                    }
                }
                else if (!modifiedRequest.body && canHaveBody) {
                    // No existing body, create one with extra fields (only for methods that allow a body)
                    logger.trace({ body: config.body }, '[ExtraParams] Creating new body with extra fields');
                    const headers = new Headers(modifiedRequest.headers);
                    headers.set('content-type', 'application/json');
                    modifiedRequest = new Request(modifiedRequest.url, {
                        method: modifiedRequest.method,
                        headers,
                        body: JSON.stringify(config.body),
                    });
                }
            }
            return modifiedRequest;
        },
    };
}
//# sourceMappingURL=middleware.js.map