/**
 * 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 { existsSync, readFileSync } from 'node:fs';
import BaseApplicationGenerator from "../base-application/index.js";
import { PRIORITY_NAMES } from "../base-application/priorities.js";
import { loadEntitiesAnnotations, loadEntitiesOtherSide } from "../base-application/support/index.js";
import { relationshipEquals, relationshipNeedsForeignKeyRecreationOnly } from "../liquibase/support/index.js";
import { addEntitiesOtherRelationships } from "../server/support/index.js";
const { DEFAULT, WRITING_ENTITIES, POST_WRITING_ENTITIES } = PRIORITY_NAMES;
const baseChangelog = () => ({
    newEntity: false,
    changedEntity: false,
    incremental: false,
    previousEntity: undefined,
    addedFields: [],
    removedFields: [],
    addedRelationships: [],
    removedRelationships: [],
    relationshipsToRecreateForeignKeysOnly: [],
    removedDefaultValueFields: [],
    addedDefaultValueFields: [],
});
/**
 * This is the base class for a generator for every generator.
 */
export default class BaseEntityChangesGenerator extends BaseApplicationGenerator {
    recreateInitialChangelog;
    entityChanges;
    getTaskFirstArgForPriority(priorityName) {
        const firstArg = super.getTaskFirstArgForPriority(priorityName);
        if ([DEFAULT, WRITING_ENTITIES, POST_WRITING_ENTITIES].includes(priorityName)) {
            const { application, entities } = firstArg;
            this.entityChanges = this.generateIncrementalChanges({ application, entities });
        }
        if ([DEFAULT].includes(priorityName)) {
            return { ...firstArg, entityChanges: this.entityChanges };
        }
        if ([WRITING_ENTITIES, POST_WRITING_ENTITIES].includes(priorityName)) {
            // const { entities = [] } = this.options;
            // const filteredEntities = data.entities.filter(entity => entities.includes(entity.name));
            return { ...firstArg, entityChanges: this.entityChanges };
        }
        return firstArg;
    }
    /**
     * Generate changelog from differences between the liquibase entity and current entity.
     */
    generateIncrementalChanges({ application, entities: paramEntities, }) {
        const { recreateInitialChangelog } = this;
        const { incrementalChangelog } = application;
        const entityNames = paramEntities.filter(e => !e.builtIn).map(e => e.name);
        const entitiesByName = Object.fromEntries(paramEntities.map(entity => [entity.name, entity]));
        const entitiesWithExistingChangelog = new Set(entityNames.filter(entityName => !this.isChangelogNew({ entityName, changelogDate: entitiesByName[entityName].annotations?.changelogDate })));
        const previousEntitiesByName = Object.fromEntries(entityNames
            .map(entityName => ({ entityName, entityConfigPath: this.getEntityConfigPath(entityName) }))
            .filter(({ entityConfigPath }) => existsSync(entityConfigPath))
            .map(({ entityName, entityConfigPath }) => [
            entityName,
            { name: entityName, ...JSON.parse(readFileSync(entityConfigPath).toString()) },
        ]));
        for (const [entityName, entity] of Object.entries(entitiesByName)) {
            if (entity.builtIn) {
                previousEntitiesByName[entityName] = entity;
            }
        }
        if (application.generateBuiltInUserEntity && !previousEntitiesByName.User) {
            previousEntitiesByName.User = { name: 'User', builtIn: true };
        }
        const entities = Object.values(previousEntitiesByName);
        loadEntitiesAnnotations(entities);
        loadEntitiesOtherSide(entities);
        addEntitiesOtherRelationships(entities);
        // Compare entity changes and create changelogs
        return entityNames.map(entityName => {
            const newConfig = entitiesByName[entityName];
            const newFields = (newConfig.fields || []).filter(field => !field.transient);
            const newRelationships = newConfig.relationships || [];
            const oldConfig = previousEntitiesByName[entityName];
            if (!oldConfig || recreateInitialChangelog || !incrementalChangelog || !entitiesWithExistingChangelog.has(entityName)) {
                return {
                    ...baseChangelog(),
                    incremental: newConfig.incrementalChangelog,
                    changelogDate: newConfig.changelogDate,
                    newEntity: true,
                    entity: newConfig,
                    entityName,
                };
            }
            this._debug(`Calculating diffs for ${entityName}`);
            const oldFields = (oldConfig.fields || []).filter(field => !field.transient);
            const oldFieldNames = oldFields.filter(field => !field.id).map(field => field.fieldName);
            const newFieldNames = newFields.filter(field => !field.id).map(field => field.fieldName);
            // Calculate new fields
            const addedFieldNames = newFieldNames.filter(fieldName => !oldFieldNames.includes(fieldName));
            const addedFields = addedFieldNames.map(fieldName => newFields.find(field => fieldName === field.fieldName));
            // Calculate removed fields
            const removedFieldNames = oldFieldNames.filter(fieldName => !newFieldNames.includes(fieldName));
            const removedFields = removedFieldNames.map(fieldName => oldFields.find(field => fieldName === field.fieldName));
            const oldRelationships = oldConfig.relationships || [];
            // Calculate changed/newly added relationships
            const addedRelationships = newRelationships.filter(newRelationship => 
            // id changes are not supported
            !newRelationship.id &&
                // check if the same relationship wasn't already part of the old config
                !oldRelationships.some(oldRelationship => relationshipEquals(oldRelationship, newRelationship)));
            // Calculate to be removed relationships
            const removedRelationships = oldRelationships.filter(oldRelationship => 
            // id changes are not supported
            !oldRelationship.id &&
                // check if there are relationships not anymore in the new config
                !newRelationships.some(newRelationship => relationshipEquals(newRelationship, oldRelationship)));
            // calculate relationships that only need a foreign key recreation from the ones that are added
            // we need both the added and the removed ones here
            const relationshipsToRecreateForeignKeysOnly = [
                ...addedRelationships.filter(addedRelationship => removedRelationships.some(removedRelationship => relationshipNeedsForeignKeyRecreationOnly(removedRelationship, addedRelationship))),
                ...removedRelationships.filter(removedRelationship => addedRelationships.some(addedRelationship => relationshipNeedsForeignKeyRecreationOnly(addedRelationship, removedRelationship))),
            ];
            const oldFieldsWithDefaultValues = oldFields.filter(field => this.hasAnyDefaultValue(field));
            const newFieldsWithDefaultValues = newFields.filter(field => this.hasAnyDefaultValue(field));
            // find the old fields that have not been deleted anyway or otherwise where the default value is different on the same new field
            const removedDefaultValueFields = oldFieldsWithDefaultValues
                .filter(oldField => !removedFieldNames.includes(oldField.fieldName))
                .filter(
            // field was not removed, so check its default value
            oldField => this.doDefaultValuesDiffer(oldField, newFields.find(newField => newField.fieldName === oldField.fieldName)));
            // find the new fields that have not been added newly anyway or otherwise where the old field had a different default value
            const addedDefaultValueFields = newFieldsWithDefaultValues
                .filter(newField => !addedFieldNames.includes(newField.fieldName))
                .filter(
            // field was not added newly, so check its default value
            newField => this.doDefaultValuesDiffer(oldFields.find(oldField => oldField.fieldName === newField.fieldName), newField));
            return {
                ...baseChangelog(),
                previousEntity: oldConfig,
                entity: newConfig,
                incremental: true,
                changedEntity: true,
                entityName,
                addedFields,
                removedFields,
                addedRelationships,
                removedRelationships,
                relationshipsToRecreateForeignKeysOnly,
                removedDefaultValueFields,
                addedDefaultValueFields,
            };
        });
    }
    hasAnyDefaultValue(field) {
        return field.defaultValue !== undefined || field.defaultValueComputed !== undefined;
    }
    doDefaultValuesDiffer(field1, field2) {
        return field1.defaultValue !== field2.defaultValue || field1.defaultValueComputed !== field2.defaultValueComputed;
    }
}
