/*!
 * Copyright (C) Microsoft Corporation. All rights reserved.
 */
import { DataSources, HttpMethod } from '../common/types';
import { arrayBufferToBase64, convertArrayBufferToString, extractDataverseUrlParts, } from '../common/utils';
import { createErrorResponse, DataOperationErrorMessages, ErrorCodes, getErrorMessage, parseHttpPluginError, PowerDataRuntimeError, } from '../error/error';
import { isOperationResult } from '../types';
/**
 * RuntimeDataClient handles data operations through PowerOperationExecutor
 * @private
 */
export class RuntimeDataClient {
    _powerOperationExecutor;
    // Static identifiers for services
    // Used to identify specific services within the PowerApps environment
    static SERVICES = {
        dataClient: 'AppHttpClientPlugin',
        identityService: 'AppIdentityServicePlugin',
    };
    // Static identifiers for service actions
    // Used to identify specific actions within the service
    // These actions are used to send HTTP requests and get access tokens
    static ACTIONS = {
        sendHttp: 'sendHttpAsync',
        getToken: 'getAppAccessTokenAsync',
        getDynamicToken: 'getAppDynamicResourceAccessTokenAsync',
    };
    // Request source identifier for telemetry
    // Used to identify the source of the request in telemetry data
    static REQUEST_SOURCE = 'PublishedApp';
    // Constructor for RuntimeDataClient
    // Accepts an IPowerOperationExecutor instance for executing operations
    constructor(_powerOperationExecutor) {
        this._powerOperationExecutor = _powerOperationExecutor;
    }
    /**
     * Creates a new instance of RuntimeDataClient
     */
    static createInstanceAsync(powerOperationExecutor) {
        return Promise.resolve(new RuntimeDataClient(powerOperationExecutor));
    }
    /**
     * Creates data using POST method
     * @param url - The URL for the request
     * @param apiId - The API ID for authentication
     * @param tableName - The name of the table to access
     * @param body - The request body for the POST method
     * @param operationName - Optional operation name for telemetry
     * @return Promise resolving to the response data
     * @throws Error if the request fails or the response is invalid
     * @throws Error if the request body is invalid
     */
    async createDataAsync(url, apiId, tableName, body, context) {
        try {
            if (!body) {
                throw new Error(`${DataOperationErrorMessages.InvalidRequest}: ${DataOperationErrorMessages.MissingRequestBody}`);
            }
            const config = {
                url,
                method: HttpMethod.POST,
                apiId,
                tableName,
                body: JSON.stringify(body),
            };
            context = this._ensureContext(context, 'runtimeDataClient.createDataAsync');
            return await this._executeRequest(config, context);
        }
        catch (error) {
            if (isOperationResult(error)) {
                return error;
            }
            else {
                return createErrorResponse(error, DataOperationErrorMessages.CreateFailed);
            }
        }
    }
    /**
     * Updates data using PATCH method
     * @param url - The URL for the request
     * @param apiId - The API ID for authentication
     * @param tableName - The name of the table to access
     * @param body - The request body for the PATCH method
     * @param operationName - Optional operation name for telemetry
     * @return Promise resolving to the response data
     * @throws Error if the request fails or the response is invalid
     * @throws Error if the request body is invalid
     */
    async updateDataAsync(url, apiId, tableName, body, context) {
        try {
            if (!body) {
                throw new Error(`${DataOperationErrorMessages.InvalidRequest}: ${DataOperationErrorMessages.MissingRequestBody}`);
            }
            const config = {
                url,
                method: HttpMethod.PATCH,
                apiId,
                tableName,
                body: JSON.stringify(body),
            };
            context = this._ensureContext(context, 'runtimeDataClient.updateDataAsync');
            return await this._executeRequest(config, context);
        }
        catch (error) {
            if (isOperationResult(error)) {
                return error;
            }
            else {
                return createErrorResponse(error, DataOperationErrorMessages.UpdateFailed);
            }
        }
    }
    /**
     * Uploads data using PATCH method for file/image columns.
     * @param url - The URL for the request (should include the column name, e.g., /accounts(id)/sample_filecolumn)
     * @param apiId - The API ID for authentication
     * @param tableName - The name of the table to access
     * @param data - The binary content to upload
     * @param context - Operation context, must include fileName for upload and may include telemetry information
     * @return Promise resolving to the response data
     * @remarks File size is not validated client-side; limits are enforced server-side by Dataverse.
     * For file columns the limit is 128 MB or the column's MaxSizeInKB setting, whichever is lower.
     * For image columns the maximum configurable size is 30 MB.
     */
    async uploadDataAsync(url, apiId, tableName, data, context) {
        try {
            if (!data) {
                throw new Error(`${DataOperationErrorMessages.InvalidRequest}: ${DataOperationErrorMessages.MissingRequestBody}`);
            }
            // Expecting data (body) to contain binary data and file name for upload
            if (!context?.fileName) {
                throw new Error(`${DataOperationErrorMessages.InvalidRequest}: ${DataOperationErrorMessages.MissingFileData}`);
            }
            const config = {
                url,
                method: HttpMethod.PATCH,
                apiId,
                tableName,
                headers: {
                    'Content-Type': 'application/octet-stream',
                    'x-ms-file-name': context.fileName,
                },
                body: data,
            };
            context = this._ensureContext(context, 'runtimeDataClient.uploadDataAsync');
            return await this._executeRequest(config, context);
        }
        catch (error) {
            if (isOperationResult(error)) {
                return error;
            }
            else {
                return createErrorResponse(error, DataOperationErrorMessages.UploadFailed);
            }
        }
    }
    /**
     * Deletes data using DELETE method
     * @param url - The URL for the request
     * @param connectionApi - The API ID for authentication
     * @param serviceNamespace - The name of the service namespace
     * @param operationName - Optional operation name for telemetry
     * @return Promise resolving to the response data
     * @throws Error if the request fails or the response is invalid
     */
    async deleteDataAsync(url, connectionApi, serviceNamespace, context) {
        try {
            const config = {
                url,
                method: HttpMethod.DELETE,
                apiId: connectionApi,
                tableName: serviceNamespace,
            };
            context = this._ensureContext(context, 'runtimeDataClient.deleteDataAsync');
            return await this._executeRequest(config, context);
        }
        catch (error) {
            if (isOperationResult(error)) {
                return error;
            }
            else {
                return createErrorResponse(error, DataOperationErrorMessages.DeleteFailed);
            }
        }
    }
    /**
     * Retrieves data using GET or POST method
     * @param url - The URL for the request
     * @param apiId - The API ID for authentication
     * @param tableName - The name of the table to access
     * @param method - The HTTP method
     * @param body - Optional request body for POST method
     * @param context - Optional operation context
     * @param operationName - Optional operation name for telemetry
     * @return Promise resolving to the response data
     * @throws Error if the request fails or the response is invalid
     */
    async retrieveDataAsync(url, apiId, tableName, method, headers, body, context) {
        try {
            const config = {
                url,
                method,
                apiId,
                tableName,
                headers,
                body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
            };
            context = this._ensureContext(context, 'runtimeDataClient.retrieveDataAsync');
            return await this._executeRequest(config, context);
        }
        catch (error) {
            if (isOperationResult(error)) {
                return error;
            }
            else {
                return createErrorResponse(error, DataOperationErrorMessages.RetrieveFailed);
            }
        }
    }
    /**
     * Gets an access token for the specified API.
     * If the API is Dataverse, retrieves a dynamic resource token; otherwise, retrieves a standard appservice API token.
     * @param apiId - The API ID for authentication
     * @param datasetName - Optional dataset name for Dataverse
     * @returns Promise resolving to the access token
     * @throws Error if token acquisition fails
     */
    async _getAccessToken(apiId, datasetName) {
        try {
            let result;
            if (apiId === DataSources.Dataverse) {
                // For Dataverse datasources, use dynamic token with datasetName as resourceId
                result = await this._powerOperationExecutor.execute(RuntimeDataClient.SERVICES.identityService, RuntimeDataClient.ACTIONS.getDynamicToken, [datasetName]);
            }
            else {
                result = await this._powerOperationExecutor.execute(RuntimeDataClient.SERVICES.identityService, RuntimeDataClient.ACTIONS.getToken, [apiId]);
            }
            return result.data;
        }
        catch (error) {
            throw new PowerDataRuntimeError(ErrorCodes.TokenAcquisitionFailed, getErrorMessage(error));
        }
    }
    // Merge Prefer headers for Dataverse batch payloads
    _mergePreferHeaders(configHeaders, method) {
        let preferHeader = '';
        if (configHeaders?.Prefer) {
            preferHeader += configHeaders.Prefer;
        }
        if (method === HttpMethod.POST || method === HttpMethod.PATCH) {
            const defaultPrefer = 'return=representation,odata.include-annotations=*';
            if (preferHeader) {
                // Only add if not already present
                if (!preferHeader.includes('return=representation')) {
                    preferHeader += (preferHeader ? ',' : '') + defaultPrefer;
                }
            }
            else {
                preferHeader = defaultPrefer;
            }
        }
        return preferHeader;
    }
    /**
     * Creates headers for the HTTP request.
     * Combines default headers with any custom headers provided in the config.
     * Custom headers are optional and take precedence over default headers.
     * @param token - The access token for authentication
     * @param config - The HTTP request configuration
     * @return The headers for the request
     * @throws Error if header creation fails
     */
    _createHeaders(token, config, context) {
        const isFileUpload = config.headers?.['Content-Type'] === 'application/octet-stream';
        const baseHeaders = {
            Accept: 'application/json',
            'x-ms-protocol-semantics': 'cdp',
            ServiceNamespace: config.tableName,
            Authorization: `paauth ${token}`,
            'x-ms-pa-client-custom-headers-options': '{"addCustomHeaders":true}',
            'x-ms-enable-selects': 'true',
            'x-ms-pa-client-telemetry-options': `paclient-telemetry {"operationName":"${context?.operationName ?? 'runtimeDataClient.executeRequest'}"}`,
            'x-ms-pa-client-telemetry-additional-data': `{"apiId":"${config.apiId}"}`,
        };
        if (config.apiId === DataSources.Dataverse) {
            // Extract baseUrl and encodedPath for Dataverse batch payload construction
            baseHeaders['x-ms-protocol-semantics'] = DataSources.Dataverse;
            baseHeaders.Authorization = `dynamicauth ${token}`;
            const { baseUrl, encodedPath } = extractDataverseUrlParts(config.url);
            const batchId = context?.batchId || '';
            const preferHeader = this._mergePreferHeaders(config.headers, config.method);
            if (!isFileUpload && !context?.skipBatch) {
                baseHeaders.BatchInfo = JSON.stringify({
                    baseUrl,
                    encodedPath, // for use in batch payload construction
                    headers: {
                        Accept: 'application/json',
                        ...(preferHeader ? { Prefer: preferHeader } : {}),
                        ...(config.method === HttpMethod.POST || config.method === HttpMethod.PATCH
                            ? { 'Content-Type': 'application/json' }
                            : {}),
                    },
                    batchId,
                });
            }
        }
        if (config.headers) {
            return { ...baseHeaders, ...config.headers };
        }
        return baseHeaders;
    }
    /**
     * Executes an HTTP request with the given configuration
     * @param config - The HTTP request configuration
     * @param context - Optional operation context
     * @return Promise resolving to the response data
     * @throws Error if the request fails or the response is invalid
     * @throws Error if the response content type is invalid
     */
    async _executeRequest(config, context) {
        const token = await this._getAccessToken(config.apiId, context?.datasetName);
        const headers = this._createHeaders(token, config, context);
        const requestBody = config.body
            ? config.headers?.['Content-Type'] === 'application/octet-stream'
                ? new Blob([config.body], { type: 'application/octet-stream' })
                : new Blob([config.body], { type: 'application/json' })
            : '';
        let result;
        try {
            result = await this._powerOperationExecutor.execute(RuntimeDataClient.SERVICES.dataClient, RuntimeDataClient.ACTIONS.sendHttp, [
                {
                    url: config.url,
                    method: config.method,
                    requestSource: RuntimeDataClient.REQUEST_SOURCE,
                    allowSessionStorage: true,
                    returnDirectResponse: true,
                    headers,
                },
                requestBody,
                'arraybuffer',
            ]);
        }
        catch (error) {
            return {
                success: false,
                error: parseHttpPluginError(error),
                data: undefined,
            };
        }
        const responseData = result.data;
        const responseHeaders = responseData[0].headers;
        const contentType = responseHeaders['Content-Type'];
        if (!contentType) {
            return {
                success: true,
                data: undefined,
            };
        }
        else if (contentType.indexOf('application/json') !== -1) {
            const data = result.data[1];
            let text = this._decodeArrayBuffer(data);
            if (!text) {
                text = '{}'; // Handle empty response gracefully
            }
            const parsedResult = JSON.parse(text);
            if (context?.isDataVerseOperation || this._isDataverseCall(config.url)) {
                // If the call was to a native Dataverse API, we return the complete OData response
                // that includes metadata, annotations, and the value array
                return {
                    success: true,
                    data: parsedResult,
                };
            }
            // Check if the response is an OData format with a value array
            else if (!context?.isExecuteAsync &&
                'value' in parsedResult &&
                Array.isArray(parsedResult.value)) {
                // Cast to OData response and return just the value array
                return {
                    success: true,
                    data: parsedResult.value,
                    count: parsedResult['@odata.count'],
                };
            }
            else {
                // Return the parsed result directly
                return {
                    success: true,
                    data: parsedResult,
                };
            }
        }
        else if (contentType.indexOf('image/') !== -1) {
            const buffer = result.data[1];
            if (buffer instanceof ArrayBuffer) {
                const value = arrayBufferToBase64(buffer);
                return {
                    success: true,
                    data: value,
                };
            }
            return {
                success: true,
                data: buffer,
            };
        }
        else if (contentType.indexOf('application/octet-stream') !== -1) {
            const buffer = result.data[1];
            const rawFileName = responseHeaders['x-ms-file-name'];
            const fileName = typeof rawFileName === 'string' && rawFileName ? rawFileName : undefined;
            return {
                success: true,
                data: (buffer instanceof ArrayBuffer
                    ? new Uint8Array(buffer)
                    : buffer),
                ...(fileName !== undefined ? { fileName } : {}),
            };
        }
        else {
            const buffer = result.data[1];
            if (buffer instanceof ArrayBuffer) {
                const value = convertArrayBufferToString(buffer);
                const status = responseData[0].status;
                const responseType = context?.responseInfo?.[status];
                if (responseType) {
                    let parsedValue;
                    try {
                        parsedValue = JSON.parse(value);
                    }
                    catch (err) {
                        return {
                            success: false,
                            data: undefined,
                            error: new Error(DataOperationErrorMessages.InvalidResponse),
                        };
                    }
                    if (responseType.type === 'array' && !Array.isArray(parsedValue)) {
                        return {
                            success: false,
                            data: undefined,
                            error: new Error(DataOperationErrorMessages.InvalidResponse),
                        };
                    }
                    if (responseType.type === 'object' &&
                        (typeof parsedValue !== 'object' || Array.isArray(parsedValue) || parsedValue === null)) {
                        return {
                            success: false,
                            data: undefined,
                            error: new Error(DataOperationErrorMessages.InvalidResponse),
                        };
                    }
                    return {
                        success: true,
                        data: parsedValue,
                    };
                }
                else {
                    // Return the string value directly
                    return {
                        success: true,
                        data: value,
                    };
                }
            }
            const statusCode = responseData[0].status;
            if (statusCode >= 200 && statusCode < 300) {
                // Treat 2xx responses with no body (e.g. 204 No Content from delete/upload) as success with undefined data.
                return {
                    success: true,
                    data: undefined,
                };
            }
            return {
                success: false,
                data: responseData,
                error: new Error(DataOperationErrorMessages.InvalidResponse),
            };
        }
    }
    _ensureContext(context, defaultOperationName) {
        if (!context) {
            context = {};
        }
        if (!context.operationName) {
            context.operationName = defaultOperationName;
        }
        return context;
    }
    /**
     * Checks if the given URL is a Dataverse API call
     * @param url - The URL to check
     * @returns True if the URL is a Dataverse API call, false otherwise
     */
    _isDataverseCall(url) {
        if (!url) {
            return false;
        }
        const urlLower = decodeURIComponent(url).toLowerCase();
        return urlLower.includes('/api/data/') && !urlLower.includes('/apim');
    }
    /**
     * Decodes ArrayBuffer to string, handling both browser and Node.js environments
     * @param buffer - The ArrayBuffer to decode
     * @returns The decoded string
     */
    _decodeArrayBuffer(buffer) {
        // Try using native TextDecoder first (browser environment)
        if (typeof TextDecoder !== 'undefined') {
            return new TextDecoder().decode(buffer);
        }
        // Fallback for environments without TextDecoder (e.g., Jest/Node.js)
        // Convert ArrayBuffer to Uint8Array and then to string
        const uint8Array = new Uint8Array(buffer);
        const results = [];
        // Use a chunk-based approach for better performance with large buffers
        const chunkSize = 8192;
        for (let i = 0; i < uint8Array.length; i += chunkSize) {
            const chunk = uint8Array.subarray(i, Math.min(i + chunkSize, uint8Array.length));
            results.push(String.fromCharCode.apply(null, Array.from(chunk)));
        }
        // Handle UTF-8 decoding for proper Unicode support
        try {
            return results.join('');
        }
        catch {
            // If UTF-8 decoding fails, return the raw string
            return results.join('');
        }
    }
}
//# sourceMappingURL=runtimeDataClient.js.map