/*!
 * Copyright (C) Microsoft Corporation. All rights reserved.
 */
import { processDataSourceInfo } from '../CodeGen/dataSourceInfoProcessor.js';
import { generateModelService } from '../CodeGen/modelServiceGenerator.js';
import { tryExtractDisplayNameFromCdpMetadata } from '../CodeGen/shared/cdpUtils.js';
import { sanitizeTabularResourceName } from '../CodeGen/shared/nameUtils.js';
import { DataSources } from '../Config/Config.types.js';
import { readRepoConfig, writeRepoConfig } from '../Config/ConfigManager.js';
import { initializePlayerActions } from '../initializePlayerActions.js';
import { getExistingCdpNames, upsertConnectionReference, } from '../ReferenceStores/connectionReferences.js';
import { getExistingDataverseNames, upsertDatabaseReference, } from '../ReferenceStores/databaseReferences.js';
import { DataverseMetadataService, DataverseSystemDataService, getConnectionByNameAsync, getConnectorAsync, getPlayerServiceConfig, getSqlStoredProcedureMetadataAsync, getTableMetadataAsync, getTabularConnectorVersion, isConnectionShareable, } from '../services/index.js';
import { getVfs } from '../VfsManager.js';
import { getDataverseOrgUrl, getDataverseOrgUrlByEnvironmentName, getEnvironmentVariableName, isEnvironmentVariable, normalizeParam, resolveEnvironmentVariable, } from './DataUtils.js';
function trackInfoEvent(logger, suffix, eventData) {
    try {
        logger?.trackActivityEvent?.(`AddDataSource.${suffix}`, eventData);
    }
    catch {
        /* swallow */
    }
}
/**
 * Adds a data source based on the provided Config
 * Pre-requisites:
 * Host must call `initializeDataSource` before this function.
 * Required scopes for this function:
 * PPAPI - Connectivity.Connectors.Read, Connectivity.Connections.Read
 * APIHub - user_impersonation
 * DV - user_impersonation
 * @param config - Parameters and file paths configuration. Set `config.actionsParams.skipCodeGen` to true to skip model service generation.
 * @returns Promise<void>
 */
export async function addDataSourceAsync(config) {
    const apiId = await normalizeParam(config.actionsParams.apiId);
    if (!apiId) {
        throw new Error('Argument apiId is required for connector data sources. Use --apiId or -a to specify it.');
    }
    const { logger } = config;
    initializePlayerActions({
        vfs: config.vfs,
        authProvider: config.authProvider,
        region: config.region,
        environmentName: config.environmentName,
        logger: config.logger,
    });
    const dataSourceType = apiId.toLowerCase() === DataSources.Dataverse.toLowerCase()
        ? DataSources.Dataverse
        : DataSources.Connector;
    if (dataSourceType === DataSources.Dataverse) {
        const addDataverseScenario = logger.trackScenario('AddDataSource.Dataverse', {
            tableName: config.actionsParams.tableName,
            envUrl: config.actionsParams.envUrl,
            solutionId: config.actionsParams.solutionId,
        });
        try {
            await addDataverseDataSourceAsync(config);
            addDataverseScenario.complete();
        }
        catch (err) {
            addDataverseScenario.failure({
                error: err instanceof Error ? err.message : String(err),
            });
            throw err;
        }
        return;
    }
    const addCdpScenario = logger.trackScenario('AddDataSource.Connector', config);
    try {
        await addCdpDataSourceAsync(config, apiId);
        addCdpScenario.complete();
    }
    catch (err) {
        addCdpScenario.failure({
            error: err instanceof Error ? err.message : String(err),
        });
        throw err;
    }
}
/**
 * Adds a connector data source based on the provided environment variables.
 * Expects the following environment variables:
 * - API_ID: The ID of the API (e.g., 'dataverse' or a connector ID)
 * - CONNECTION_ID: The ID of the connection (for connector data sources)
 * - DATASET?: The dataset name for tabular/stored proc connector data sources
 * @param config - Local file paths configuration. Set `config.actionsParams.skipCodeGen` to true to skip model service generation.
 * @param apiId - The resolved API ID for the connector
 * @returns Promise<void>
 */
async function addCdpDataSourceAsync(config, apiId) {
    let connectionId;
    let resourceName;
    let dataset;
    let isProcedure = false;
    // The Power Platform environment variables that override the defaults
    let datasetOverride;
    let resourceNameOverride;
    const logger = config.logger;
    const { schemaPath, codeGenPath, powerConfigPath } = config.localFilePaths;
    const connectionReferenceLogicalName = await normalizeParam(config.actionsParams.connectionRef);
    if (!apiId) {
        throw new Error('Argument apiId is required for connector data sources. Use --apiId or -a to specify it.');
    }
    if (connectionReferenceLogicalName && connectionReferenceLogicalName?.length > 0) {
        const dataverseAPI = new DataverseSystemDataService(getPlayerServiceConfig().httpClient);
        const orgUrl = await getDataverseOrgUrl(powerConfigPath);
        if (!orgUrl) {
            throw new Error('Unable to determine the Dataverse organization URL. This is required for resolving connection references.');
        }
        const resolvedConnectionId = await dataverseAPI.getConnectionIdForReference(connectionReferenceLogicalName, orgUrl, logger);
        if (resolvedConnectionId) {
            connectionId = resolvedConnectionId;
        }
        else {
            throw new Error(`Failed to resolve connection ID for reference '${connectionReferenceLogicalName}'`);
        }
    }
    // If we did not get an ID from the connection reference, try to get it from the CONNECTION_ID param
    if (!connectionId) {
        connectionId = await normalizeParam(config.actionsParams.connectionId);
    }
    // If no connection ID is provided after checking both the connection reference and CONNECTION_ID param, throw an error
    if (!connectionId) {
        throw new Error('Argument connectionId is required for connector data sources. Use --connectionId or -c to specify it.');
    }
    // Fetch connector and connection
    const connector = await getConnectorAsync(apiId, logger);
    if (!connector) {
        trackInfoEvent(logger, 'ServiceCall.GetConnector.NotFound', { apiId });
        throw new Error(`Connector with ID ${apiId} not found.`);
    }
    const connection = await getConnectionByNameAsync(connectionId, apiId, logger);
    if (!connection) {
        trackInfoEvent(logger, 'ServiceCall.GetConnectionByName.NotFound', { connectionId, apiId });
        throw new Error(`Connection with name ${connectionId} not found for connector ${apiId}.`);
    }
    const connectorName = connector.name;
    const apiName = connectorName.startsWith('shared_')
        ? connectorName.replace('shared_', '')
        : connectorName;
    // Read resourceName and dataset only for tabular connectors (those that advertise the
    // "tabular" capability), such as SQL, SharePoint, Oracle, MySQL, PostgreSQL, etc.
    const isTabular = connector.properties.capabilities?.includes('tabular') ?? false;
    if (isTabular) {
        resourceName = await normalizeParam(config.actionsParams.tableName);
        dataset = await normalizeParam(config.actionsParams.dataset);
    }
    // If SQL, check if the resourceName is actually a stored procedure
    if (apiName === 'sql') {
        const storedProcedure = await normalizeParam(config.actionsParams.sqlStoredProcedure);
        if (storedProcedure) {
            resourceName = storedProcedure;
            isProcedure = true;
        }
        else if (!resourceName) {
            throw new Error('Either SQL_STORED_PROCEDURE or TABLE must be provided for SQL data sources.');
        }
    }
    if (isEnvironmentVariable(dataset)) {
        // Handle environment variable case
        const orgUrl = await getDataverseOrgUrl(powerConfigPath);
        if (!orgUrl) {
            throw new Error('Unable to determine the Dataverse organization URL.');
        }
        datasetOverride = getEnvironmentVariableName(dataset);
        const resolvedEnvVariable = await resolveEnvironmentVariable(dataset, getPlayerServiceConfig().httpClient, orgUrl, logger);
        // the APIM metadata call expects the dataset name to be double encoded
        if (resolvedEnvVariable) {
            dataset = encodeURIComponent(encodeURIComponent(resolvedEnvVariable));
        }
    }
    if (isEnvironmentVariable(resourceName)) {
        const orgUrl = await getDataverseOrgUrl(powerConfigPath);
        if (!orgUrl) {
            throw new Error('Unable to determine the Dataverse organization URL.');
        }
        resourceNameOverride = getEnvironmentVariableName(resourceName);
        const resolvedEnvVariable = await resolveEnvironmentVariable(resourceName, getPlayerServiceConfig().httpClient, orgUrl, logger);
        if (resolvedEnvVariable) {
            resourceName = resolvedEnvVariable;
        }
    }
    let tabularMetadata;
    if (resourceName) {
        if (dataset) {
            tabularMetadata = {
                dataset,
                resourceName,
                isProcedure,
                datasetOverride,
                resourceNameOverride,
            };
        }
        else {
            throw new Error(`Dataset is required when specifying a ${isProcedure ? 'SQL stored procedure' : 'table name'}.`);
        }
    }
    if (!tabularMetadata && !connector.properties.swagger) {
        throw new Error(`Connector ${config.actionsParams.apiId} did not return a Swagger definition.`);
    }
    const authenticationType = connection.properties?.connectionParametersSet?.name;
    let sharedConnectionId;
    if (isConnectionShareable(connector, authenticationType)) {
        sharedConnectionId = connection.id;
    }
    let metadata;
    if (dataset && resourceName) {
        metadata = isProcedure
            ? await getSqlStoredProcedureMetadataAsync(connection, connector, dataset, resourceName, logger)
            : await getTableMetadataAsync(connection, connector, dataset, resourceName, logger);
    }
    else {
        metadata = connector;
    }
    await handleAddCdpDataSourceAsync({
        metadata,
        connector,
        schemaPath,
        codeGenPath,
        configPath: powerConfigPath,
        tabularMetadata,
        authenticationType,
        sharedConnectionId,
        connectionReferenceLogicalName,
        dataSourceShareLink: config.actionsParams.dataSourceShareLink,
        siteId: config.actionsParams.siteId,
    }, logger, config.actionsParams.skipCodeGen);
}
/**
 * Adds a Dataverse data source by fetching entity definition metadata
 * Expects the following environment variables:
 * - TABLE: The name of the Dataverse table
 * @param config - Local file paths configuration. Set `config.actionsParams.skipCodeGen` to true to skip model service generation.
 * @returns Promise that resolves when the data source is added
 */
async function addDataverseDataSourceAsync(config) {
    const logger = config.logger;
    const { schemaPath, codeGenPath, powerConfigPath } = config.localFilePaths;
    const tableName = (await normalizeParam(config.actionsParams.tableName))?.toLowerCase();
    if (!tableName) {
        throw new Error('Argument resourceName is required for Dataverse data sources. Use --resourceName or -t to specify it.');
    }
    // The org url might be provided by PAC CLI as an environment variable
    let orgUrl = await normalizeParam(config.actionsParams.envUrl);
    if (!orgUrl && config.environmentName && config.environmentName !== 'default') {
        orgUrl = await getDataverseOrgUrlByEnvironmentName(config.environmentName);
    }
    if (!orgUrl) {
        throw new Error('Unable to determine the Dataverse organization URL');
    }
    const { httpClient } = getPlayerServiceConfig();
    const metadataService = new DataverseMetadataService(httpClient);
    const entityDefinition = await metadataService.getEntityDefinitionAsync(orgUrl, tableName, true, logger);
    const schema = metadataService.convertEntityDefinitionToSchema(entityDefinition);
    const entitySetName = entityDefinition.EntitySetName || tableName + 's';
    await handleAddDataverseDataSource({
        schema,
        schemaPath,
        codeGenPath,
        configPath: powerConfigPath,
        orgUrl,
        entitySetName,
        logicalName: tableName,
    }, logger, config.actionsParams.skipCodeGen);
}
/**
 * Ensures a Dataverse entity data source exists for the given logical name. If the entity is
 * already tracked in `power.config.json` (identified by `logicalName` in any
 * `databaseReferences[*].dataSources` entry), this is a no-op. Otherwise fetches the entity
 * definition from Dataverse, saves the schema file, and updates `power.config.json`.
 *
 * Code generation is intentionally skipped — the caller is responsible for running
 * `generateModelService` after all entity data sources are ensured.
 */
export async function ensureDataverseEntityDataSource(params) {
    const { orgUrl, logicalName, localFilePaths, metadataService, logger } = params;
    const { schemaPath, codeGenPath, powerConfigPath } = localFilePaths;
    const config = await readRepoConfig(powerConfigPath);
    const alreadyPresent = Object.values(config.databaseReferences ?? {}).some((dbRef) => Object.values(dbRef.dataSources ?? {}).some((ds) => ds.logicalName === logicalName));
    if (alreadyPresent)
        return undefined;
    const entityDefinition = await metadataService.getEntityDefinitionAsync(orgUrl, logicalName, true, logger);
    const schema = metadataService.convertEntityDefinitionToSchema(entityDefinition);
    const entitySetName = entityDefinition.EntitySetName || logicalName + 's';
    const dataSourceName = await handleAddDataverseDataSource({
        schema,
        schemaPath,
        codeGenPath,
        configPath: powerConfigPath,
        orgUrl,
        entitySetName,
        logicalName,
    }, logger, true // skipCodeGen — caller runs generateModelService after all entities are ensured
    );
    const vfs = getVfs();
    return vfs.join(schemaPath, DataSources.Dataverse.toLowerCase(), `${dataSourceName}.Schema.json`);
}
/**
 * Handles adding a Dataverse data source by updating database references,
 * saving metadata, processing data source info, and optionally generating model services.
 * @param params - Dataverse data source parameters (schema, paths, org URL, entity info)
 * @param logger - Logger instance for telemetry
 * @param skipCodeGen - If true, skips the code generation step (model service generation) after adding the data source
 * @returns The data source name
 */
async function handleAddDataverseDataSource(params, logger, skipCodeGen) {
    const { schema, schemaPath, codeGenPath, configPath, orgUrl, entitySetName, logicalName } = params;
    const dataSourceName = await updateDatabaseReferences(configPath, {
        orgUrl,
        entitySetName,
        logicalName,
        displayName: schema.title,
    }, logger);
    const metadataFilePath = await saveMetadata(schema, DataSources.Dataverse.toLowerCase(), dataSourceName, schemaPath, undefined, logger);
    await processDataSourceInfo(schemaPath, logger);
    if (!skipCodeGen) {
        await generateModelService({
            schemaFolderPath: schemaPath,
            codeGenPath,
            schemaFilePath: metadataFilePath,
            logger,
        });
    }
    return dataSourceName;
}
/**
 * Updates database references for Dataverse data sources
 */
async function updateDatabaseReferences(configPath, params, logger) {
    try {
        trackInfoEvent(logger, 'UpdateDatabaseReferences.Start', { logicalName: params.logicalName });
        // Check if config file exists
        const vfs = getVfs();
        const configExists = await vfs.exists(configPath);
        if (!configExists) {
            throw new Error(`Configuration file not found at ${configPath}`);
        }
        const config = await readRepoConfig(configPath);
        if (!config.databaseReferences) {
            config.databaseReferences = {};
        }
        const dataSourceName = upsertDatabaseReference(config.databaseReferences, {
            ...params,
            // Provide the existing CDP data source names to avoid conflicts
            cdpDataSourceNames: getExistingCdpNames(config.connectionReferences ?? {}),
        });
        await writeRepoConfig(configPath, config);
        trackInfoEvent(logger, 'UpdateDatabaseReferences.Success', {
            logicalName: params.logicalName,
            dataSourceName,
        });
        return dataSourceName;
    }
    catch (error) {
        trackInfoEvent(logger, 'UpdateDatabaseReferences.Failure', {
            logicalName: params.logicalName,
            error: error instanceof Error ? error.message : String(error),
        });
        throw new Error(`Failed to update database references: ${error instanceof Error ? error.message : String(error)}`);
    }
}
/**
 * Handles adding a CDP (connector) data source by updating connection references,
 * saving metadata, processing data source info, and optionally generating model services.
 * @param params - CDP data source parameters (metadata, connector, paths, auth info)
 * @param logger - Logger instance for telemetry
 * @param skipCodeGen - If true, skips the code generation step (model service generation) after adding the data source
 * @returns The data source name
 */
async function handleAddCdpDataSourceAsync(params, logger, skipCodeGen) {
    const { metadata, connector, schemaPath, codeGenPath, configPath, tabularMetadata, authenticationType, sharedConnectionId, connectionReferenceLogicalName, siteId, } = params;
    // Compute values inside the function
    const apiId = connector.id;
    const connectorDisplayName = connector.properties.displayName || connector.name;
    const apiName = connector.name.toLowerCase().replace(/^shared_/, '');
    const dataSourceShareLink = params.dataSourceShareLink;
    if (tabularMetadata) {
        const resourceDisplayName = tryExtractDisplayNameFromCdpMetadata(metadata) ?? tabularMetadata.resourceName;
        tabularMetadata.friendlyName = sanitizeTabularResourceName(resourceDisplayName);
    }
    const { dataSourceName, spEntity } = await updateConnectionReferences(configPath, {
        apiId,
        apiName,
        connectorDisplayName,
        tabularMetadata,
        authenticationType,
        sharedConnectionId,
        connectionReferenceLogicalName,
        dataSourceShareLink,
        siteId,
    }, logger);
    trackInfoEvent(logger, 'UpdateConnectionReferences.Success', {
        apiName,
        dataSourceName,
        hasSpEntity: !!spEntity,
    });
    const connectorVersion = tabularMetadata ? getTabularConnectorVersion(connector) : undefined;
    const schemaFilePath = await saveMetadata(metadata, apiName, dataSourceName, schemaPath, spEntity, logger, connectorVersion);
    await processDataSourceInfo(schemaPath, logger);
    if (!skipCodeGen) {
        await generateModelService({
            schemaFolderPath: schemaPath,
            codeGenPath,
            schemaFilePath: schemaFilePath,
            logger,
        });
    }
    return dataSourceName;
}
/**
 * Writes the metadata as a JSON file {dsName}.Schema.json under schemaPath/apiName/(spEntity)/
 * Returns the file path of the saved metadata
 */
async function saveMetadata(metadata, apiName, dsName, schemaPath, spEntity, logger, connectorVersion) {
    const scenario = logger?.trackScenario('AddDataSource.SaveMetadata', {
        apiName,
        dsName,
        spEntity,
    });
    try {
        const vfs = getVfs();
        // Annotate tabular metadata with the connector's API version so that
        // processDataSourceInfo can use it instead of guessing from the table name.
        const annotatedMetadata = connectorVersion !== undefined
            ? { ...metadata, 'x-ms-connection-version': connectorVersion }
            : metadata;
        // Convert the metadata object to a JSON string
        const tabularJson = JSON.stringify(annotatedMetadata, null, 2);
        // Define the directory and file path
        let directoryPath = vfs.join(schemaPath, apiName);
        if (spEntity) {
            directoryPath = vfs.join(directoryPath, spEntity);
        }
        const filePath = vfs.join(directoryPath, `${dsName}.Schema.json`);
        // Ensure the directory exists
        await vfs.mkdir(directoryPath, { recursive: true });
        // Save the JSON string to a file
        await vfs.writeFile(filePath, tabularJson, { encoding: 'utf-8' });
        scenario?.complete();
        return filePath;
    }
    catch (error) {
        scenario?.failure({ error });
        throw error;
    }
}
/**
 * Updates connection references for connector data sources
 */
async function updateConnectionReferences(configPath, params, logger) {
    const vfs = getVfs();
    try {
        trackInfoEvent(logger, 'UpdateConnectionReferences.Start', { apiName: params.apiName });
        // Check if config file exists
        if (!(await vfs.exists(configPath))) {
            throw new Error(`Configuration file not found at ${configPath}`);
        }
        const config = await readRepoConfig(configPath);
        if (!config.connectionReferences) {
            config.connectionReferences = {};
        }
        const { dataSourceName, spEntity } = upsertConnectionReference(config.connectionReferences, {
            ...params,
            dataverseDataSourceNames: getExistingDataverseNames(config.databaseReferences ?? {}),
        });
        await writeRepoConfig(configPath, config);
        trackInfoEvent(logger, 'UpdateConnectionReferences.Commit', {
            apiName: params.apiName,
            dataSourceName,
        });
        return { dataSourceName, spEntity };
    }
    catch (error) {
        trackInfoEvent(logger, 'UpdateConnectionReferences.Failure', {
            apiName: params.apiName,
            error: error instanceof Error ? error.message : String(error),
        });
        throw error;
    }
}
//# sourceMappingURL=AddDataSource.js.map