/*!
 * Copyright (C) Microsoft Corporation. All rights reserved.
 */
import { DataSources, HttpMethod } from '../../common/types';
import { DataverseOperationName } from '../../error/constants';
import { createErrorResponse, DataOperationErrorMessages, ErrorCodes, PowerDataRuntimeError, } from '../../error/error';
import { Log } from '../../telemetry/log';
import { convertOptionsToQueryString } from './shared/stringQueryOptions';
/** @private */
export const ODATA_CONTEXT = '@odata.context';
/** @private */
export const ODATA_NEXT_LINK = '@odata.nextLink';
/** @private */
export const CRM_TOTAL_RECORD_COUNT = '@Microsoft.Dynamics.CRM.totalrecordcount';
/** @private */
export const CRM_TOTAL_RECORD_COUNT_LIMIT_EXCEEDED = '@Microsoft.Dynamics.CRM.totalrecordcountlimitexceeded';
/** @private */
export const CRM_GLOBAL_METADATA_VERSION = '@Microsoft.Dynamics.CRM.globalmetadataversion';
/**
 * DataverseDataOperation provides functionality for performing CRUD operations
 * against the Dataverse data source using the XRM WebApi or runtime metadata client.
 * @private
 */
export class DataverseDataOperationExecutor {
    // Static identifiers for services and actions
    // Used to identify specific services and actions within the PowerApps environment
    _clientProvider;
    _dataSourceService;
    _databaseReferences;
    constructor(clientProvider, dataSourceService) {
        this._clientProvider = clientProvider;
        this._dataSourceService = dataSourceService;
    }
    /**
     * Creates a new record in Dataverse
     * @param tableName - The name of the table
     * @param data - The record data to create
     * @returns Promise resolving to operation result
     */
    async createRecordAsync(tableName, data) {
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.createDataAsync(requestUrl, DataSources.Dataverse, // Use environment name for Dataverse authentication
            tableName, data, {
                operationName: DataverseOperationName.CreateRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
            });
            const returnValue = {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
            };
            return returnValue;
        }, DataOperationErrorMessages.CreateFailed);
    }
    /**
     * Updates an existing record in Dataverse
     * @param tableName - The name of the table
     * @param id - The record identifier
     * @param data - The updated record data
     * @returns Promise resolving to operation result
     */
    async updateRecordAsync(tableName, id, data) {
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, `(${id})`), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.updateDataAsync(requestUrl, DataSources.Dataverse, tableName, data, {
                operationName: DataverseOperationName.UpdateRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
            });
            const returnValue = {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
            };
            return returnValue;
        }, DataOperationErrorMessages.UpdateFailed);
    }
    /**
     * Uploads a record with binary data to Dataverse
     * @param tableName - The name of the table
     * @param id - The record identifier
     * @param columnName - The name of the file column
     * @param fileName - The name of the file (used for Dataverse file columns)
     * @param data - The binary data to upload
     * @returns Promise resolving to operation result
     */
    async uploadFileToRecord(tableName, id, columnName, fileName, data) {
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, `(${id})/${columnName}`), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.uploadDataAsync(requestUrl, DataSources.Dataverse, tableName, data, {
                operationName: DataverseOperationName.UploadRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
                fileName,
            });
            const returnValue = {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
            };
            return returnValue;
        }, DataOperationErrorMessages.UploadFailed);
    }
    /**
     * Downloads binary data from a file column in Dataverse
     * @param tableName - The name of the table
     * @param id - The record identifier
     * @param columnName - The name of the file column
     * @returns Promise resolving to operation result with file contents as binary data (Uint8Array)
     */
    async downloadFileFromRecord(tableName, id, columnName) {
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, `(${id})/${columnName}/$value`), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.retrieveDataAsync(requestUrl, DataSources.Dataverse, tableName, HttpMethod.GET, { Accept: 'application/octet-stream' }, undefined, {
                operationName: DataverseOperationName.DownloadRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
                skipBatch: true,
            });
            const returnValue = {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
                fileName: dataverseResponse.fileName,
            };
            return returnValue;
        }, DataOperationErrorMessages.DownloadFailed);
    }
    /**
     * Downloads an image from a Dataverse image column via the Web API $value endpoint.
     * @param tableName - The logical name of the Dataverse table.
     * @param recordId - The identifier of the record.
     * @param columnName - The name of the image column.
     */
    async downloadImageFromRecord(tableName, recordId, columnName, fullSize = false) {
        const urlSuffix = `(${recordId})/${columnName}/$value${fullSize ? '?size=full' : ''}`;
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, urlSuffix), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.retrieveDataAsync(requestUrl, DataSources.Dataverse, tableName, HttpMethod.GET, { Accept: 'application/octet-stream' }, undefined, {
                operationName: DataverseOperationName.DownloadRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
                skipBatch: true,
            });
            return {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
                fileName: deriveImageFileName(columnName, dataverseResponse.data),
            };
        }, DataOperationErrorMessages.DownloadFailed);
    }
    /**
     * Deletes a record from Dataverse
     * @param tableName - The name of the table
     * @param id - The record identifier
     * @returns Promise resolving to operation result
     */
    async deleteRecordAsync(tableName, id) {
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, `(${id})`), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.deleteDataAsync(requestUrl, DataSources.Dataverse, tableName, {
                operationName: DataverseOperationName.DeleteRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
            });
            const returnValue = {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
            };
            return returnValue;
        }, DataOperationErrorMessages.DeleteFailed);
    }
    /**
     * Deletes the file or image data stored in a file/image column of an existing record.
     * Sends a DELETE request to `/<tableName>(<recordId>)/<columnName>`.
     * Only applicable to Dataverse data sources.
     * @param tableName - The logical name of the Dataverse table.
     * @param recordId - The identifier of the record.
     * @param columnName - The name of the file or image column whose data should be deleted.
     */
    async deleteFileOrImageFromRecord(tableName, recordId, columnName) {
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, `(${recordId})/${columnName}`), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.deleteDataAsync(requestUrl, DataSources.Dataverse, tableName, {
                operationName: DataverseOperationName.DeleteFileOrImageRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
            });
            return {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
            };
        }, DataOperationErrorMessages.DeleteFileOrImageFailed);
    }
    /**
     * Retrieves a single record from Dataverse
     * @param tableName - The name of the table
     * @param id - The record identifier
     * @param options - The retrieval options
     * @returns Promise resolving to operation result
     */
    async retrieveRecordAsync(tableName, id, options) {
        const { maxPageSize = 500, ...rest } = options || {};
        const optionsString = convertOptionsToQueryString(rest);
        // Build Prefer header with updated maxPageSize
        const headers = { Prefer: `odata.maxpagesize=${maxPageSize},odata.include-annotations=*` };
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, `(${id})${optionsString}`), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.retrieveDataAsync(requestUrl, DataSources.Dataverse, tableName, HttpMethod.GET, headers, undefined, // No body for GET requests
            {
                operationName: DataverseOperationName.RetrieveRecord,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
            });
            const returnValue = {
                success: dataverseResponse.success,
                data: dataverseResponse.data,
                error: dataverseResponse.error,
            };
            return returnValue;
        }, DataOperationErrorMessages.RetrieveFailed);
    }
    /**
     * Retrieves multiple records from Dataverse
     * @param tableName - The name of the table
     * @param options - The retrieval options
     * @param maxPageSize - Optional maximum page size
     * @returns Promise resolving to operation result
     */
    async retrieveMultipleRecordsAsync(tableName, options) {
        const { maxPageSize = 500, ...rest } = options || {};
        const optionsString = convertOptionsToQueryString(rest);
        // Build Prefer header with updated maxPageSize
        const headers = { Prefer: `odata.maxpagesize=${maxPageSize},odata.include-annotations=*` };
        return this._executeNativeDataverseOperation(tableName, (dataSourceInfo, tblName) => this._getDataverseRequestUrl(dataSourceInfo, tblName, optionsString), async (dataClient, requestUrl, dataSourceInfo) => {
            const dataverseResponse = await dataClient.retrieveDataAsync(requestUrl, DataSources.Dataverse, tableName, HttpMethod.GET, headers, undefined, // No body for GET requests
            {
                operationName: DataverseOperationName.RetrieveMultipleRecords,
                datasetName: dataSourceInfo.datasetName,
                isDataVerseOperation: true,
            });
            const returnValue = {
                success: dataverseResponse.success,
                data: dataverseResponse?.data?.value || [],
                skipToken: extractSkipToken(dataverseResponse?.data?.[ODATA_NEXT_LINK]),
                error: dataverseResponse.error,
            };
            return returnValue;
        }, DataOperationErrorMessages.RetrieveMultipleFailed);
    }
    /**
     * Executes a custom Dataverse operation
     * @param operation - The operation to execute
     * @returns Promise resolving to operation result
     */
    async executeAsync(operation) {
        const { dataverseRequest } = operation;
        if (!dataverseRequest) {
            return {
                success: false,
                data: null,
                error: { message: 'Dataverse request details are required for Dataverse operations.' },
            };
        }
        const { action, parameters } = dataverseRequest;
        switch (action) {
            // Future custom actions can be handled here
            case 'getEntityMetadata': {
                const { tableName, options } = parameters;
                if (!tableName) {
                    return {
                        success: false,
                        data: null,
                        error: { message: 'Table name is required for getEntityMetadata action.' },
                    };
                }
                return this._getEntityMetadata(tableName, options ?? {});
            }
            case 'customapi': {
                const { operationName, tableName, body } = parameters;
                const dataSourceInfo = await this._dataSourceService.getDataSource(tableName);
                const apiDef = dataSourceInfo.apis[operationName];
                if (!apiDef) {
                    return {
                        success: false,
                        data: null,
                        error: {
                            message: `Operation '${operationName}' not found in data source '${tableName}'.`,
                        },
                    };
                }
                const dvInfo = await this._getDefaultDataverseEnvironmentInfo();
                const instanceUrl = this._getInstanceUrl(dvInfo);
                const bodyRecord = (body ?? {});
                // Substitute path parameters into the pre-built OData path
                // Collect body params (actions) and OData function params (functions, in: 'query')
                let resolvedPath = apiDef.path;
                const requestBody = {};
                const functionParams = [];
                for (const param of apiDef.parameters) {
                    if (param.in === 'path') {
                        resolvedPath = resolvedPath.replace(`{${param.name}}`, encodeURIComponent(String(bodyRecord[param.name] ?? '')));
                    }
                    else if (param.in === 'body') {
                        if (param.name in bodyRecord) {
                            requestBody[param.name] = bodyRecord[param.name];
                        }
                    }
                    else if (param.in === 'query') {
                        // Dataverse functions pass parameters as OData function params: FunctionName(p1=v1,p2=v2)
                        if (param.name in bodyRecord) {
                            functionParams.push(`${param.name}=${toODataLiteral(bodyRecord[param.name], param.type)}`);
                        }
                    }
                }
                if (functionParams.length > 0) {
                    resolvedPath += `(${functionParams.join(',')})`;
                }
                const requestUrl = `${instanceUrl}${resolvedPath.startsWith('/') ? resolvedPath.slice(1) : resolvedPath}`;
                const dataClient = await this._getDataClient();
                const isGet = apiDef.method.toUpperCase() === HttpMethod.GET;
                const response = isGet
                    ? await dataClient.retrieveDataAsync(requestUrl, DataSources.Dataverse, operationName, HttpMethod.GET, undefined, undefined, {
                        operationName: DataverseOperationName.ExecuteCustomApi,
                        datasetName: dvInfo.datasetName,
                        isDataVerseOperation: true,
                    })
                    : await dataClient.createDataAsync(requestUrl, DataSources.Dataverse, operationName, requestBody, {
                        operationName: DataverseOperationName.ExecuteCustomApi,
                        datasetName: dvInfo.datasetName,
                        isDataVerseOperation: true,
                    });
                return { success: response.success, data: response.data, error: response.error };
            }
            default:
                Log.trackEvent('DataverseDataOperation.UnsupportedAction', {
                    message: `Unsupported Dataverse action: ${action}`,
                });
                return {
                    success: false,
                    data: null,
                    error: { message: `Unsupported Dataverse action: "${action}"` },
                };
        }
    }
    async _getEntityMetadata(tableName, options) {
        const client = await this._getDataClient();
        const dataSourceInfo = await this._getDataverseDataSourceInfo(tableName);
        const url = this._generateMetadataRequestUrl(dataSourceInfo, options);
        return client.retrieveDataAsync(url, DataSources.Dataverse, 'EntityDefinitions', HttpMethod.GET, {
            Consistency: 'Strong', // Force CDS to return latest metadata
        }, undefined, {
            operationName: DataverseOperationName.RetrieveRecord,
            datasetName: dataSourceInfo.datasetName,
            isDataVerseOperation: true,
        });
    }
    /**
     * Returns the database references for Dataverse, grouped by environment/database.
     * These come from the launch app response via runtime metadata client.
     */
    async getDatabaseReferences() {
        if (this._databaseReferences) {
            return this._databaseReferences;
        }
        // Get database references from runtime metadata (launch app response)
        // This is the authoritative source containing the complete linkedEnvironmentMetadata
        const runtimeDatabaseReferences = await this._loadDatabaseReferencesFromRuntime();
        if (runtimeDatabaseReferences && Object.keys(runtimeDatabaseReferences).length > 0) {
            this._databaseReferences = runtimeDatabaseReferences;
            return this._databaseReferences;
        }
        throw new PowerDataRuntimeError(ErrorCodes.DataSourceNotFound, 'Failed to load Dataverse database references from runtime.');
    }
    /**
     * Loads database references from runtime metadata client (launch app response).
     */
    async _loadDatabaseReferencesFromRuntime() {
        try {
            const metadataClient = await this._getMetadataClient();
            // Get CDS data source configs from runtime metadata
            const response = await metadataClient.getAppDataSourceConfigsAsync();
            if (!response.success || !response.data) {
                return undefined;
            }
            const cdsDataSources = Object.values(response.data);
            if (cdsDataSources.length === 0) {
                return undefined;
            }
            // Build database references from runtime CDS data source configs
            const databaseReferences = {};
            for (const cdsDataSource of cdsDataSources) {
                // Type assertion for CDS data source config
                const cdsConfig = cdsDataSource;
                // Extract instance URL and other metadata from runtime URL
                const instanceUrl = this._extractInstanceUrlFromRuntimeUrl(cdsConfig.runtimeUrl);
                // Use a standard environment name for CDS
                const envName = 'default.cds';
                if (!databaseReferences[envName]) {
                    databaseReferences[envName] = {
                        databaseDetails: {
                            referenceType: 'Environmental',
                            environmentName: envName,
                            overrideValues: {
                                status: 'NotSpecified',
                                environmentVariableName: '',
                            },
                            linkedEnvironmentMetadata: {
                                resourceId: '', // This would be available in the full app info
                                friendlyName: '',
                                uniqueName: '',
                                domainName: '',
                                version: cdsConfig.version || '9.2',
                                instanceUrl,
                                instanceApiUrl: cdsConfig.runtimeUrl,
                                baseLanguage: 1033,
                                instanceState: 'Ready',
                                createdTime: '',
                                platformSku: '',
                            },
                        },
                        dataSources: {},
                    };
                }
                // Add the data source
                const dataSourceName = cdsConfig.entitySetName || cdsConfig.logicalName;
                databaseReferences[envName].dataSources[dataSourceName] = {
                    entitySetName: cdsConfig.entitySetName,
                    logicalName: cdsConfig.logicalName,
                    isHidden: false,
                };
            }
            return databaseReferences;
        }
        catch (error) {
            // Info-level logging for tracing errors loading from runtime
            Log.trackEvent('DataverseDataOperation.FailedToLoadDatabaseReferences', {
                message: '[DataverseDataOperation] Failed to load database references from runtime',
                error,
            });
            // Silently fail and return undefined if there's any error loading from runtime
            return undefined;
        }
    }
    _extractInstanceUrlFromRuntimeUrl(runtimeUrl) {
        try {
            // Runtime URL format: https://org.crm.dynamics.com/api/data/v9.2/
            // Extract: https://org.crm.dynamics.com
            // Using a simpler approach to avoid URL import issues
            const matches = runtimeUrl.match(/^(https?:\/\/[^/]+)/);
            return matches ? matches[1] : runtimeUrl;
        }
        catch (error) {
            Log.trackEvent('DataverseDataOperation.FailedToExtractInstanceUrl', {
                message: '[DataverseDataOperation] Failed to extract instance URL from runtime URL',
                error,
            });
            // Fallback to original URL on error
            return runtimeUrl;
        }
    }
    /**
     * Helper to get a native data client and database reference
     */
    async _getDataClient() {
        const dataClient = await this._clientProvider.getDataClientAsync();
        if (!dataClient) {
            Log.trackEvent('DataverseDataOperation.DataClientNotAvailable', {
                message: '[DataverseDataOperation] Data client is not available',
            });
            throw new PowerDataRuntimeError(ErrorCodes.DataClientNotAvailable, 'Data client is not available.');
        }
        return dataClient;
    }
    /**
     * Gets the metadata client instance
     */
    async _getMetadataClient() {
        const metadataClient = await this._clientProvider.getMetadataClientAsync();
        if (!metadataClient) {
            Log.trackEvent('DataverseDataOperation.MetadataClientNotAvailable', {
                message: '[DataverseDataOperation] Metadata client is not available',
            });
            throw new PowerDataRuntimeError(ErrorCodes.MetadataClientNotAvailable);
        }
        return metadataClient;
    }
    /**
     * Template method for connector-style CRUD operations to reduce duplication.
     * Handles client, dataSourceInfo, requestUrl, and error handling.
     */
    async _executeNativeDataverseOperation(tableName, buildUrl, operation, errorMessage) {
        try {
            const dataClient = await this._getDataClient();
            const dataSourceInfo = await this._getDataverseDataSourceInfo(tableName);
            const requestUrl = buildUrl(dataSourceInfo, tableName);
            return operation(dataClient, requestUrl, dataSourceInfo);
        }
        catch (error) {
            return createErrorResponse(error, errorMessage);
        }
    }
    /**
     * Helper to get the Dataverse datasourceinfo from databaseReferences
     */
    async _getDataverseDataSourceInfo(tableName) {
        let dbRefs;
        // Separate try-catch for getting database references
        try {
            dbRefs = await this.getDatabaseReferences();
        }
        catch (error) {
            Log.trackEvent('DataverseDataOperation.GetDataSourceInfoFailed', {
                message: '[DataverseDataOperation] Failed to get database references',
                tableName,
                error,
            });
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new PowerDataRuntimeError(ErrorCodes.DataSourceNotFound, `Failed to get Dataverse data source info for table '${tableName}': ${errorMessage}`);
        }
        // Find the environment/database that contains this table
        for (const dbKey of Object.keys(dbRefs)) {
            const db = dbRefs[dbKey];
            if (db.dataSources[tableName]) {
                const ds = db.dataSources[tableName];
                return {
                    datasetName: db.databaseDetails?.environmentName,
                    referenceType: db.databaseDetails?.referenceType,
                    linkedEnvironmentMetadata: db.databaseDetails?.linkedEnvironmentMetadata,
                    entitySetName: ds?.entitySetName,
                    logicalName: ds?.logicalName,
                    isHidden: ds?.isHidden,
                    tableId: ds?.logicalName,
                    apis: {},
                };
            }
        }
        // Table not found - this will now throw without being caught
        const notFoundMsg = `No Dataverse data source found for table: ${tableName}`;
        Log.trackEvent('DataverseDataOperation.DataSourceNotFound', {
            message: notFoundMsg,
            tableName,
        });
        throw new PowerDataRuntimeError(ErrorCodes.DataSourceNotFound, notFoundMsg);
    }
    /**
     * Returns the Dataverse environment info from the default CDS database reference.
     * Used for custom API operations where the data source name is not an entity table.
     */
    async _getDefaultDataverseEnvironmentInfo() {
        const dbRefs = await this.getDatabaseReferences();
        const db = dbRefs['default.cds'];
        if (!db?.databaseDetails?.linkedEnvironmentMetadata?.instanceUrl) {
            throw new PowerDataRuntimeError(ErrorCodes.DataSourceNotFound, 'No Dataverse environment found at default.cds database reference.');
        }
        return {
            datasetName: db.databaseDetails.environmentName,
            referenceType: db.databaseDetails.referenceType,
            linkedEnvironmentMetadata: db.databaseDetails.linkedEnvironmentMetadata,
            tableId: '',
            apis: {},
        };
    }
    _getInstanceUrl(dataSourceInfo) {
        const instanceUrl = dataSourceInfo.linkedEnvironmentMetadata?.instanceUrl;
        if (!instanceUrl) {
            throw new PowerDataRuntimeError(ErrorCodes.DataClientInitFailed, 'No instanceUrl found for Dataverse table.');
        }
        // Ensure instanceUrl ends with a slash and construct proper URL
        const baseUrl = instanceUrl.endsWith('/') ? instanceUrl : `${instanceUrl}/`;
        return baseUrl;
    }
    _getDataverseRequestUrl(dataSourceInfo, tableName, urlPath = '') {
        const baseUrl = this._getInstanceUrl(dataSourceInfo);
        return `${baseUrl}api/data/v9.0/${tableName}${urlPath}`;
    }
    /**
     * Constructs GET request URL for fetching metadata using options object.
     * @param dataSourceInfo - The data source information for the Dataverse table.
     * @param options - The options for the metadata request.
     * @returns The constructed metadata request URL.
     */
    _generateMetadataRequestUrl(dataSourceInfo, options) {
        const { logicalName } = dataSourceInfo;
        if (!logicalName) {
            throw new PowerDataRuntimeError(ErrorCodes.DataClientInitFailed, 'No logicalName found for Dataverse table.');
        }
        const url = new URL(`${this._getInstanceUrl(dataSourceInfo)}api/data/v9.0/EntityDefinitions(LogicalName='${logicalName}')`);
        const { metadata, schema } = options;
        const selects = new Set(Array.isArray(metadata) ? metadata : []);
        selects.add('LogicalName');
        const expands = [];
        if (schema?.manyToOne) {
            expands.push('ManyToOneRelationships');
        }
        if (schema?.oneToMany) {
            expands.push('OneToManyRelationships');
        }
        if (schema?.manyToMany) {
            expands.push('ManyToManyRelationships');
        }
        if (schema?.columns === 'all') {
            // request all attributes
            expands.push('Attributes');
        }
        else if (schema && Array.isArray(schema.columns) && schema.columns.length > 0) {
            // If specific columns are requested, include only those
            const attributesCollection = schema.columns.map((a) => `'${a}'`).join(',');
            expands.push(`Attributes($filter=Microsoft.Dynamics.CRM.In(PropertyName='LogicalName',PropertyValues=[${attributesCollection}]))`);
        }
        // otherwise don't include any attributes
        url.search = new URLSearchParams({
            $select: [...selects].join(','),
            $expand: expands.join(','),
        }).toString();
        return url.toString();
    }
}
/**
 * Extracts the skip token from the next link URL.
 * @param nextLink - The @odata.nextLink URL containing the skip token
 * @returns The extracted skip token or undefined if not found.
 */
export function extractSkipToken(nextLink) {
    if (!nextLink?.trim()) {
        return undefined;
    }
    const match = nextLink.match(/[?&]\$?skiptoken=([^&#]+)/i);
    return match ? decodeURIComponent(match[1]) : undefined;
}
/**
 * Detects the image format from the magic bytes at the start of the binary data.
 * This is necessary because Dataverse image columns do not provide file extension or MIME type information.
 * The use of download url companion column (e.g: columnName_url) is not possible due to CORS, so we have to rely on the binary data sniff to determine the extension. This is a standard technique.
 * Referring: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/image-column-data?tabs=sdk#download-url
 * Sniffing the magic bytes allows us to derive a file extension for the downloaded image, which can be used when saving the file or setting the correct MIME type for display.
 * Docs: https://en.wikipedia.org/wiki/List_of_file_signatures
 * Dataverse image columns support only GIF, JPEG, BMP, and PNG.
 * The function checks for these formats and returns the corresponding extension.
 * See: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/image-column-data?tabs=sdk#image-file-types
 * Returns a file extension string, or "png" as a fallback if the format is unrecognized.
 */
function sniffImageExtension(data) {
    if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) {
        return 'jpg';
    }
    if (data.length >= 8 &&
        data[0] === 0x89 &&
        data[1] === 0x50 &&
        data[2] === 0x4e &&
        data[3] === 0x47 &&
        data[4] === 0x0d &&
        data[5] === 0x0a &&
        data[6] === 0x1a &&
        data[7] === 0x0a) {
        return 'png';
    }
    if (data.length >= 4 &&
        data[0] === 0x47 &&
        data[1] === 0x49 &&
        data[2] === 0x46 &&
        data[3] === 0x38) {
        return 'gif';
    }
    if (data.length >= 2 && data[0] === 0x42 && data[1] === 0x4d) {
        return 'bmp';
    }
    return 'png';
}
/**
 * Derives a filename for a downloaded image by sniffing the magic bytes of the binary data.
 * Falls back to "png" if the format is unrecognized, as PNG is the most common Dataverse image format.
 */
export function deriveImageFileName(columnName, data) {
    const ext = data ? sniffImageExtension(data) : 'png';
    return `${columnName}.${ext}`;
}
/**
 * Converts a JavaScript value to an OData primitive literal string for use in function call URLs.
 * Strings are single-quoted with internal single quotes escaped by doubling.
 * Numbers and booleans are serialized as-is. Null/undefined becomes 'null'.
 */
export function toODataLiteral(value, type) {
    if (value === null || value === undefined) {
        return 'null';
    }
    if (type === 'string') {
        return `'${String(value).replace(/'/g, "''")}'`;
    }
    return String(value);
}
//# sourceMappingURL=dataverseDataOperationExecutor.js.map