"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateConvertImportFunctions = generateConvertImportFunctions;
const Generator = __importStar(require("@postxl/generator"));
const utils_1 = require("@postxl/utils");
/**
 * Generates the Convert function for the Import module
 */
function generateConvertImportFunctions(context) {
    const { dto } = context.types;
    const { detectDelta, converterFunctions } = context.import;
    const { importedDataToBulkMutations, deltaToBulkMutations } = converterFunctions;
    const imports = Generator.ImportGenerator
        //
        .from(importedDataToBulkMutations.location)
        .addImport({
        items: [Generator.toFunctionName('ExhaustiveSwitchCheck')],
        from: Generator.toPackageName('@postxl/utils'),
    })
        .addType(detectDelta.allModels.type)
        .addType(dto.genericModel)
        .addType(dto.idType)
        .addType(context.repositories.bulkMutation)
        .addType(context.repositories.bulkMutationForModel)
        .addType(context.decoders.dataModel.decodedType)
        .addType(detectDelta.types.generic.modelType);
    const importConverterBlocks = [];
    const deltaConverterBlocks = [];
    let state = initializeModelsState(context);
    let nextStep = determineNextStep(state);
    // logState({ state, nextStep, lastStep: 'init' })
    let iteration = 0;
    while (nextStep !== 'completed') {
        iteration++;
        const { importConvertBlock, deltaConvertBlock, state: stateUpdated, } = generateBlocks({ state, nextStep, imports, iteration, context });
        importConverterBlocks.push(importConvertBlock);
        deltaConverterBlocks.push(deltaConvertBlock);
        state = updateModelState(stateUpdated);
        nextStep = determineNextStep(state);
        // logState({ state, nextStep, lastStep: 'updateModelState' })
    }
    return `
${imports.generate()}

/**
 * Converts ${detectDelta.types.generic.modelType.name} to ${context.repositories.bulkMutation.name} steps, taking into account the order of dependencies.
 * **Important**: It assumes that data has been pre-checked and converted to \`${detectDelta.types.generic.modelType.name}\`.
 */
export function ${deltaToBulkMutations.name}(
  input: ${detectDelta.allModels.type.name}
  ): ${context.repositories.bulkMutation.name}[] {
  return [${deltaConverterBlocks.join(',\n')}]
}

/**
 * Converts any models to ${context.repositories.bulkMutation.name} steps, taking into account the order of dependencies.
 * **Important**: It does not check if the items exist, ie. it is meant mainly for seeding.
 *                For sophisticated imports that handle deltas, use \`${deltaToBulkMutations.name}\` instead!
 */
export function ${importedDataToBulkMutations.name}(
  input: ${context.decoders.dataModel.decodedType.name}
  ): ${context.repositories.bulkMutation.name}[] {
  return [${importConverterBlocks.join(',\n')}]
}

/**
 * Removes empty arrays from a ${context.repositories.bulkMutation.name} object.
 */
function _removeBlank${context.repositories.bulkMutation.name}<Model extends ${dto.genericModel.name}<ID>, ID extends ${dto.idType.name}>(
  bulk: ${context.repositories.bulkMutationForModel.name}<Model, ID>,
): ${context.repositories.bulkMutationForModel.name}<Model, ID> {
  const result: ${context.repositories.bulkMutationForModel.name}<Model, ID> = {}
  if (bulk.create && bulk.create.length > 0) { result.create = bulk.create }
  if (bulk.update && bulk.update.length > 0) { result.update = bulk.update }
  if (bulk.upsert && bulk.upsert.length > 0) { result.upsert = bulk.upsert }
  if (bulk.delete && bulk.delete.length > 0) { result.delete = bulk.delete }

  return result
}

/**
 * Converts a list of Deltas for a model to a ${context.repositories.bulkMutation.name} object.
 */
function deltaModelTo${context.repositories.bulkMutation.name}<
  Model extends ${dto.genericModel.name}<ID>, 
  ID extends ${dto.idType.name},
  ModelErrors
>(
  items?: ${detectDelta.types.generic.modelType.name}<Model, ID, ModelErrors>[],
): ${context.repositories.bulkMutationForModel.name}<Model, ID> {
  const result: Required<${context.repositories.bulkMutationForModel.name}<Model, ID>> = {
    create: [],
    update: [],
    upsert: [],
    delete: [],
  }

  for (const item of items ?? []) {
    switch (item.type) {
      case 'create':
        result.create.push(item.createDto)
        break
      case 'update':
        result.update.push(item.updateDto)
        break
      case 'delete':
        result.delete.push(item.input.id)
        break
      case 'unchanged':
        break
      case 'errors':
        throw new Error('Cannot convert delta to bulk mutation as it contains errors')
      default:
        throw new ExhaustiveSwitchCheck(item)
    }
  }

  return _removeBlank${context.repositories.bulkMutation.name}(result)
}
`;
}
// Helpful for debugging the complex logic, so should not be removed
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function logState({ iteration, state, nextStep, lastStep, }) {
    console.log(`
Iteration: ${iteration++}
LastStep: ${lastStep}
Completed: ${[...state.completed.values()].map((model) => model.name).join(', ')}
CreatedToBeLinked: ${[...state.createdToBeLinked.values()].map((x) => x.model.name).join(', ')}
CanBeCreatedFullyLinked: ${[...state.canBeCreatedFullyLinked.values()].map((x) => x.model.name).join(', ')}
CanBeCreatedPartiallyLinked: ${[...state.canBeCreatedPartiallyLinked.values()].map((x) => x.model.name).join(', ')}
Blocked: ${[...state.blocked.values()].map((x) => x.model.name).join(', ')}
NextStep: ${nextStep}`);
}
/**
 * Groups the fields of a model into fields that can be created, fields that can be created as null, and fields that block the creation of the model.
 */
function groupModelFields({ state, model }) {
    const fieldsGrouped = {
        model,
        create: new Set(),
        nullableFieldsForLaterLinking: new Set(),
        requiredFieldsBlockingLinking: new Set(),
        ignoreFields: new Set(),
    };
    for (const field of model.fields.values()) {
        addFieldToFieldsGroup({ state, fieldsGrouped, field });
    }
    return fieldsGrouped;
}
function addFieldToFieldsGroup({ state, fieldsGrouped, field, }) {
    switch (field.kind) {
        case 'id':
        case 'enum':
            fieldsGrouped.create.add(field);
            return;
        case 'scalar':
            // We ignore createdAt and updatedAt fields as they are set automatically.
            if (field.isCreatedAt || field.isUpdatedAt) {
                fieldsGrouped.ignoreFields.add(field);
                return;
            }
            fieldsGrouped.create.add(field);
            return;
        case 'relation':
            addFieldRelationToFieldsGroup({ state, fieldsGrouped, field });
            return;
        case 'discriminatedUnion':
            addFieldDiscriminatedUnionToFieldsGroup({ state, fieldsGrouped, field });
            break;
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
}
function addFieldRelationToFieldsGroup({ state, fieldsGrouped, field, }) {
    // Related model is already created so we can create the field.
    if (state.completed.has(field.referencedModelName) || state.createdToBeLinked.has(field.referencedModelName)) {
        fieldsGrouped.create.add(field);
        return;
    }
    // In nullable relation we can set the field to null and link it later.
    if (!field.isRequired) {
        fieldsGrouped.nullableFieldsForLaterLinking.add(field);
        return;
    }
    // All other options are exhausted meaning this is a required relation and the related model hasn't been created yet.
    // We cannot create the field yet so we add it to backlog.
    fieldsGrouped.requiredFieldsBlockingLinking.add(field);
}
function addFieldDiscriminatedUnionToFieldsGroup({ state, fieldsGrouped, field, }) {
    const relationFields = [];
    for (const f of field.commonFields.values()) {
        if (f.kind === 'relation') {
            relationFields.push(f);
        }
    }
    for (const member of field.members.values()) {
        for (const f of member.fields.values()) {
            if (f.kind === 'relation') {
                relationFields.push(f);
            }
        }
    }
    // we start with the least restrictive setting `create`. in case any related field is not processed yet, it will be set to the stricter `nullableFieldsForLaterLinking` or `requiredFieldsBlockingLinking`
    let duState = 'create';
    for (const relationField of relationFields) {
        // in case related field is already processed, we can keep the duState as is
        if (state.completed.has(relationField.referencedModelName) ||
            state.createdToBeLinked.has(relationField.referencedModelName)) {
            continue;
        }
        if (relationField.isRequired) {
            duState = 'requiredFieldsBlockingLinking';
            // it cannot get stricter, hence we break
            break;
        }
        else {
            duState = 'nullableFieldsForLaterLinking';
        }
    }
    fieldsGrouped[duState].add(field);
}
/**
 * Determines the state (created partially linked, or can be created fully linked) of a model based on its fields
 */
function determineModelNextStep(fieldsGrouped) {
    // If we have blocking fields, we cannot create the model yet
    if (fieldsGrouped.requiredFieldsBlockingLinking.size > 0) {
        return 'blocked';
    }
    // If we have no blocking fields, we can create the model but need to link some fields later
    if (fieldsGrouped.nullableFieldsForLaterLinking.size > 0) {
        return 'canBeCreatedPartiallyLinked';
    }
    // If no field is blocking and all nullable fields can be linked, we can create the model and link all fields
    return 'canBeCreatedFullyLinked';
}
function assignModelToState({ state, modelNextStep, fieldsGrouped, }) {
    switch (modelNextStep) {
        case 'blocked':
            state.blocked.set(fieldsGrouped.model.name, fieldsGrouped);
            break;
        case 'canBeCreatedPartiallyLinked':
            state.canBeCreatedPartiallyLinked.set(fieldsGrouped.model.name, fieldsGrouped);
            break;
        case 'canBeCreatedFullyLinked':
            state.canBeCreatedFullyLinked.set(fieldsGrouped.model.name, fieldsGrouped);
            break;
        default:
            throw new utils_1.ExhaustiveSwitchCheck(modelNextStep);
    }
}
function initializeModelsState({ models }) {
    const state = {
        completed: new Map(),
        createdToBeLinked: new Map(),
        canBeCreatedFullyLinked: new Map(),
        canBeCreatedPartiallyLinked: new Map(),
        blocked: new Map(),
    };
    for (const model of models.values()) {
        if (model.repository.kind === 'NoRepository' || model.isReadonly) {
            continue;
        }
        const fieldsGrouped = groupModelFields({ state, model });
        const modelNextStep = determineModelNextStep(fieldsGrouped);
        assignModelToState({ state, modelNextStep, fieldsGrouped });
    }
    return state;
}
/**
 * Checks if any of the blocked or partially linked models can be created now.
 */
function updateModelState(oldState) {
    const newState = copyModelState(oldState);
    newState.blocked = new Map();
    newState.canBeCreatedPartiallyLinked = new Map();
    // We iterate over all blocked models and check if they can be created now
    for (const [, fieldsGrouped] of oldState.blocked) {
        const fieldsGroupedUpdated = groupModelFields({ state: oldState, model: fieldsGrouped.model });
        const modelNextStep = determineModelNextStep(fieldsGroupedUpdated);
        assignModelToState({ state: newState, modelNextStep, fieldsGrouped: fieldsGroupedUpdated });
    }
    // We iterate over all partially linked models and check if they can be created fully linked now
    for (const [, fieldsGrouped] of oldState.canBeCreatedPartiallyLinked) {
        const fieldsGroupedUpdated = groupModelFields({ state: oldState, model: fieldsGrouped.model });
        const modelNextStep = determineModelNextStep(fieldsGroupedUpdated);
        assignModelToState({ state: newState, modelNextStep, fieldsGrouped: fieldsGroupedUpdated });
    }
    return newState;
}
function determineNextStep(modelsState) {
    // We prioritize models that can be created fully linked, so we do not need unnecessary linking steps later
    if (modelsState.canBeCreatedFullyLinked.size > 0) {
        return 'createFullyLinked';
    }
    // If no model can be created fully linked, we check if we can create models partially linked
    if (modelsState.canBeCreatedPartiallyLinked.size > 0) {
        return 'createPartiallyLinked';
    }
    // Lastly, we link all models that require linking
    if (modelsState.createdToBeLinked.size > 0) {
        return 'link';
    }
    // If we now still have blocked models, we have a circular dependency
    if (modelsState.blocked.size > 0) {
        throw new Error(`Cannot create the following models because they have a circular dependency: ${Array.from(modelsState.blocked.values())
            .map((model) => model.model.name)
            .join(', ')}`);
    }
    // If we have no blocked models, we are done
    return 'completed';
}
function generateBlocks(params) {
    switch (params.nextStep) {
        case 'createFullyLinked':
            return generateBulkMutationBlockCreateFullyLinked(params);
        case 'createPartiallyLinked':
            return generateBulkMutationBlockCreatePartiallyLinked(params);
        case 'link':
            return generateBulkMutationBlockLink(params);
        default:
            throw new utils_1.ExhaustiveSwitchCheck(params.nextStep);
    }
}
function copyModelState(state) {
    return {
        completed: new Map(state.completed),
        createdToBeLinked: new Map(state.createdToBeLinked),
        canBeCreatedFullyLinked: new Map(state.canBeCreatedFullyLinked),
        canBeCreatedPartiallyLinked: new Map(state.canBeCreatedPartiallyLinked),
        blocked: new Map(state.blocked),
    };
}
function generateBulkMutationBlockCreateFullyLinked({ state, iteration, imports, context, }) {
    const newState = copyModelState(state);
    newState.canBeCreatedFullyLinked = new Map();
    const models = state.canBeCreatedFullyLinked;
    const importConvertBlocks = [];
    const deltaConvertBlocks = [];
    const modelNames = [];
    for (const [, fieldsGrouped] of models) {
        const model = fieldsGrouped.model;
        if (model.repository.kind === 'NoRepository' || model.isReadonly) {
            continue;
        }
        imports.addType(model.types.dto.create);
        const { block: importConvertBlock } = generateModelCreateBlock({ fieldsGrouped, model });
        importConvertBlocks.push(importConvertBlock);
        deltaConvertBlocks.push(`${model.repository.bulkMutationEntry}: deltaModelTo${context.repositories.bulkMutation.name}(input.${model.decoder.dataModelEntry})`);
        newState.completed.set(fieldsGrouped.model.name, fieldsGrouped.model);
        modelNames.push(model.userFriendlyNamePlural);
    }
    const importConvertBlock = `
  // Step ${iteration}: Create fully linked models: ${modelNames.join(', ')}
  {
    ${importConvertBlocks.join(',\n')}
  }`;
    const deltaConvertBlock = `
  // Step ${iteration}: Create/update entire entries for: ${modelNames.join(', ')}
  {
    ${deltaConvertBlocks.join(',\n')}
  }`;
    return { importConvertBlock, deltaConvertBlock, state: newState };
}
function generateBulkMutationBlockCreatePartiallyLinked({ state, imports, iteration, context, }) {
    const newState = copyModelState(state);
    newState.canBeCreatedPartiallyLinked = new Map();
    const models = state.canBeCreatedPartiallyLinked;
    const importConvertBlocks = [];
    const deltaConvertBlocks = [];
    const modelNames = [];
    for (const [, fieldsGrouped] of models) {
        const model = fieldsGrouped.model;
        if (model.repository.kind === 'NoRepository' || model.isReadonly) {
            continue;
        }
        imports.addType(model.types.dto.create);
        const { block: importConvertBlock, fieldsToBeLinkedLater } = generateModelCreateBlock({ fieldsGrouped, model });
        importConvertBlocks.push(importConvertBlock);
        const deltaConvertBlock = generateDeltaBlock({ fieldsGrouped, model, context, imports });
        deltaConvertBlocks.push(deltaConvertBlock);
        newState.createdToBeLinked.set(fieldsGrouped.model.name, fieldsGrouped);
        modelNames.push(`${model.userFriendlyNamePlural} (without ${fieldsToBeLinkedLater.join(', ')})`);
    }
    const importConvertBlock = `
  // Step ${iteration}: Create partially linked models: 
  // * ${modelNames.join('\n  // * ')}
  {
    ${importConvertBlocks.join(',\n')}
  }`;
    const deltaConvertBlock = `
  // Step ${iteration}: Create partially linked models:
  // * ${modelNames.join('\n  // * ')}
  {
    ${deltaConvertBlocks.join(',\n')}
  }`;
    return { importConvertBlock, deltaConvertBlock, state: newState };
}
function generateModelCreateBlock({ fieldsGrouped, model }) {
    if (model.repository.kind === 'NoRepository' || model.isReadonly) {
        throw new Error('Cannot generate create block for readonly model');
    }
    const { createFromObject: createAssignments, fieldsToBeLinkedLater } = generateFieldAssignmentBlocks({
        fieldsGrouped,
        model,
    });
    return {
        block: /* ts */ `
    ${model.repository.bulkMutationEntry}: {
      create: input.${model.decoder.dataModelEntry}?.map(
        (item: ${model.types.dto.create.name}) => ({
          ${createAssignments.join(',\n')}
        })) 
        ?? []
      }`,
        fieldsToBeLinkedLater,
    };
}
function generateDeltaBlock({ fieldsGrouped, model, context, imports, }) {
    const { createFromDelta: createAssignments, updateFromDelta: updateAssignments } = generateFieldAssignmentBlocks({
        fieldsGrouped,
        model,
    });
    if (model.repository.kind === 'NoRepository' || model.isReadonly) {
        throw new Error('Cannot generate delta for readonly model');
    }
    const genericsDefinition = `${model.types.name}, ${model.types.id.name}`;
    const { modelType: type, create, update } = context.import.detectDelta.types.generic;
    imports
        .addType(model.types)
        .addType(model.types.id)
        .addType(type)
        .addType(create.type)
        .addType(update.type)
        .add(create.typeGuard)
        .add(update.typeGuard);
    return /* ts */ `
  ${model.repository.bulkMutationEntry}: {
    create:
      input.${model.decoder.dataModelEntry}
        ?.filter(${create.typeGuard.name})
        .map((item: ${create.type.name}<${genericsDefinition}>) => ({
          ${createAssignments.join(',\n')}
        })) ?? [],
    update:
      input.${model.decoder.dataModelEntry}
        ?.filter(${update.typeGuard.name})
        .map((item: ${update.type.name}<${genericsDefinition}>) => ({
          id: item.input.id,
          ${updateAssignments.join(',\n')},
        })) ?? [],
    }`;
}
function generateBulkMutationBlockLink({ state, imports, iteration, context, }) {
    const newState = copyModelState(state);
    newState.createdToBeLinked = new Map();
    const models = state.createdToBeLinked;
    const importConvertBlocks = [];
    const deltaConvertBlocks = [];
    const modelNames = [];
    for (const [, fieldsGrouped] of models) {
        const model = fieldsGrouped.model;
        if (model.repository.kind === 'NoRepository' || model.isReadonly) {
            continue;
        }
        imports.addType(model.types.dto.update);
        const { importConvertBlock, deltaConvertBlock } = generateModelLinkBlocks({ fieldsGrouped, model, context });
        importConvertBlocks.push(importConvertBlock);
        deltaConvertBlocks.push(deltaConvertBlock);
        newState.completed.set(fieldsGrouped.model.name, fieldsGrouped.model);
        modelNames.push(model.userFriendlyNamePlural);
    }
    const importConvertBlock = `
  // Step ${iteration}: Link models: ${modelNames.join(', ')}
  {
    ${importConvertBlocks.join(',\n')}
  }`;
    const deltaConvertBlock = `
  // Step ${iteration}: Link models: ${modelNames.join(', ')}
  {
    ${deltaConvertBlocks.join(',\n')}
  }`;
    return { importConvertBlock, deltaConvertBlock, state: newState };
}
function generateModelLinkBlocks({ fieldsGrouped, model, context, }) {
    if (model.repository.kind === 'NoRepository' || model.isReadonly) {
        throw new Error('Cannot generate link blocks for readonly model');
    }
    const { nullableFieldsForLaterLinking } = fieldsGrouped;
    const genericsDefinition = `${model.types.name}, ${model.types.id.name}`;
    const { create, update } = context.import.detectDelta.types.generic;
    if (nullableFieldsForLaterLinking.size === 0) {
        throw new Error(`Cannot link fields for ${model.name} as it does not have any nullable fields`);
    }
    const { linkFromObject, linkFromCreateDTO, linkFromDelta } = generateFieldAssignmentBlocks({
        fieldsGrouped,
        model,
    });
    return {
        importConvertBlock: /* ts */ `
      ${model.repository.bulkMutationEntry}: {
        update: input.${model.decoder.dataModelEntry}?.map(
          (item: ${model.types.dto.update.name}) => ({
            id: item.id,
            ${linkFromObject.join(',\n')}
          }))
          ?? []
      }`,
        deltaConvertBlock: /* ts */ `
    ${model.repository.bulkMutationEntry}: {
        update: [
          ...input.${model.decoder.dataModelEntry}
            ?.filter(${create.typeGuard.name})
            .map((item: ${create.type.name}<${genericsDefinition}>) => ({
              id: item.input.id,
              ${linkFromCreateDTO.join(',\n')}
            })) ?? [],
      
          ...input.${model.decoder.dataModelEntry}
            ?.filter(${update.typeGuard.name})
            .map((item: ${update.type.name}<${genericsDefinition}>) => ({
              id: item.input.id,
              ${linkFromDelta.join(',\n')},
            })) ?? [],
          ]
      }`,
    };
}
function generateFieldAssignmentBlocks({ fieldsGrouped, }) {
    const { model, create, nullableFieldsForLaterLinking, requiredFieldsBlockingLinking } = fieldsGrouped;
    if (requiredFieldsBlockingLinking.size > 0) {
        const _fields = Array.from(requiredFieldsBlockingLinking.values())
            .map((field) => field.name)
            .join(', ');
        throw new Error(`Cannot create model ${model.name} because some required fields don't exist yet: ${_fields}`);
    }
    const result = {
        fieldsToBeLinkedLater: [],
        createFromObject: [],
        createFromDelta: [],
        updateFromDelta: [],
        linkFromObject: [],
        linkFromCreateDTO: [],
        linkFromDelta: [],
    };
    for (const field of create) {
        result.createFromObject.push(`${field.name}: item.${field.name}`);
        result.createFromDelta.push(`${field.name}: item.input.${field.name}`);
        if (field.kind !== 'id') {
            result.updateFromDelta.push(`${field.name}: '${field.name}' in item.delta && item.delta['${field.name}'] ? item.delta['${field.name}'].new : undefined`);
        }
    }
    for (const field of nullableFieldsForLaterLinking) {
        result.createFromObject.push(`${field.name}: null`);
        result.createFromDelta.push(`${field.name}: null`);
        if (field.kind !== 'id') {
            result.updateFromDelta.push(`${field.name}: '${field.name}' in item.delta  ? null : undefined`);
        }
        result.fieldsToBeLinkedLater.push(field.name);
        result.linkFromObject.push(`${field.name}: item.${field.name}`);
        result.linkFromCreateDTO.push(`${field.name}: item.input.${field.name}`);
        result.linkFromDelta.push(`${field.name}: '${field.name}' in item.delta && item.delta['${field.name}'] ? item.delta['${field.name}'].new : undefined`);
    }
    return result;
}
//# sourceMappingURL=convert-functions.generator.js.map