/*!
 * Copyright (C) Microsoft Corporation. All rights reserved.
 */
import { DataSources } from '../Config/Config.types.js';
import { readRepoConfig } from '../Config/ConfigManager.js';
import { initializePlayerActions } from '../initializePlayerActions.js';
import { getConnectionByNameAsync, getFlowMetadataAsync, listConnectionsForConnectorAsync, } from '../services/index.js';
import { getVfs, setVfs } from '../VfsManager.js';
import { addDataSourceAsync } from './AddDataSource.js';
import { addFlowAsync } from './AddFlow.js';
import { removeFlowAsync } from './RemoveFlow.js';
/**
 * Refreshes one or all data sources by re-adding them, which regenerates models, services, and schemas.
 * Reads the power.config.json to determine which data sources exist.
 * If `config.actionsParams.dataSourceName` is specified, only that data source is refreshed.
 * Failures for individual data sources are collected and returned rather than aborting the entire operation.
 * @param config - Context including VFS, auth, file paths, and optional data source name filter.
 * @returns A result object with `succeeded` and `failed` arrays.
 */
export async function refreshDataSourceAsync(config) {
    setVfs(config.vfs);
    const vfs = getVfs();
    const { powerConfigPath } = config.localFilePaths;
    if (!(await vfs.exists(powerConfigPath))) {
        throw new Error(`Configuration file not found at ${powerConfigPath}`);
    }
    // Initialize services so listConnectionsForConnectorAsync can be used during collection
    initializePlayerActions({
        vfs: config.vfs,
        authProvider: config.authProvider,
        region: config.region,
        environmentName: config.environmentName,
        logger: config.logger,
    });
    const repoConfig = await readRepoConfig(powerConfigPath);
    const targetDataSourceName = config.actionsParams.dataSourceName;
    const { logger } = config;
    const toRefresh = [];
    // Collect Dataverse data sources from databaseReferences
    if (repoConfig.databaseReferences) {
        for (const dbRef of Object.values(repoConfig.databaseReferences)) {
            for (const [dsName, dsInfo] of Object.entries(dbRef.dataSources)) {
                if (!targetDataSourceName || dsName === targetDataSourceName) {
                    toRefresh.push({
                        dsName,
                        addParams: {
                            apiId: DataSources.Dataverse,
                            tableName: dsInfo.logicalName,
                        },
                    });
                }
            }
        }
    }
    // Collect connector data sources from connectionReferences
    if (repoConfig.connectionReferences) {
        for (const [, connRef] of Object.entries(repoConfig.connectionReferences)) {
            // The map key is a random UUID used internally — it must NOT be used as apiId.
            //
            // connRef.id stores the full connector path returned by the API, e.g.
            // "/providers/Microsoft.PowerApps/apis/shared_sharepointonline".
            // getConnectorAsync expects only the short name ("shared_sharepointonline"), so we
            // extract the last path segment. If connRef.id is already a short name (no slashes)
            // it is returned unchanged.
            //
            // Connection identity is recovered in priority order:
            //  1. xrmConnectionReferenceLogicalName — set when originally added via --connection-ref.
            //     Pass it as connectionRef so addCdpDataSourceAsync resolves the connection ID from
            //     Dataverse the same way it did originally.
            //  2. sharedConnectionId — set for shareable connectors added via --connection-id.
            //     Stored as the full resource path (e.g. "/providers/.../connections/abc123");
            //     extract the last segment since getConnectionByNameAsync expects just the name.
            //  3. Neither stored → look up a matching connection via the API. This handles SSO
            //     connectors (SPO, Teams, Office 365) which are not shareable and have no
            //     connection reference logical name.
            const apiId = connRef.id.split('/').pop() ?? connRef.id;
            const connectionRef = connRef.xrmConnectionReferenceLogicalName ?? undefined;
            const rawSharedConnId = connRef.sharedConnectionId ?? undefined;
            let connectionId = connectionRef
                ? undefined
                : (rawSharedConnId?.split('/').pop() ?? undefined);
            if (!connectionRef && !connectionId) {
                connectionId = await findConnectionIdForRefAsync(connRef.id, connRef.authenticationType ?? undefined, logger);
            }
            if (connRef.dataSets && Object.keys(connRef.dataSets).length > 0) {
                // Tabular connector: each (dataset, dsName) pair is a separate data source
                for (const [dataset, dataSet] of Object.entries(connRef.dataSets)) {
                    for (const [dsName, dsInfo] of Object.entries(dataSet.dataSources)) {
                        if (!targetDataSourceName || dsName === targetDataSourceName) {
                            if (dsInfo.tableName === undefined && apiId === 'shared_sql') {
                                // SQL entries with no tableName are stored procedures; recover the procedure
                                // name from the schema file written by the original add-data-source call.
                                const procName = await tryReadStoredProcedureNameAsync(vfs, config.localFilePaths.schemaPath, dsName);
                                toRefresh.push({
                                    dsName,
                                    addParams: {
                                        apiId,
                                        connectionId,
                                        connectionRef,
                                        dataset,
                                        ...(procName !== undefined
                                            ? { sqlStoredProcedure: procName }
                                            : { tableName: dsName }),
                                    },
                                });
                            }
                            else {
                                toRefresh.push({
                                    dsName,
                                    addParams: {
                                        apiId,
                                        connectionId,
                                        connectionRef,
                                        dataset,
                                        tableName: dsInfo.tableName ?? dsName,
                                    },
                                });
                            }
                        }
                    }
                }
            }
            // Non-tabular data sources: entries in dataSources not covered by any dataSets entry.
            // For pure non-tabular connectors (no dataSets) this is all of dataSources. For connectors
            // that support both tabular and non-tabular, this handles the non-tabular remainder.
            const tabularDsNames = new Set(Object.values(connRef.dataSets ?? {}).flatMap((ds) => Object.keys(ds.dataSources)));
            const nonTabularSources = (connRef.dataSources ?? []).filter((ds) => !tabularDsNames.has(ds));
            if (nonTabularSources.length > 0) {
                // One addDataSource call per connection reference refreshes all its non-tabular data sources
                const isMatch = !targetDataSourceName || nonTabularSources.some((ds) => ds === targetDataSourceName);
                if (isMatch) {
                    toRefresh.push({
                        dsName: nonTabularSources[0],
                        addParams: {
                            apiId,
                            connectionId,
                            connectionRef,
                            workflowName: connRef.workflowDetails?.workflowName,
                        },
                    });
                }
            }
        }
    }
    if (targetDataSourceName && toRefresh.length === 0) {
        throw new Error(`Data source '${targetDataSourceName}' not found in power.config.json`);
    }
    const succeeded = [];
    const failed = [];
    for (const entry of toRefresh) {
        try {
            // For flows, we need to remove and re-add to refresh the swagger and trigger model regeneration. For other connectors, re-adding is sufficient.
            if (entry.addParams.workflowName) {
                await refreshFlowAsync(entry.addParams.workflowName, config);
            }
            else {
                const addConfig = {
                    vfs: config.vfs,
                    authProvider: config.authProvider,
                    region: config.region,
                    environmentName: config.environmentName,
                    localFilePaths: config.localFilePaths,
                    logger: config.logger,
                    actionsParams: {
                        apiId: entry.addParams.apiId,
                        connectionId: entry.addParams.connectionId,
                        connectionRef: entry.addParams.connectionRef,
                        tableName: entry.addParams.tableName,
                        dataset: entry.addParams.dataset,
                        sqlStoredProcedure: entry.addParams.sqlStoredProcedure,
                    },
                };
                await addDataSourceAsync(addConfig);
            }
            succeeded.push(entry.dsName);
        }
        catch (err) {
            failed.push({
                dataSourceName: entry.dsName,
                error: err instanceof Error ? err.message : String(err),
            });
        }
    }
    return { succeeded, failed };
}
/**
 * Refreshes a single flow by verifying access to all dependent connections, removing, and re-adding it.
 * Access is checked before any removal is attempted so we don't remove a flow we can't re-add.
 */
async function refreshFlowAsync(workflowName, config) {
    const flowMetadata = await getFlowMetadataAsync(workflowName, config.logger);
    // Check access to all invoker connections before making any destructive changes.
    const invokerConnectionReferences = Object.fromEntries(Object.entries(flowMetadata.properties.connectionReferences).filter(([, connRef]) => connRef.source.toLowerCase() === 'invoker'));
    await verifyConnectionAccessAsync(invokerConnectionReferences, config.logger);
    const flowContext = {
        vfs: config.vfs,
        authProvider: config.authProvider,
        region: config.region,
        environmentName: config.environmentName,
        localFilePaths: config.localFilePaths,
        logger: config.logger,
        actionsParams: { flowId: workflowName },
    };
    await removeFlowAsync(flowContext);
    const addResult = await addFlowAsync(flowContext);
    if (!addResult.success) {
        throw new Error(addResult.error ?? 'addFlowAsync failed');
    }
}
/**
 * Verifies that all invoker connection references are accessible.
 * Mirrors the access check in AddFlow's fetchEnrichedDependenciesAsync.
 * Throws if any connection cannot be retrieved.
 */
async function verifyConnectionAccessAsync(connectionReferences, logger) {
    await Promise.all(Object.values(connectionReferences).map(async (connRef) => {
        const connector = connRef.apiDefinition;
        try {
            const connection = await getConnectionByNameAsync(connRef.connectionName, connector.name, logger);
            if (!connection) {
                throw new Error();
            }
        }
        catch {
            throw new Error(`Unable to retrieve connection '${connRef.id}'. You may not have access to the underlying connections.`);
        }
    }));
}
/**
 * Finds a connection ID for a connector reference by listing connections and selecting the best match.
 * Used as a fallback for SSO connectors (e.g., SharePoint, Teams, Office 365) that are not shareable
 * and have no stored sharedConnectionId or xrmConnectionReferenceLogicalName.
 *
 * Matching criteria (mirrors the PowerApps client's ConnectionOperations logic):
 *  - Skips on-premises gateway connections (no stored gateway info to match against).
 *  - If `authenticationType` is specified, only considers connections with a matching
 *    `connectionParametersSet.name`.
 * @returns The connection name (used as connection ID) of the first match, or undefined if none found.
 */
async function findConnectionIdForRefAsync(connectorFullId, authenticationType, logger) {
    const connectorId = connectorFullId.split('/').pop() ?? connectorFullId;
    let connections;
    try {
        connections = await listConnectionsForConnectorAsync(connectorId, logger);
    }
    catch {
        return undefined;
    }
    const match = connections.find((conn) => {
        // Skip on-premises gateway connections; we have no stored gateway info to match against
        if (conn.properties.gatewayObject) {
            return false;
        }
        // Match authenticationType if the stored connection reference specifies one
        if (authenticationType) {
            return conn.properties.connectionParametersSet?.name === authenticationType;
        }
        return true;
    });
    return match?.name;
}
/**
 * Recovers the SQL stored procedure name from its saved schema file.
 *
 * SQL stored procedures are stored as `{}` in `dataSets.dataSources` — the procedure name
 * is not persisted in the config. `addDataSourceAsync` always writes a schema file at:
 *   `schemaPath/sql/{dsName}/<friendlyName>.Schema.json`
 * where `dsName` is the key in `dataSets.dataSources`. The schema JSON contains a top-level
 * `name` field with the original SQL procedure name (e.g. `"[dbo].[GetContacts]"`), which is
 * what `addDataSourceAsync` needs as `sqlStoredProcedure` to re-fetch the metadata.
 *
 * @returns The stored procedure name, or `undefined` if the schema file cannot be found/parsed.
 */
async function tryReadStoredProcedureNameAsync(vfs, schemaPath, dsName) {
    try {
        const procDir = vfs.join(schemaPath, 'sql', dsName);
        const files = await vfs.readdir(procDir);
        const schemaFileName = files.find((f) => f.endsWith('.Schema.json'));
        if (!schemaFileName) {
            return undefined;
        }
        const content = await vfs.readFile(vfs.join(procDir, schemaFileName), 'utf-8');
        const parsed = JSON.parse(content);
        if (parsed &&
            typeof parsed === 'object' &&
            'name' in parsed &&
            typeof parsed.name === 'string') {
            return parsed.name;
        }
    }
    catch {
        // Schema file missing or unreadable — fall through
    }
    return undefined;
}
//# sourceMappingURL=RefreshDataSource.js.map