"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateDetectModelDeltaFunction = generateDetectModelDeltaFunction;
const generator_1 = require("@postxl/generator");
const utils_1 = require("@postxl/utils");
function generateDetectModelDeltaFunction(context, model) {
    const imports = generator_1.ImportGenerator
        //
        .from(model.import.delta.detect.location)
        .addType(model.types)
        .addType(model.types.id)
        .add(context.import.detectDelta._utils.getDelta)
        .add(context.import.detectDelta._utils.keepErrors)
        .add(context.import.detectDelta._utils.detectModelDelta);
    const errorType = prepareErrorType({ context, model, imports });
    const detectDeltaFunction = prepareDetectDeltaFunction({ model, context, imports });
    return /* ts */ `
${imports.generate()}

${errorType}

${detectDeltaFunction}`;
}
function prepareErrorType({ context, model, imports, }) {
    const modelErrors = {
        missingFields: [],
        invalidReferenceFieldErrors: [],
        nonUniqueFieldErrors: [],
        invalidTypeFieldErrors: [],
        isRequiredDependencyErrors: [],
    };
    // Prepare field specific error types
    for (const field of model.fields.values()) {
        prepareFieldErrors({ context, model, field, modelErrors, imports });
    }
    prepareRelationErrors({ context, model, modelErrors, imports });
    const missingFieldError = modelErrors.missingFields.length > 0
        ? [
            `${context.import.detectDelta.types.generic.errors.missingField.type.name}<${modelErrors.missingFields.join(' | ')}> `,
        ]
        : [];
    if (modelErrors.missingFields.length > 0) {
        imports.addType(context.import.detectDelta.types.generic.errors.missingField.type);
    }
    if (modelErrors.invalidReferenceFieldErrors.length > 0) {
        imports.addType(context.import.detectDelta.types.generic.errors.invalidReference.type);
    }
    if (modelErrors.nonUniqueFieldErrors.length > 0) {
        imports.addType(context.import.detectDelta.types.generic.errors.nonUnique.type);
    }
    if (modelErrors.invalidTypeFieldErrors.length > 0) {
        imports.addType(context.import.detectDelta.types.generic.errors.invalidType.type);
    }
    if (modelErrors.isRequiredDependencyErrors.length > 0) {
        imports.addType(context.import.detectDelta.types.generic.errors.isRequiredDependency.type);
    }
    const modelErrorTypes = [
        ...missingFieldError,
        ...modelErrors.invalidReferenceFieldErrors,
        ...modelErrors.nonUniqueFieldErrors,
        ...modelErrors.invalidTypeFieldErrors,
        ...modelErrors.isRequiredDependencyErrors,
    ];
    if (modelErrorTypes.length === 0) {
        modelErrorTypes.push(`never`);
    }
    return `export type ${model.import.delta.errors.name} = ${modelErrorTypes.join('\n| ')}`;
}
function prepareFieldErrors({ context, model, field, modelErrors, imports, }) {
    switch (field.kind) {
        case 'id':
        case 'scalar':
        case 'relation':
        case 'enum':
            return prepareModelFieldDatabaseNative({ context, model, field, modelErrors, imports });
        case 'discriminatedUnion':
            return prepareModelFieldDiscriminatedUnion({ context, model, field, modelErrors, imports });
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
}
function prepareModelFieldDatabaseNative({ context, field, modelErrors, imports, }) {
    if (field.kind === 'id') {
        return;
    }
    if (field.isRequired) {
        modelErrors.missingFields.push(`"${field.name}"`);
    }
    const errors = context.import.detectDelta.types.generic.errors;
    if (field.kind === 'relation') {
        const referencedModel = context.models.get(field.referencedModelName);
        imports.addType(referencedModel.types.id);
        modelErrors.invalidReferenceFieldErrors.push(`
        ${errors.invalidReference.type.name}<"${referencedModel.types.name}", "${field.name}", ${referencedModel.types.id.name}>
      `);
    }
    if (field.isUnique) {
        if (field.isRequired) {
            modelErrors.nonUniqueFieldErrors.push(`${errors.nonUnique.type.name}<"${field.name}", ${field.type}>`);
        }
        else {
            modelErrors.nonUniqueFieldErrors.push(`${errors.nonUnique.type.name}<"${field.name}", ${field.type} | null>`);
        }
    }
    if (field.kind == 'scalar' && field.databaseType === 'Int') {
        modelErrors.invalidTypeFieldErrors.push(`
        ${errors.invalidType.type.name}<"${field.name}", "${field.databaseType}", ${field.type}>
      `);
    }
}
function prepareModelFieldDiscriminatedUnion({ context, model, field, modelErrors, imports, }) {
    for (const f of field.commonFields.values()) {
        prepareModelFieldDatabaseNative({ context, model, field: f, modelErrors, imports });
    }
    for (const member of field.members.values()) {
        for (const f of member.fields.values()) {
            prepareModelFieldDatabaseNative({ context, model, field: f, modelErrors, imports });
        }
    }
}
function prepareRelationErrors({ context, model, imports, modelErrors, }) {
    for (const relatedModel of context.models.values()) {
        if (relatedModel.name === model.name) {
            continue;
        }
        for (const field of relatedModel.fields.values()) {
            prepareRelationErrorsForRelatedModel({ context, field, model, relatedModel, imports, modelErrors });
        }
    }
}
function prepareRelationErrorsForRelatedModel({ context, model, relatedModel, field, imports, modelErrors, }) {
    switch (field.kind) {
        case 'id':
        case 'scalar':
        case 'enum':
            return;
        case 'relation': {
            if (field.referencedModelName === model.name && field.isRequired) {
                imports.addType(relatedModel.types).addType(relatedModel.types.id);
                modelErrors.isRequiredDependencyErrors.push(`
          ${context.import.detectDelta.types.generic.errors.isRequiredDependency.type.name}<
              '${relatedModel.name}',
              '${field.name}',
              ${relatedModel.types.name},
              ${relatedModel.types.id.name}
            >
          `);
            }
            return;
        }
        case 'discriminatedUnion': {
            for (const f of field.commonFields.values()) {
                prepareRelationErrorsForRelatedModel({ context, model, relatedModel, field: f, imports, modelErrors });
            }
            for (const member of field.members.values()) {
                for (const f of member.fields.values()) {
                    prepareRelationErrorsForRelatedModel({ context, model, relatedModel, field: f, imports, modelErrors });
                }
            }
            return;
        }
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
}
function prepareDetectDeltaFunction({ model, context, imports, }) {
    if (model.repository.kind === 'NoRepository') {
        return '';
    }
    const generic = context.import.detectDelta.types.generic;
    imports
        .addType(model.import.delta.errors)
        .addType(context.import.detectDelta.types.generic.modelType)
        .addType(context.import.detectDelta.allModels.idMapType)
        .add(context.repositories.dataService);
    const returnType = `${generic.modelType.name}<${model.types.name}, ${model.types.id.name}, ${model.import.delta.errors.name}>[]`;
    const blocks = {
        sharedValidations: [],
        requiredFields: [],
        fieldNames: [],
        deleteValidations: [],
    };
    for (const field of model.fields.values()) {
        handleField({ field, blocks, model, context, imports });
    }
    for (const relatedModel of context.models.values()) {
        checkRelatedModel({ relatedModel, blocks, model, context, imports });
    }
    if (blocks.requiredFields.length > 0) {
        imports.addType(generic.errors.missingField.type).add(context.import.detectDelta._utils.validation.requiredFields);
    }
    const requiredFieldsValidation = blocks.requiredFields.length > 0
        ? `...${context.import.detectDelta._utils.validation.requiredFields.name}({item, fieldNames: [${blocks.requiredFields.join(',')}]}) as ${model.import.delta.errors.name}[],`
        : '';
    const sharedValidationsFunction = `(${blocks.sharedValidations.length === 0 ? '_' : ''}item: ${model.name}) => ${(0, generator_1.promises)(blocks.sharedValidations)}`;
    const usesIdMap = sharedValidationsFunction.includes('idMap');
    const validateDelete = blocks.deleteValidations.length > 0
        ? `async ({ existingItem }) => ${context.import.detectDelta._utils.keepErrors.name}([${blocks.deleteValidations.join(',\n')}])`
        : 'async () => Promise.resolve([]) // NOSONAR - keep all validations async to adhere to the same interface';
    return `
/**
 * Detects changes for a ${model.userFriendlyName} item and returns the result as a ${generic.modelType.name} object.
 */
export async function detect${model.internalSingularNameCapitalized}Delta(
  data: ${context.repositories.dataService.name},
  items: ${model.types.name}[],
  ${usesIdMap ? 'idMap' : '_idMap'}: ${context.import.detectDelta.allModels.idMapType.name}
): Promise<${returnType}> {
  const sharedValidations: (item: ${model.name}) => Promise<(${model.import.delta.errors.name} | undefined)[]> = 

    ${sharedValidationsFunction}

    return ${context.import.detectDelta._utils.detectModelDelta.name}({
      items,
      getId: (id: ${model.types.id.name}) => data.${model.repository.dataServiceVariable}.get(id),
      getDelta: ({ item, existingItem }: { item: ${model.types.name}; existingItem: ${model.types.name} }) =>
        ${context.import.detectDelta._utils.getDelta.name}({
          item,
          existingItem,
          properties: [${blocks.fieldNames.join(',')}],
        }),
      validate: {
        create: async ({ newItem: item }): Promise<${model.import.delta.errors.name}[]> =>
          ${context.import.detectDelta._utils.keepErrors.name}([
            ${requiredFieldsValidation}
            ...(await sharedValidations(item)),
          ]),
        update: async ({ updatedItem: item }): Promise<${model.import.delta.errors.name}[]> =>
          ${context.import.detectDelta._utils.keepErrors.name}(await sharedValidations(item)),
        delete: ${validateDelete}
      }
    })
}`;
}
function handleField({ field, blocks: { sharedValidations, fieldNames, requiredFields }, model, context, imports, }) {
    if (field.isReadonly) {
        return;
    }
    switch (field.kind) {
        case 'id':
            return;
        case 'scalar':
            handleScalarField({ field, model, context, imports, sharedValidations });
            break;
        case 'relation':
            handleRelationField({ field, model, context, imports, sharedValidations });
            break;
        case 'enum':
            break;
        case 'discriminatedUnion':
            break;
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
    const fieldName = field.name;
    fieldNames.push(`'${fieldName}'`);
    if (field.isRequired) {
        requiredFields.push(`'${fieldName}'`);
    }
}
function handleScalarField({ field, model, context, imports, sharedValidations, }) {
    if (field.isCreatedAt || field.isUpdatedAt) {
        return;
    }
    if (field.isUnique && model.repository.kind !== 'NoRepository') {
        imports.addType(context.import.detectDelta.types.generic.errors.nonUnique.type);
        imports.add(context.import.detectDelta._utils.validation.uniqueField);
        const getByName = `getBy${(0, utils_1.toPascalCase)(field.name)}`;
        sharedValidations.push(`${context.import.detectDelta._utils.validation.uniqueField.name}({
        item,
        fieldName: '${field.name}',
        getExistingItem: (value) => data.${model.repository.dataServiceVariable}.${getByName}(value)
      })`);
    }
    if (field.databaseType === 'Int') {
        imports.addType(context.import.detectDelta.types.generic.errors.invalidType.type);
        imports.add(context.import.detectDelta._utils.validation.int);
        sharedValidations.push(`${context.import.detectDelta._utils.validation.int.name}({item, fieldName: '${field.name}'})`);
    }
}
function handleRelationField({ field, context, imports, sharedValidations, }) {
    const relatedModel = context.models.get(field.referencedModelName);
    if (relatedModel.repository.kind === 'NoRepository') {
        return;
    }
    imports.addType(relatedModel.types.id);
    imports.add(context.import.detectDelta._utils.validation.referenceField);
    sharedValidations.push(` ${context.import.detectDelta._utils.validation.referenceField.name}({
         item,
         fieldName: '${field.name}',
         relatedModel: '${field.referencedModelName}',
         getExistingItem: async (id: ${relatedModel.types.id.name}) => (await data.${relatedModel.repository.dataServiceVariable}.get(id)) ?? idMap.${relatedModel.decoder.dataModelEntry}.get(id) ?? null,
       })`);
}
function checkRelatedModel({ relatedModel, blocks: { deleteValidations }, model, context, imports, }) {
    if (relatedModel.name === model.name) {
        return;
    }
    if (relatedModel.repository.kind === 'NoRepository') {
        return;
    }
    for (const relatedField of relatedModel.fields.values()) {
        if (relatedField.kind !== 'relation' ||
            relatedField.referencedModelName !== model.name ||
            !relatedField.isRequired) {
            continue;
        }
        imports.add(context.import.detectDelta._utils.validation.requiredDependency);
        const relatedFieldName = (0, utils_1.toPascalCase)(relatedField.name);
        deleteValidations.push(`
      await validateRequiredDependency({
        item: existingItem,
        relatedModel: '${relatedModel.name}',
        relatedField: '${relatedField.name}',
        getReferencedItems: async (id: ${model.types.id.name}) => [
          ...(await data.${relatedModel.repository.dataServiceVariable}.getItemsFor${relatedFieldName}(id)).values()
        ]
      })`);
    }
}
//# sourceMappingURL=model-function.generator.js.map