/**
 * Copyright 2013-2026 the original author or authors from the JHipster project.
 *
 * This file is part of the JHipster project, see https://www.jhipster.tech/
 * for more information.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { intersection, sortedUniq, uniq, upperFirst } from 'lodash-es';
import { APPLICATION_TYPE_GATEWAY, APPLICATION_TYPE_MICROSERVICE } from "../../../lib/core/application-types.js";
import { binaryOptions } from "../../../lib/jdl/core/built-in-options/index.js";
import { databaseTypes, fieldTypes, searchEngineTypes } from "../../../lib/jhipster/index.js";
import { getMicroserviceAppName, mutateData, stringHashCode } from "../../../lib/utils/index.js";
import { parseChangelog } from "../../base/support/timestamp.js";
import { getTypescriptKeyType } from "../../client/support/index.js";
import { getDatabaseTypeData, hibernateSnakeCase } from "../../server/support/index.js";
import { mutateEntity } from "../entity.js";
import { createFaker } from "./faker.js";
import { fieldIsEnum } from "./field-utils.js";
const NO_SEARCH_ENGINE = searchEngineTypes.NO;
const { CommonDBTypes } = fieldTypes;
const { BOOLEAN, LONG, STRING, UUID, INTEGER } = CommonDBTypes;
const { CASSANDRA, COUCHBASE, NEO4J, SQL, MONGODB } = databaseTypes;
const { INSTANT, ZONED_DATE_TIME, DURATION, LOCAL_DATE, BIG_DECIMAL, LOCAL_TIME } = fieldTypes.CommonDBTypes;
const { BYTES, BYTE_BUFFER } = fieldTypes.RelationalOnlyDBTypes;
const { IMAGE, TEXT } = fieldTypes.BlobTypes;
const BASE_TEMPLATE_DATA = {
    primaryKey: undefined,
    entityPackage: undefined,
    skipUiGrouping: false,
    anyFieldHasDocumentation: false,
    existingEnum: false,
    searchEngine: NO_SEARCH_ENGINE,
    microserviceName: undefined,
    entityAuthority: undefined,
    entityReadAuthority: undefined,
    adminEntity: undefined,
    builtInUserManagement: undefined,
    requiresPersistableImplementation: false,
    updatableEntity: undefined,
    anyFieldIsDateDerived: false,
    anyFieldIsTimeDerived: false,
    anyFieldIsInstant: false,
    anyFieldIsUUID: false,
    anyFieldIsZonedDateTime: false,
    anyFieldIsDuration: false,
    anyFieldIsLocalDate: false,
    anyFieldIsBigDecimal: false,
    anyFieldIsBlobDerived: false,
    anyFieldHasImageContentType: false,
    anyFieldHasTextContentType: false,
    anyFieldHasFileBasedContentType: false,
    anyPropertyHasValidation: false,
    fieldsContainNoOwnerOneToOne: false,
    get otherRelationships() {
        return [];
    },
    get enums() {
        return [];
    },
    // these variable hold field and relationship names for question options during update
    get fieldNameChoices() {
        return [];
    },
    get differentRelationships() {
        return {};
    },
};
export const entityDefaultConfig = {
    pagination: binaryOptions.DefaultValues[binaryOptions.Options.PAGINATION],
    anyPropertyHasValidation: undefined,
    dto: binaryOptions.DefaultValues[binaryOptions.Options.DTO],
    service: binaryOptions.DefaultValues[binaryOptions.Options.SERVICE],
    jpaMetamodelFiltering: false,
    readOnly: false,
    embedded: false,
    fluentMethods: true,
    get fields() {
        return [];
    },
    get relationships() {
        return [];
    },
};
export default function prepareEntity(entityWithConfig, generator) {
    const entityName = upperFirst(entityWithConfig.name);
    mutateData(entityWithConfig, entityDefaultConfig, BASE_TEMPLATE_DATA);
    if (entityWithConfig.changelogDate) {
        try {
            // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
            entityWithConfig.changelogDateForRecent = parseChangelog(String(entityWithConfig.changelogDate));
        }
        catch (error) {
            throw new Error(`Error parsing changelog date for entity ${entityName}: ${error.message}`, { cause: error });
        }
    }
    entityWithConfig.useMicroserviceJson = entityWithConfig.useMicroserviceJson || entityWithConfig.microserviceName !== undefined;
    entityWithConfig.microserviceAppName = '';
    if (generator.jhipsterConfig.applicationType === APPLICATION_TYPE_GATEWAY && entityWithConfig.useMicroserviceJson) {
        if (!entityWithConfig.microserviceName) {
            throw new Error('Microservice name for the entity is not found. Entity cannot be generated!');
        }
        entityWithConfig.microserviceAppName = getMicroserviceAppName({ microserviceName: entityWithConfig.microserviceName });
        entityWithConfig.skipServer = true;
    }
    mutateData(entityWithConfig, mutateEntity);
    mutateData(entityWithConfig, {
        __override__: false,
        hasRelationshipWithBuiltInUser: ({ relationships }) => relationships.some(relationship => relationship.otherEntity.builtInUser),
    });
    entityWithConfig.generateFakeData = type => {
        const fieldsToGenerate = type === 'cypress' ? entityWithConfig.fields.filter(field => !field.id || !field.autoGenerate) : entityWithConfig.fields;
        const fieldEntries = fieldsToGenerate
            .map(field => {
            const fieldData = field.generateFakeData(type);
            if (!field.nullable && fieldData === null)
                return undefined;
            return [field.fieldName, fieldData];
        })
            .filter(Boolean);
        const withError = fieldEntries.find(entry => !entry);
        if (withError) {
            generator.log.warn(`Error generating a full sample for entity ${entityName}`);
            return undefined;
        }
        return Object.fromEntries(fieldEntries);
    };
    return entityWithConfig;
}
export function derivedPrimaryKeyProperties(primaryKey) {
    mutateData(primaryKey, {
        hasUUID: primaryKey.fields?.some(field => field.fieldType === UUID),
        hasLong: primaryKey.fields?.some(field => field.fieldType === LONG),
        hasInteger: primaryKey.fields?.some(field => field.fieldType === INTEGER),
        typeUUID: primaryKey.type === UUID,
        typeString: primaryKey.type === STRING,
        typeLong: primaryKey.type === LONG,
        typeInteger: primaryKey.type === INTEGER,
        typeNumeric: !primaryKey.composite && primaryKey.fields[0].fieldTypeNumeric,
    });
}
export function prepareEntityPrimaryKeyForTemplates({ entity: entityWithConfig, enableCompositeId = true, application, }) {
    const idFields = entityWithConfig.fields.filter(field => field.id);
    const idRelationships = entityWithConfig.relationships.filter(relationship => relationship.id);
    let idCount = idFields.length + idRelationships.length;
    if (idCount === 0) {
        let idField = entityWithConfig.fields.find(field => field.fieldName === 'id');
        if (idField) {
            idField.id = true;
            idField.autoGenerate = idField.autoGenerate ?? true;
        }
        else {
            if (entityWithConfig.microserviceName && !application?.microfrontend) {
                this?.log.warn("Microservice entities should have the id field type specified (e.g., id String) to make sure gateway and microservice types don't conflict");
            }
            idField = {
                fieldName: 'id',
                id: true,
                fieldNameHumanized: 'ID',
                propertyTranslationKey: 'global.field.id',
                autoGenerate: true,
            };
            entityWithConfig.fields.unshift(idField);
        }
        idFields.push(idField);
        idCount++;
    }
    else if (idRelationships.length > 0) {
        idRelationships.forEach(relationship => {
            // relationships id data are not available at this point, so calculate it when needed.
            relationship.derivedPrimaryKey = {
                get derivedFields() {
                    return relationship.otherEntity.primaryKey.fields.map(field => ({
                        originalField: field,
                        ...field,
                        derived: true,
                        derivedEntity: relationship.otherEntity,
                        jpaGeneratedValue: false,
                        liquibaseAutoIncrement: false,
                        // Mapsid is generated by relationship select
                        autoGenerate: true,
                        readonly: true,
                        get derivedPath() {
                            if (field.derivedPath) {
                                if (relationship.otherEntity.primaryKey.derived) {
                                    return [relationship.relationshipName, ...field.derivedPath.splice(1)];
                                }
                                return [relationship.relationshipName, ...field.derivedPath];
                            }
                            return [relationship.relationshipName, field.fieldName];
                        },
                        get path() {
                            return [relationship.relationshipName, ...field.path];
                        },
                        get fieldName() {
                            return idCount === 1 ? field.fieldName : `${relationship.relationshipName}${field.fieldNameCapitalized}`;
                        },
                        get fieldNameCapitalized() {
                            return idCount === 1 ?
                                field.fieldNameCapitalized
                                : `${relationship.relationshipNameCapitalized}${field.fieldNameCapitalized}`;
                        },
                        get columnName() {
                            return idCount === 1 ?
                                field.columnName
                                : `${hibernateSnakeCase(relationship.relationshipName)}_${field.columnName}`;
                        },
                    }));
                },
            };
        });
    }
    if (idCount === 1 && idRelationships.length === 1) {
        const relationshipId = idRelationships[0];
        // One-To-One relationships with id uses @MapsId.
        // Almost every info is taken from the parent, except some info like autoGenerate and derived.
        // calling fieldName as id is for backward compatibility, in the future we may want to prefix it with relationship name.
        entityWithConfig.primaryKey = {
            // fieldName: 'id',
            derived: true,
            // MapsId copy the id from the relationship.
            autoGenerate: true,
            get fields() {
                return this.derivedFields;
            },
            get derivedFields() {
                return relationshipId.derivedPrimaryKey.derivedFields;
            },
            get ownFields() {
                return relationshipId.otherEntity.primaryKey.ownFields;
            },
            relationships: idRelationships,
            get name() {
                return relationshipId.otherEntity.primaryKey.name;
            },
            get hibernateSnakeCaseName() {
                return hibernateSnakeCase(relationshipId.otherEntity.primaryKey.name);
            },
            get nameCapitalized() {
                return relationshipId.otherEntity.primaryKey.nameCapitalized;
            },
            get type() {
                return relationshipId.otherEntity.primaryKey.type;
            },
            get tsType() {
                return relationshipId.otherEntity.primaryKey.tsType;
            },
            get composite() {
                return relationshipId.otherEntity.primaryKey.composite;
            },
            get ids() {
                return this.fields.map(field => fieldToId(field));
            },
        };
    }
    else {
        const composite = enableCompositeId ? idCount > 1 : false;
        let primaryKeyName;
        let primaryKeyType;
        if (composite) {
            primaryKeyName = 'id';
            primaryKeyType = `${entityWithConfig.entityClass}Id`;
        }
        else {
            const idField = idFields[0];
            // Allow ids type to be empty and fallback to default type for the database.
            if (!idField.fieldType) {
                idField.fieldType = application?.pkType ?? getDatabaseTypeData(entityWithConfig.databaseType).defaultPrimaryKeyType;
            }
            primaryKeyName = idField.fieldName;
            primaryKeyType = idField.fieldType;
        }
        entityWithConfig.primaryKey = {
            derived: false,
            name: primaryKeyName,
            hibernateSnakeCaseName: hibernateSnakeCase(primaryKeyName),
            nameCapitalized: upperFirst(primaryKeyName),
            type: primaryKeyType,
            tsType: getTypescriptKeyType(primaryKeyType),
            composite,
            relationships: idRelationships,
            // Fields declared in this entity
            ownFields: idFields,
            // Fields declared and inherited
            get fields() {
                return [...this.ownFields, ...this.derivedFields];
            },
            get autoGenerate() {
                return this.composite ? false : this.fields[0].autoGenerate;
            },
            // Fields inherited from id relationships.
            get derivedFields() {
                return this.relationships.flatMap(rel => rel.derivedPrimaryKey.derivedFields);
            },
            get ids() {
                return this.fields.map(field => fieldToId(field));
            },
        };
    }
    return entityWithConfig;
}
function fieldToId(field) {
    return {
        field,
        get name() {
            return field.fieldName;
        },
        get nameCapitalized() {
            return field.fieldNameCapitalized;
        },
        get nameDotted() {
            return field.derivedPath ? field.derivedPath.join('.') : field.fieldName;
        },
        get nameDottedAsserted() {
            return field.derivedPath ? `${field.derivedPath.join('!.')}!` : `${field.fieldName}!`;
        },
        get setter() {
            return `set${this.nameCapitalized}`;
        },
        get getter() {
            return (field.fieldType === BOOLEAN ? 'is' : 'get') + this.nameCapitalized;
        },
        get autoGenerate() {
            return !!field.autoGenerate;
        },
    };
}
/**
 * Copy required application config into entity.
 * Some entity features are related to the backend instead of the current app.
 * This allows generating entity files based on the backend features.
 */
export function loadRequiredConfigIntoEntity(entity, config) {
    mutateData(entity, {
        __override__: false,
        // applicationType: config.applicationType,
        // baseName: config.baseName,
        // authenticationType: config.authenticationType,
        reactive: config.reactive,
        microfrontend: config.microfrontend,
        // Workaround different paths
        clientFramework: config.clientFramework,
        databaseType: config.databaseType,
        prodDatabaseType: config.prodDatabaseType,
        searchEngine: config.searchEngine,
        // jhiPrefix: config.jhiPrefix,
        // entitySuffix: config.entitySuffix,
        // dtoSuffix: config.dtoSuffix,
        // packageName: config.packageName,
        microserviceName: ({ builtIn }) => (!builtIn && config.applicationType === APPLICATION_TYPE_MICROSERVICE ? config.baseName : undefined),
    });
    if (entity.searchEngine === true && (!entity.microserviceName || entity.microserviceName === config.baseName)) {
        // If the entity belongs to this application and searchEngine is true.
        if (config.searchEngine && config.searchEngine !== NO_SEARCH_ENGINE) {
            // Replace with the searchEngine from the application.
            entity.searchEngine = config.searchEngine;
        }
        else {
            entity.searchEngine = NO_SEARCH_ENGINE;
            this?.log.warn('Search engine is enabled at entity level, but disabled at application level. Search engine will be disabled');
        }
    }
    return entity;
}
export function preparePostEntityCommonDerivedProperties(entity) {
    const { fields } = entity;
    const fieldsType = sortedUniq(fields.map(({ fieldType }) => fieldType).filter(fieldType => !fieldIsEnum(fieldType)));
    // TODO move to server generator
    entity.anyFieldHasDocumentation = entity.fields.some(({ documentation }) => documentation);
    entity.anyFieldIsZonedDateTime = fieldsType.includes(ZONED_DATE_TIME);
    entity.anyFieldIsInstant = fieldsType.includes(INSTANT);
    entity.anyFieldIsLocalTime = fieldsType.includes(LOCAL_TIME);
    entity.anyFieldIsDuration = fieldsType.includes(DURATION);
    entity.anyFieldIsLocalDate = fieldsType.includes(LOCAL_DATE);
    entity.anyFieldIsBigDecimal = fieldsType.includes(BIG_DECIMAL);
    entity.anyFieldIsUUID = fieldsType.includes(UUID);
    entity.anyFieldIsTimeDerived = entity.anyFieldIsZonedDateTime || entity.anyFieldIsInstant;
    entity.anyFieldIsDateDerived = entity.anyFieldIsTimeDerived || entity.anyFieldIsLocalDate;
    entity.anyFieldIsBlobDerived = intersection(fieldsType, [BYTES, BYTE_BUFFER]).length > 0;
    if (entity.anyFieldIsBlobDerived) {
        const blobFields = fields.filter(({ fieldType }) => [BYTES, BYTE_BUFFER].includes(fieldType));
        const blobFieldsContentType = sortedUniq(blobFields.map(({ fieldTypeBlobContent }) => fieldTypeBlobContent));
        entity.anyFieldHasImageContentType = blobFieldsContentType.includes(IMAGE);
        entity.anyFieldHasFileBasedContentType = blobFieldsContentType.some(fieldTypeBlobContent => fieldTypeBlobContent !== TEXT);
        entity.anyFieldHasTextContentType = blobFieldsContentType.includes(TEXT);
    }
    preparePostEntityCommonDerivedPropertiesNotTyped(entity);
}
function preparePostEntityCommonDerivedPropertiesNotTyped(entity) {
    const { relationships, fields } = entity;
    const oneToOneRelationships = relationships.filter(({ relationshipType }) => relationshipType === 'one-to-one');
    entity.fieldsContainNoOwnerOneToOne = oneToOneRelationships.some(({ ownerSide }) => !ownerSide);
    entity.anyPropertyHasValidation =
        entity.anyPropertyHasValidation || relationships.some(({ relationshipValidate }) => relationshipValidate);
    const relationshipsByOtherEntity = relationships
        .map(relationship => [relationship.otherEntity.entityNameCapitalized, relationship])
        .reduce((relationshipsByOtherEntity, [type, relationship]) => {
        if (relationshipsByOtherEntity[type]) {
            relationshipsByOtherEntity[type].push(relationship);
        }
        else {
            relationshipsByOtherEntity[type] = [relationship];
        }
        return relationshipsByOtherEntity;
    }, {});
    entity.relationshipsByOtherEntity = relationshipsByOtherEntity;
    entity.differentRelationships = relationshipsByOtherEntity;
    entity.anyPropertyHasValidation = entity.anyPropertyHasValidation || fields.some(({ fieldValidate }) => fieldValidate);
    entity.otherEntities = uniq(entity.relationships.map(rel => rel.otherEntity));
    entity.persistableRelationships = relationships.filter(({ persistableRelationship }) => persistableRelationship);
    entity.otherEntitiesWithPersistableRelationship = uniq(entity.persistableRelationships.map(rel => rel.otherEntity));
    entity.updatableEntity =
        entity.fields.some(field => !field.id && !field.transient) ||
            entity.relationships.some(relationship => !relationship.id && relationship.persistableRelationship);
    entity.entityContainsCollectionField = entity.relationships.some(relationship => relationship.collection);
    if (entity.primaryKey) {
        derivedPrimaryKeyProperties(entity.primaryKey);
        entity.requiresPersistableImplementation =
            entity.requiresPersistableImplementation || entity.fields.some(field => field.requiresPersistableImplementation);
    }
    const types = entity.relationships
        .filter(rel => rel.otherEntity.primaryKey)
        .flatMap(rel => rel.otherEntity.primaryKey.fields.map(f => f.fieldType));
    entity.otherEntityPrimaryKeyTypes = [...new Set(types)];
    entity.otherEntityPrimaryKeyTypesIncludesUUID = types.includes(UUID);
    entity.relationships.forEach(relationship => {
        if (!relationship.otherEntity.primaryKey) {
            relationship.bagRelationship = false;
            relationship.relationshipEagerLoad = false;
            return;
        }
        mutateData(relationship, {
            bagRelationship: relationship.ownerSide && relationship.collection,
            relationshipEagerLoad: ({ relationshipEagerLoad, bagRelationship, ownerSide, otherEntity, otherEntityField }) => relationshipEagerLoad ??
                (bagRelationship ||
                    entity.eagerLoad ||
                    // Fetch relationships if otherEntityField differs otherwise the id is enough
                    (ownerSide && otherEntity.primaryKey.name !== otherEntityField)),
        });
    });
    entity.relationshipsContainEagerLoad = entity.relationships.some(relationship => relationship.relationshipEagerLoad);
    entity.containsBagRelationships = entity.relationships.some(relationship => relationship.bagRelationship);
    entity.implementsEagerLoadApis = // Cassandra doesn't provides *WithEagerRelationships apis
        ![CASSANDRA, COUCHBASE, NEO4J].includes(entity.databaseType) &&
            // Only sql and mongodb provides *WithEagerRelationships apis for imperative implementation
            (entity.reactive || [SQL, MONGODB].includes(entity.databaseType)) &&
            entity.relationshipsContainEagerLoad;
    entity.eagerRelations = entity.relationships.filter(rel => rel.relationshipEagerLoad);
    entity.regularEagerRelations = entity.eagerRelations.filter(rel => rel.id !== true);
    entity.reactiveEagerRelations = entity.relationships.filter(rel => rel.relationshipType === 'many-to-one' || (rel.relationshipType === 'one-to-one' && rel.ownerSide));
    entity.reactiveRegularEagerRelations = entity.reactiveEagerRelations.filter(rel => rel.id !== true);
}
export async function addFakerToEntity(entityWithConfig, nativeLanguage = 'en') {
    entityWithConfig.faker = entityWithConfig.faker || (await createFaker(nativeLanguage));
    entityWithConfig.resetFakerSeed = (suffix = '') => entityWithConfig.faker.seed(stringHashCode(entityWithConfig.name.toLowerCase() + suffix));
    entityWithConfig.resetFakerSeed();
}
