"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validatePrismaApplication = validatePrismaApplication;
const utils_1 = require("@autobe/utils");
const tstl_1 = require("tstl");
const MapUtil_1 = require("../utils/MapUtil");
function validatePrismaApplication(application) {
    const dict = new Map(application.files
        .map((file, fi) => file.models.map((model, mi) => [
        model.name,
        {
            file,
            model,
            fileIndex: fi,
            modelIndex: mi,
        },
    ]))
        .flat());
    const errors = [
        ...validateDuplicatedFiles(application),
        ...validateDuplicatedModels(application),
        ...application.files
            .map((file, fi) => file.models.map((model, mi) => {
            const accessor = `application.files[${fi}].models[${mi}]`;
            return [
                ...validateDuplicatedFields(dict, model, accessor),
                ...validateDuplicatedIndexes(model, accessor),
                ...validateIndexes(model, accessor),
                ...validateReferences(model, accessor, dict),
            ];
        }))
            .flat(2),
    ];
    return errors.length === 0
        ? {
            success: true,
            data: application,
        }
        : {
            success: false,
            data: application,
            errors,
        };
}
/* -----------------------------------------------------------
  DUPLICATES
----------------------------------------------------------- */
function validateDuplicatedFiles(app) {
    const group = new Map();
    app.files.forEach((file, fileIndex) => {
        const container = { file, index: fileIndex };
        MapUtil_1.MapUtil.take(group, file.filename, () => []).push(container);
    });
    const errors = [];
    for (const array of group.values())
        if (array.length !== 1)
            array.forEach((container, i) => {
                errors.push({
                    path: `application.files[${container.index}]`,
                    table: null,
                    field: null,
                    message: [
                        `File ${container.file.filename} is duplicated.`,
                        "",
                        "Accessors of the other duplicated files are:",
                        "",
                        ...array
                            .filter((_oppo, j) => i !== j)
                            .map((oppo) => `- application.files[${oppo.index}]`),
                    ].join("\n"),
                });
            });
    return errors;
}
function validateDuplicatedModels(app) {
    const modelContainers = new Map();
    app.files.forEach((file, fileIndex) => {
        file.models.forEach((model, modelIndex) => {
            const container = {
                file,
                model,
                fileIndex,
                modelIndex,
            };
            MapUtil_1.MapUtil.take(modelContainers, model.name, () => []).push(container);
        });
    });
    const errors = [];
    for (const array of modelContainers.values())
        if (array.length !== 1)
            array.forEach((container, i) => {
                errors.push({
                    path: `application.files[${container.fileIndex}].models[${container.modelIndex}]`,
                    table: container.model.name,
                    field: null,
                    message: [
                        `Model ${container.model.name} is duplicated.`,
                        "",
                        "Accessors of the other duplicated models are:",
                        "",
                        ...array
                            .filter((_oppo, j) => i !== j)
                            .map((oppo) => `- application.files[${oppo.fileIndex}].models[${oppo.modelIndex}]`),
                    ].join("\n"),
                });
            });
    return errors;
}
function validateDuplicatedFields(dict, model, accessor) {
    const errors = [];
    // FIND DUPLICATED FIELDS
    const group = new Map();
    MapUtil_1.MapUtil.take(group, model.primaryField.name, () => []).push(`${accessor}.primaryField.name`);
    model.foreignFields.forEach((field, i) => MapUtil_1.MapUtil.take(group, field.name, () => []).push(`${accessor}.foreignFields[${i}].name`));
    model.plainFields.forEach((field, i) => MapUtil_1.MapUtil.take(group, field.name, () => []).push(`${accessor}.plainFields[${i}].name`));
    for (const [field, array] of group)
        if (array.length !== 1)
            array.forEach((path, i) => {
                errors.push({
                    path,
                    table: model.name,
                    field,
                    message: [
                        `Field ${field} is duplicated.`,
                        "",
                        "Accessors of the other duplicated fields are:",
                        "",
                        ...array.filter((_oppo, j) => i !== j).map((a) => `- ${a}`),
                    ].join("\n"),
                });
            });
    // FIND TABLE NAMED FIELDS
    model.plainFields.forEach((field, i) => {
        if (dict.has(field.name) === true)
            errors.push({
                path: `${accessor}.plainFields[${i}].name`,
                table: model.name,
                field: field.name,
                message: utils_1.StringUtil.trim `
          There's a same named table in the application.

          Check whether the field has been designed for denormalization
          like pre-calculation. If do so, remove the field.

          Otherwise, change the field name to something else.
        `,
            });
    });
    return errors;
}
function validateDuplicatedIndexes(model, accessor) {
    const errors = [];
    // FIND DUPLICATED INDEXES
    const group = new tstl_1.HashMap((x) => (0, tstl_1.hash)(...x), (x, y) => x.length === y.length && x.every((v, i) => v === y[i]));
    model.uniqueIndexes.forEach((unique, i) => group
        .take(unique.fieldNames, () => [])
        .push(`${accessor}.uniqueIndexes[${i}].fieldNames`));
    model.plainIndexes.forEach((plain, i) => group
        .take(plain.fieldNames, () => [])
        .push(`${accessor}.plainIndexes[${i}].fieldNames`));
    model.ginIndexes.forEach((gin, i) => group
        .take([gin.fieldName], () => [])
        .push(`${accessor}.ginIndexes[${i}].fieldName`));
    for (const { first: fieldNames, second: array } of group)
        if (array.length !== 1)
            array.forEach((path, i) => {
                errors.push({
                    path,
                    table: model.name,
                    field: null,
                    message: [
                        `Duplicated index found (${fieldNames.join(", ")}).`,
                        "",
                        "Accessors of the other duplicated indexes are:",
                        "",
                        ...array.filter((_oppo, j) => i !== j).map((a) => `- ${a}`),
                    ].join("\n"),
                });
            });
    // SUBSET RELATIONS
    model.uniqueIndexes.forEach((unique, i) => {
        if (unique.fieldNames.length <= 1)
            return;
        unique.fieldNames.forEach((_, j, array) => {
            if (j === array.length - 1)
                return;
            const subset = unique.fieldNames.slice(0, j);
            if (j === 0 &&
                model.foreignFields.some((f) => f.name === subset[0] && f.unique === true))
                errors.push({
                    path: `${accessor}.uniqueIndexes[${i}].fieldNames[0]`,
                    table: model.name,
                    field: null,
                    message: utils_1.StringUtil.trim `
            Duplicated unique index found (${subset[0]}).
            
            You have defined an unique index that is already included,
            in the foreign field with unique constraint.
            
            Remove this unique index to avoid the duplication.
          `,
                });
            const cIndex = model.uniqueIndexes.findIndex((u) => u.fieldNames.length === subset.length &&
                u.fieldNames.every((name, k) => name === subset[k]));
            if (cIndex !== -1)
                errors.push({
                    path: `${accessor}.uniqueIndexes[${i}].fieldNames`,
                    table: model.name,
                    field: null,
                    message: utils_1.StringUtil.trim `
            Subset unique index found (${subset.join(", ")}).
            
            You have defined an unique index with multiple fields,
            but its subset is already defined as an unique index.
            
            Consider to change the unique index to a plain index,
            or drop the redundant unique index please.
          `,
                });
        });
    });
    // SUPERSET RELATIONS
    model.plainIndexes.forEach((x, i) => {
        model.plainIndexes.forEach((y, j) => {
            if (i === j)
                return;
            else if (x.fieldNames.length < y.fieldNames.length &&
                x.fieldNames.every((n, z) => y.fieldNames[z] === n))
                errors.push({
                    path: `${accessor}.plainIndexes[${i}].fieldNames`,
                    table: model.name,
                    field: null,
                    message: utils_1.StringUtil.trim `
            Superset plain index found (${y.fieldNames.join(", ")}).
            
            You have defined a plain index with multiple fields,
            but its superset is already defined as another plain index.
            
            As subset index is vulnerable, drop this plain index please.
          `,
                });
        });
    });
    // CONSIDER GIN INDEXES
    if (model.ginIndexes.length !==
        new Set(model.ginIndexes.map((g) => g.fieldName)).size)
        errors.push({
            path: `${accessor}.ginIndexes[].fieldName`,
            table: model.name,
            field: null,
            message: utils_1.StringUtil.trim `
        Duplicated GIN index found.
        
        GIN index can only be used once per field.

        Please remove the duplicated GIN indexes.
      `,
        });
    return errors;
}
/* -----------------------------------------------------------
  VALIDATIONS
----------------------------------------------------------- */
function validateIndexes(model, accessor) {
    // EMENSION
    model.uniqueIndexes = model.uniqueIndexes.filter((unique) => unique.fieldNames.length !== 0 &&
        unique.fieldNames[0] !== model.primaryField.name);
    model.plainIndexes = model.plainIndexes.filter((plain) => plain.fieldNames.length !== 0 &&
        plain.fieldNames[0] !== model.primaryField.name);
    const errors = [];
    const columnNames = new Set([
        model.primaryField.name,
        ...model.foreignFields.map((field) => field.name),
        ...model.plainFields.map((field) => field.name),
    ]);
    // COLUMN LEVEL VALIDATION
    const validate = (props) => {
        props.indexes.forEach((idx, i) => {
            // FIND TARGET FIELD
            props.fieldNames(idx).forEach((name, j) => {
                if (!columnNames.has(name))
                    errors.push({
                        path: `${accessor}.${props.accessor(i, j)}`,
                        table: model.name,
                        field: null,
                        message: `Field ${name} does not exist in model ${model.name}.`,
                    });
                if (props.additional)
                    props.additional(idx, i);
            });
        });
    };
    validate({
        title: "unique index",
        indexes: model.uniqueIndexes,
        fieldNames: (idx) => idx.fieldNames,
        accessor: (i, j) => `uniqueIndexes[${i}].fieldNames[${j}]`,
    });
    validate({
        title: "index",
        indexes: model.plainIndexes,
        fieldNames: (idx) => idx.fieldNames,
        accessor: (i, j) => `plainIndexes[${i}].fieldNames[${j}]`,
    });
    validate({
        title: "index",
        indexes: model.ginIndexes,
        fieldNames: (idx) => [idx.fieldName],
        accessor: (i) => `ginIndexes[${i}].fieldName`,
        additional: (gin, i) => {
            const pIndex = model.plainFields.findIndex((plain) => plain.name === gin.fieldName);
            if (pIndex === -1)
                errors.push({
                    path: `${accessor}.ginIndexes[${i}].fieldName`,
                    table: model.name,
                    field: null,
                    message: utils_1.StringUtil.trim `
            GIN index can only be used on string typed field.
            However, the target field ${gin.fieldName} does not exist
            in the {@link plainFields}.
          `,
                });
            else if (model.plainFields[pIndex].type !== "string")
                errors.push({
                    path: `${accessor}.ginIndexes[${i}].fieldName`,
                    table: model.name,
                    field: model.plainFields[pIndex].name,
                    message: utils_1.StringUtil.trim `
            GIN index can only be used on string typed field.
            However, the target field ${gin.fieldName} is not string,
            but ${model.plainFields[pIndex].type}.
            
            - accessor of the wrong typed field: ${`${accessor}.plainFields[${pIndex}].type`},
          `,
                });
        },
    });
    return errors;
}
function validateReferences(model, accessor, dict) {
    const errors = [];
    model.foreignFields.forEach((field, i) => {
        const target = dict.get(field.relation.targetModel);
        if (target === undefined) {
            // CHECK EXISTENCE
            errors.push({
                path: `${accessor}.foreignFields[${i}].relation.targetModel`,
                table: model.name,
                field: field.name,
                message: `Target model ${field.relation.targetModel} does not exist.`,
            });
        }
        else if (target.model !== model) {
            // CHECK CROSS REFERENCE
            target.model.foreignFields.forEach((oppo, j) => {
                if (oppo.relation.targetModel === model.name) {
                    errors.push({
                        path: `${accessor}.foreignFields[${i}].relation.targetModel`,
                        table: model.name,
                        field: field.name,
                        message: utils_1.StringUtil.trim `
              Cross-reference dependency detected between models.

              - accessor of opposite side: application.files[${target.fileIndex}].models[${target.modelIndex}].foreignFields[${j}].relation.targetModel

              Cross-references (circular dependencies) are not permitted in AutoBe Prisma schemas.

              To resolve this issue:

              1. Remove one of the foreign key fields from either model
              2. Keep only the foreign key that represents the primary relationship direction
              3. Remove any related indexes that reference the deleted foreign key field

              The foreign key field to remove is typically:

              - A redundant field that can be computed from the existing relationship
              - A field that duplicates information already accessible through the primary relationship

              Please eliminate the circular dependency and try again.`,
                    });
                }
            });
        }
    });
    return errors;
}
//# sourceMappingURL=validatePrismaApplication.js.map