/*!
 * Copyright (C) Microsoft Corporation. All rights reserved.
 */
import { processDataSourceInfo } from '../CodeGen/dataSourceInfoProcessor.js';
import { generateModelService } from '../CodeGen/modelServiceGenerator.js';
import { readRepoConfig, writeRepoConfig } from '../Config/ConfigManager.js';
import { initializePlayerActions } from '../initializePlayerActions.js';
import { getExistingCdpNames } from '../ReferenceStores/connectionReferences.js';
import { upsertDatabaseReference } from '../ReferenceStores/databaseReferences.js';
import { DataverseMetadataService, getEnvironmentByName, getPlayerServiceConfig, } from '../services/index.js';
import { getVfs } from '../VfsManager.js';
import { ensureDataverseEntityDataSource } from './AddDataSource.js';
/**
 * Maps a CSDL type string to the JSON Schema `type` field used in Swagger parameter and response
 * objects. This is the structural type that tooling (e.g. the Swagger UI, codegen) uses to
 * understand the wire shape — not the Dataverse-specific semantic type.
 *
 * Entity references (`mscrm.*`) map to `object` because they serialize as JSON objects on the
 * wire. Collections of any kind map to `array`. All string-like Edm types (dates, GUIDs, durations)
 * collapse to `string` because they have no richer JSON primitive.
 */
function mapCsdlTypeToJsonSchemaType(csdlType) {
    if (csdlType.startsWith('Collection('))
        return 'array';
    if (csdlType.startsWith('mscrm.'))
        return 'object';
    switch (csdlType) {
        case 'Edm.Boolean':
            return 'boolean';
        case 'Edm.Int16':
        case 'Edm.Int32':
        case 'Edm.Byte':
        case 'Edm.SByte':
        case 'Edm.Int64':
        case 'Edm.Decimal':
        case 'Edm.Double':
        case 'Edm.Single':
            return 'number';
        default:
            return 'string';
    }
}
/**
 * Maps a CSDL type string to the `x-ms-dataverse-type` extension field written into the schema
 * file. This is the Dataverse semantic type used downstream by `modelServiceGenerator` to produce
 * correctly typed TypeScript — for example, `LookupType` signals that the parameter is an entity
 * reference that should resolve to a generated `*Base` interface rather than plain `object`.
 *
 * The `mscrm.*` prefix check must come before the generic `Collection(` check so that
 * `Collection(mscrm.*)` resolves to `LookupCollectionType` rather than the generic `CollectionType`.
 */
function mapCsdlTypeToDataverseType(csdlType) {
    // Check entity collection before generic collection — order matters here
    if (csdlType.startsWith('Collection(mscrm.'))
        return 'LookupCollectionType';
    if (csdlType.startsWith('Collection('))
        return 'CollectionType';
    if (csdlType.startsWith('mscrm.'))
        return 'LookupType';
    switch (csdlType) {
        case 'Edm.String':
        case 'Edm.Duration':
        case 'Edm.TimeOfDay':
            return 'StringType';
        case 'Edm.Guid':
            return 'UniqueidentifierType';
        case 'Edm.Boolean':
            return 'BooleanType';
        case 'Edm.Int16':
        case 'Edm.Int32':
        case 'Edm.Byte':
        case 'Edm.SByte':
            return 'IntegerType';
        case 'Edm.Int64':
            // Int64 is kept separate from Int32 because some runtimes treat 64-bit integers specially
            // (e.g. precision loss in JavaScript). Downstream codegen can choose to map this to bigint.
            return 'BigIntType';
        case 'Edm.Decimal':
            return 'DecimalType';
        case 'Edm.Double':
            return 'DoubleType';
        case 'Edm.Single':
            return 'FloatType';
        case 'Edm.DateTimeOffset':
        case 'Edm.Date':
            return 'DateTimeType';
        case 'Edm.Binary':
            return 'FileType';
        default:
            // Unknown CSDL types are treated as strings — this preserves forward compatibility when
            // new Edm types appear in Dataverse metadata before this mapping is updated.
            return 'StringType';
    }
}
const UNKNOWN_LOGICAL_NAME = 'unknown';
function extractEntityLogicalName(csdlType, options) {
    const collectionMatch = csdlType.match(/^Collection\(mscrm\.(\w+)\)$/);
    if (collectionMatch) {
        return collectionMatch[1];
    }
    if (!options?.collectionOnly) {
        const entityMatch = csdlType.match(/^mscrm\.(\w+)$/);
        if (entityMatch) {
            return entityMatch[1];
        }
    }
    if (csdlType.startsWith('mscrm.') || csdlType.startsWith('Collection(mscrm.')) {
        return UNKNOWN_LOGICAL_NAME;
    }
    return null;
}
/**
 * Extracts the Dataverse entity logical names from the non-binding parameters and return type of a
 * CSDL operation. These are the entities that must have their data sources ensured before codegen
 * runs, so that `buildEntityModelMap` in `modelServiceGenerator` finds schema files for them.
 *
 * The binding parameter (index 0 of bound operations) is excluded — it is handled separately by
 * `addDataverseApiAsync` when resolving the entity set name for the operation path.
 */
function extractEntityLogicalNames(operation) {
    const startIdx = operation.isBound ? 1 : 0;
    const names = new Set();
    for (const param of operation.parameters.slice(startIdx)) {
        const logicalName = extractEntityLogicalName(param.type);
        if (logicalName)
            names.add(logicalName);
    }
    if (operation.returnType) {
        const logicalName = extractEntityLogicalName(operation.returnType.type, {
            collectionOnly: true,
        });
        if (logicalName)
            names.add(logicalName);
    }
    return [...names];
}
/**
 * Converts a parsed `CsdlOperation` (from Dataverse `$metadata` XML) into the `.Schema.json`
 * file format consumed by the codegen pipeline.
 *
 * The output uses the connector schema format (`properties.swagger`) so the existing
 * `handleSwaggerSchema` codegen path processes it without a separate handler. All parameter
 * metadata — `in`, `type`, `x-ms-dataverse-type`, `x-ms-csdl-type` — is resolved at write-time
 * and baked into the swagger. Downstream processors read the swagger spec directly.
 *
 * The `x-ms-csdl-type` extension preserves the original CSDL type string (e.g.
 * `"mscrm.opportunity"`) so that `modelServiceGenerator` can resolve entity references to their
 * generated TypeScript `*Base` interfaces instead of falling back to plain `object`.
 *
 * @param operation   - The parsed CSDL operation (kind, parameters, return type, binding info)
 * @param entitySetName - OData entity set name for the binding entity (e.g. `"opportunities"`),
 *                        or `undefined` for unbound operations
 * @param apiName     - The user-supplied API name, used verbatim as `title` and lowercased as `name`
 */
export function buildDataverseOperationSchema(operation, entitySetName, apiName) {
    const { name, kind, isBound, parameters, returnType } = operation;
    // OData semantics: Functions are read-only (GET), Actions are write/side-effect (POST)
    const method = kind === 'Function' ? 'get' : 'post';
    // Bound operations target a specific entity record via its primary key in the path.
    // Unbound operations are called directly on the service root.
    const operationPath = isBound && entitySetName
        ? `/${entitySetName}({id})/Microsoft.Dynamics.CRM.${name}`
        : `/${name}`;
    const swaggerParams = [];
    if (isBound && entitySetName) {
        // The binding parameter in CSDL describes the entity type, but on the wire it becomes a GUID
        // primary key in the URL path. We replace it with a synthetic `id` path parameter.
        swaggerParams.push({
            name: 'id',
            in: 'path',
            required: true,
            type: 'string',
            'x-ms-dataverse-type': 'UniqueidentifierType',
            'x-ms-csdl-type': 'Edm.Guid',
        });
    }
    // Skip index 0 for bound operations — that is the binding parameter (the entity type), which
    // was already replaced above with the synthetic `id` path parameter.
    // Gate on entitySetName for the same reason as the id-param injection above: if isBound=true
    // but entitySetName is undefined, we have no valid path to emit, so treat it as unbound.
    const startIdx = isBound && entitySetName ? 1 : 0;
    for (const param of parameters.slice(startIdx)) {
        swaggerParams.push({
            name: param.name,
            // OData Functions pass parameters via query string; Actions pass them in the request body
            in: kind === 'Function' ? 'query' : 'body',
            // CSDL `nullable: false` means the parameter is required on the wire
            required: !param.nullable,
            type: mapCsdlTypeToJsonSchemaType(param.type),
            'x-ms-dataverse-type': mapCsdlTypeToDataverseType(param.type),
            // Preserve the raw CSDL type so modelServiceGenerator can resolve entity interface names
            'x-ms-csdl-type': param.type,
        });
    }
    // Operations without a return type yield HTTP 204 No Content.
    // Operations with a return type yield HTTP 200 with a typed response schema.
    const responses = returnType
        ? {
            '200': {
                description: 'Success',
                schema: {
                    type: mapCsdlTypeToJsonSchemaType(returnType.type),
                    'x-ms-dataverse-type': mapCsdlTypeToDataverseType(returnType.type),
                    'x-ms-csdl-type': returnType.type,
                },
            },
        }
        : { '204': { description: 'No Content' } };
    // Aligns to the connector schema format (properties.swagger) so the existing
    // handleSwaggerSchema codegen path processes this without a separate handler.
    // `name` preserves casing (e.g. "WinOpportunity") so extractModelName produces
    // a clean service class name (WinOpportunityService, dataSourceName 'winopportunity').
    const schema = {
        name: apiName,
        title: apiName,
        type: 'Microsoft.PowerApps/dataverseOperation',
        properties: {
            displayName: apiName,
            swagger: {
                swagger: '2.0',
                info: { title: apiName, version: '1.0' },
                basePath: '/api/data/v9.2',
                paths: {
                    [operationPath]: {
                        [method]: {
                            operationId: name,
                            parameters: swaggerParams,
                            responses,
                        },
                    },
                },
            },
        },
    };
    // entitySetName is only present for bound operations; omitting it for unbound keeps the schema
    // minimal and avoids ambiguity in downstream processors that check for its presence
    if (entitySetName !== undefined) {
        schema.entitySetName = entitySetName;
    }
    return schema;
}
/**
 * Adds a Dataverse API (custom action, custom function, SDK message, or first-party
 * action/function) to the app by fetching its definition from the $metadata CSDL,
 * persisting a schema file, and updating dataSourcesInfo.
 *
 * @param {AddDataverseApiContext} context
 * @returns {Promise<void>}
 */
export async function addDataverseApiAsync(context) {
    const { apiName } = context.actionsParams;
    const { schemaPath } = context.localFilePaths;
    const { logger } = context;
    initializePlayerActions({
        vfs: context.vfs,
        authProvider: context.authProvider,
        region: context.region,
        environmentName: context.environmentName,
        logger,
    });
    const envMetadata = await getEnvironmentByName(context.environmentName);
    const orgUrl = envMetadata?.properties?.linkedEnvironmentMetadata?.instanceApiUrl;
    if (!orgUrl) {
        throw new Error('Unable to determine the Dataverse organization URL. Ensure the environment has a linked Dataverse instance.');
    }
    const { httpClient } = getPlayerServiceConfig();
    const metadataService = new DataverseMetadataService(httpClient);
    // searchCsdlOperations does a case-insensitive substring match; filter to exact name
    const allMatches = await metadataService.searchCsdlOperations(orgUrl, apiName, logger);
    const exactMatches = allMatches.filter((op) => op.name === apiName);
    if (exactMatches.length === 0) {
        throw new Error(`No Dataverse operation named '${apiName}' was found. Use 'find-dataverse-api' to search for available operations.`);
    }
    if (exactMatches.length > 1) {
        throw new Error(`Multiple Dataverse operations named '${apiName}' were found. This is unexpected — operation names should be unique.`);
    }
    const operation = exactMatches[0];
    // For bound operations, resolve the entity set name and display name from the binding parameter type
    let entitySetName;
    let bindingEntityInfo;
    if (operation.isBound && operation.parameters.length > 0) {
        const bindingType = operation.parameters[0].type; // e.g. "mscrm.account"
        const entityLogicalName = bindingType.replace(/^mscrm\./, '');
        const entityDef = await metadataService.getEntityDefinitionAsync(orgUrl, entityLogicalName, false, logger);
        entitySetName = entityDef.EntitySetName;
        if (!entitySetName) {
            throw new Error(`Could not resolve entity set name for binding type '${bindingType}' (entity '${entityLogicalName}').`);
        }
        bindingEntityInfo = {
            entitySetName,
            logicalName: entityLogicalName,
            displayName: entityDef.DisplayName?.UserLocalizedLabel?.Label ?? entityLogicalName,
        };
    }
    // Ensure each entity type referenced in non-binding params and return type has an entity data
    // source (schema file + config entry). This must happen before generateModelService so that
    // buildEntityModelMap finds the schema files and resolves the correct *Base interface names.
    const entitySchemaFilePaths = [];
    for (const logicalName of extractEntityLogicalNames(operation)) {
        if (logicalName === UNKNOWN_LOGICAL_NAME) {
            continue;
        }
        const entitySchemaFilePath = await ensureDataverseEntityDataSource({
            orgUrl,
            logicalName,
            localFilePaths: context.localFilePaths,
            metadataService,
            logger,
        });
        if (entitySchemaFilePath) {
            entitySchemaFilePaths.push(entitySchemaFilePath);
        }
    }
    // Write schema file to {schemaPath}/dataverse/{apiName}.Schema.json
    const schemaData = buildDataverseOperationSchema(operation, entitySetName, apiName);
    const vfs = getVfs();
    const dirPath = vfs.join(schemaPath, 'dataverse');
    const filePath = vfs.join(dirPath, `${apiName}.Schema.json`);
    await vfs.mkdir(dirPath, { recursive: true });
    await vfs.writeFile(filePath, JSON.stringify(schemaData, null, 2), { encoding: 'utf-8' });
    // Update power.config.json: ensure default.cds exists (for org URL resolution at runtime),
    // and for bound operations add the binding entity to dataSources if not already present.
    await updatePowerConfigForDataverseApi(context.localFilePaths.powerConfigPath, bindingEntityInfo);
    // Regenerate dataSourcesInfo.ts from all schema files (idempotent)
    await processDataSourceInfo(schemaPath, logger);
    // Generate TypeScript model and service for all new schema files in parallel.
    await Promise.all([...entitySchemaFilePaths, filePath].map((schemaFilePath) => generateModelService({
        schemaFolderPath: schemaPath,
        schemaFilePath,
        codeGenPath: context.localFilePaths.codeGenPath,
        logger,
    })));
}
/**
 * Updates `power.config.json` for a Dataverse API add:
 * - Ensures `databaseReferences["default.cds"]` exists (required for org URL resolution at runtime)
 * - For bound operations: adds the binding entity to `dataSources` if not already present.
 *   Deduplicates by `logicalName` so that adding two operations bound to the same entity
 *   does not create a second entry.
 * - For unbound operations: only the `default.cds` key is guaranteed to exist; no `dataSources`
 *   entry is written because unbound operations are not associated with any entity table.
 */
async function updatePowerConfigForDataverseApi(configPath, entityInfo) {
    const config = await readRepoConfig(configPath);
    if (!config.databaseReferences) {
        config.databaseReferences = {};
    }
    // Ensure the default.cds key always exists
    if (!config.databaseReferences['default.cds']) {
        config.databaseReferences['default.cds'] = {
            dataSources: {},
            environmentVariableName: '',
        };
    }
    // Always ensure the canonical environment data source entry is present.
    // environment_39a902ba is the fixed logical name for the current-environment CDS data source
    // (CurrentEnvironmentLogicalName in Power Apps runtime). The host uses it to anchor unbound
    // Dataverse operations and returns a runtimeUrl for it via getAppCdsDataSourceConfigsAsync,
    // which the executor uses to resolve the org API URL at runtime.
    const ENV_DS_KEY = 'environment_39a902ba';
    if (!config.databaseReferences['default.cds'].dataSources[ENV_DS_KEY]) {
        config.databaseReferences['default.cds'].dataSources[ENV_DS_KEY] = {
            entitySetName: 'Environment',
            logicalName: 'environment_39a902ba',
            isHidden: false,
        };
    }
    if (entityInfo) {
        const existingDataSources = config.databaseReferences['default.cds'].dataSources ?? {};
        // Only add the entity if it is not already tracked (dedup by logicalName)
        const alreadyPresent = Object.values(existingDataSources).some((ds) => ds.logicalName === entityInfo.logicalName);
        if (!alreadyPresent) {
            upsertDatabaseReference(config.databaseReferences, {
                entitySetName: entityInfo.entitySetName,
                logicalName: entityInfo.logicalName,
                displayName: entityInfo.displayName,
                cdpDataSourceNames: getExistingCdpNames(config.connectionReferences ?? {}),
            });
        }
    }
    await writeRepoConfig(configPath, config);
}
//# sourceMappingURL=AddDataverseApi.js.map