"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.generateDetectDeltaFunctions = generateDetectDeltaFunctions;
const Generator = __importStar(require("@postxl/generator"));
function generateDetectDeltaFunctions(context) {
    const { import: { detectDelta }, } = context;
    const dto = context.types.dto;
    const generic = detectDelta.types.generic;
    const { create, update, errors, unchanged, delta_Fields, modelType } = generic;
    const imports = Generator.ImportGenerator
        //
        .from(detectDelta._utils.getDelta.location)
        .addType(dto.genericModel)
        .addType(dto.idType)
        .addType(dto.update)
        .addType(delta_Fields)
        .addType(modelType)
        .addType(errors.invalidReference.type)
        .addType(errors.invalidType.type)
        .addType(errors.missingField.type)
        .addType(errors.nonUnique.type)
        .addType(errors.isRequiredDependency.type);
    return /* ts */ `
import { ExhaustiveSwitchCheck } from '@postxl/utils'
${imports.generate()}

type Delta_Result<Model extends GenericModel<ID>, ID extends IdType> =
  | Delta_Result_Create<Model, ID>
  | Delta_Result_Update<Model, ID>
  | Delta_Result_Delete<Model, ID>
  | { type: '${unchanged.discriminant}' }

type Delta_Result_Create<Model extends GenericModel<ID>, ID extends IdType> = { type: '${create.discriminant}'; newItem: Model }
type Delta_Result_Update<Model extends GenericModel<ID>, ID extends IdType> = {
  type: '${update.discriminant}'
  delta: ${delta_Fields.name}<Model, ID>
  existingItem: Model
  updatedItem: Model
}
type Delta_Result_Delete<Model extends GenericModel<ID>, ID extends IdType> = { type: '${generic.delete.discriminant}'; existingItem: Model }

export type ${detectDelta._utils.validation.params.name}<Model extends ${dto.genericModel.name}<ID>, ID extends IdType, ModelErrors> = {
  create: (params: Omit<Delta_Result_Create<Model, ID>, 'type'>) => Promise<ModelErrors[]>
  update: (params: Omit<Delta_Result_Update<Model, ID>, 'type' | 'delta'>) => Promise<ModelErrors[]>
  delete: (params: Omit<Delta_Result_Delete<Model, ID>, 'type'>) => Promise<ModelErrors[]>
}

/**
 * Detects changes for a model and returns the result as a Delta_Model object.
 */
export async function ${detectDelta._utils.detectModelDelta.name}<Model extends ${dto.genericModel.name}<ID>, ID extends ${dto.idType.name}, ModelErrors>({
  items,
  getId,
  getDelta,
  validate
}: {
  items?: Model[]
  getId: (id: ID) => Promise<Model | null>
  getDelta: (args: { item: Model; existingItem: Model }) => ${delta_Fields.name}<Model, ID>
  validate: ${detectDelta._utils.validation.params.name}<Model, ID, ModelErrors>
}): Promise<${modelType.name}<Model, ID, ModelErrors>[]> {
  const upsertResult: ${modelType.name}<Model, ID, ModelErrors>[] = []

  if (items === undefined || items.length === 0) {
    return upsertResult
  }

  for (const item of items) {
    const deltaResult = await determineDeltaResult({ item, getId, getDelta })
    const errors = deltaResult.type === 'unchanged' ? [] : await validate[deltaResult.type](deltaResult as any)

    if (errors.length !== 0) {
      upsertResult.push({ type: 'errors', input: item, errors })
      continue
    }
    switch (deltaResult.type) {
      case '${create.discriminant}':
        upsertResult.push({ type: '${create.discriminant}', input: item, createDto: item })
        break

      case '${update.discriminant}':
        upsertResult.push({
          type: '${update.discriminant}',
          input: item,
          delta: deltaResult.delta,
          updateDto: deltaToUpdateDtoFunction({ id: item.id, delta: deltaResult.delta }),
        })
        break

      case '${generic.delete.discriminant}':
        upsertResult.push({ type: 'errors', input: item, errors })
        break

      case '${unchanged.discriminant}': {
        upsertResult.push({ type: '${unchanged.discriminant}', input: item })
        break
      }

      default:
        throw new ExhaustiveSwitchCheck(deltaResult)
    }
  }

  return upsertResult
}

/**
 * Generic function to determine what action (create/update/delete/unchanged) to take with an item
 * In case the type is update it also includes the delta for a given item. In case of delete, it also includes
 * the existing item.
 *
 * **Important**: The function does not implement any logic to automatically determine if an item should be deleted.
 * Implement this logic based on the business requirements!
 */
async function determineDeltaResult<Model extends ${dto.genericModel.name}<ID>, ID extends ${dto.idType.name}>({
  item,
  getId,
  getDelta,
}: {
  item: Model
  getId: (id: ID) => Promise<Model | null>
  getDelta: ({ item, existingItem }: { item: Model; existingItem: Model }) => ${delta_Fields.name}<Model, ID>
}): Promise<Delta_Result<Model, ID>> {
  if (item.id === undefined || item.id === '') {
    return { type: '${create.discriminant}', newItem: item }
  }
  const existingItem = await getId(item.id)

  if (!existingItem) {
    return { type: '${create.discriminant}', newItem: item }
  }

  const delta = getDelta({ item, existingItem })

  if (Object.keys(delta).length > 0) {
    return { type: '${update.discriminant}', delta, existingItem, updatedItem: item }
  }

  // We do not have a default logic to identify deletions. Implement it here, e.g. using a
  // custom "Action" field in the Excel table. If it is set to "Delete", we can handle the delete here.
  // return {type: '${generic.delete.discriminant}', existingItem}

  // If we reach this point, we assume the item has not changed
  return { type: '${unchanged.discriminant}' }
}

export function ${detectDelta._utils.getDelta.name}<Model extends ${dto.genericModel.name}<ID>, ID extends ${dto.idType.name}>({
  item,
  existingItem,
  properties,
}: {
  item: Model
  existingItem: Model
  properties: (keyof Omit<Model, 'id'>)[]
}): ${delta_Fields.name}<Model, ID> {
  const delta: ${delta_Fields.name}<Model, ID> = {}

  for (const property of properties) {
    addDelta({ delta, existingItem, item, property })
  }

  return delta
}

/**
 * Adds a delta entry for a property to the given delta object if the property
 * of the item is different from the existing item
 */
function addDelta<Model extends ${dto.genericModel.name}<ID>, ID extends ${dto.idType.name}>({
  delta,
  existingItem,
  item,
  property,
}: {
  delta: ${delta_Fields.name}<Model, ID>
  existingItem: Model
  item: Model
  property: keyof Omit<Model, 'id'>
}): void {
  if (existingItem[property] === item[property]) {
    return
  }
  delta[property] = { old: existingItem[property], new: item[property] }
}

/**
 * Converts a Delta to an UpdateDto.
 */
function deltaToUpdateDtoFunction<Model extends ${dto.genericModel.name}<ID>, ID extends ${dto.idType.name}>({
  id,
  delta,
}: {
  id: ID
  delta: ${delta_Fields.name}<Model, ID>
}): ${dto.update.name}<Model, ID> {
  const updateDto = {
    id: id,
  } as UpdateDto<Model, ID>

  for (const [key, value] of Object.entries(delta)) {
    const updatedValue = value.new as Model[keyof Omit<Model, 'id'>]
    updateDto[key as keyof Omit<Model, 'id'>] = updatedValue
  }
  return updateDto
}

/**
 * Helper function that removes undefined values from an array.
 */
export function ${detectDelta._utils.keepErrors.name}<O, I extends O | undefined>(errors: I[]): O[] {
  return errors.filter((error) => error !== undefined) as O[]
}

/**
 * Validates that the model has all required fields.
 */
export function ${detectDelta._utils.validation.requiredFields.name}<
  Model extends ${dto.genericModel.name}<ID>,
  ID extends ${dto.idType.name},
  FieldName extends Extract<keyof Omit<Model, 'id'>, string>
>({
  item,
  fieldNames,
}: {
  item: Model
  fieldNames: FieldName[]
}): ${errors.missingField.type.name}<FieldName>[] {
  const errors: ${errors.missingField.type.name}<FieldName>[] = []
  for (const fieldName of fieldNames) {
    if (item[fieldName] === undefined) {
      errors.push({ error: '${errors.missingField.discriminant}', fieldName })
    }
  }
  if (errors.length > 0) {
    return errors
  } else {
    return []
  }
}

/**
 * Validates that the field is a reference to an existing item.
 */
export async function ${detectDelta._utils.validation.referenceField.name}<
  Model extends ${dto.genericModel.name}<ID>,
  ID extends ${dto.idType.name},
  RelatedModel extends ${dto.genericModel.name}<RelatedID>,
  RelatedID extends ${dto.idType.name},
  FieldName extends keyof Omit<Model, 'id'> & string,
  Value extends RelatedID,
  RelatedModelName extends string,
>({
  item,
  fieldName,
  relatedModel,
  getExistingItem,
}: {
  item: Partial<Model>
  fieldName: FieldName
  relatedModel: RelatedModelName
  getExistingItem: (id: RelatedID) => Promise<RelatedModel | null>
}): Promise<${errors.invalidReference.type.name}<RelatedModelName, FieldName, Value> | undefined> {
  const value: Value | undefined | null = item[fieldName] as Value | undefined | null
  if (value === undefined || value === null) {  
    return undefined
  }
  const existingItem = await getExistingItem(value)
  if (!existingItem) {
    return { error: '${errors.invalidReference.discriminant}', relatedModel, fieldName, value }
  }
  return undefined
}

/**
 * Validates that the field value is unique.
 */
export async function ${detectDelta._utils.validation.uniqueField.name}<
  Model extends ${dto.genericModel.name}<ID>,
  ID extends ${dto.idType.name},
  FieldName extends keyof Omit<Model, 'id'> & string,
  Value extends Model[FieldName],
>({
  item,
  fieldName,
  getExistingItem,
}: {
  item: Partial<Model>
  fieldName: FieldName
  getExistingItem: (fieldValue: Value) => Promise<Model | undefined>
}): Promise<${errors.nonUnique.type.name}<FieldName, Value> | undefined> {
  const value: Value | undefined = item[fieldName] as Value | undefined
  if (value === undefined) {
    return undefined
  }
  const existingItem = await getExistingItem(value)
  if (existingItem) {
    return { error: '${errors.nonUnique.discriminant}', fieldName, value }
  }
  return undefined
}

/**
 * Validates that the field value is an integer.
 */
export function ${detectDelta._utils.validation.int.name}<
  Model extends ${dto.genericModel.name}<ID>,
  ID extends ${dto.idType.name},
  FieldName extends keyof Omit<Model, 'id'> & string,
>({
  item,
  fieldName,
}: {
  item: Model
  fieldName: FieldName
}): ${errors.invalidType.type.name}<FieldName, 'Int', number> | undefined {
  const value: number | undefined | null = item[fieldName] as number | undefined | null
  //we handle undefined and null in other value checks so we can ignore them here
  if (value === undefined || value === null) {
    return undefined
  }

  if (value !== Math.floor(value)) {
    return { error: '${errors.invalidType.discriminant}', fieldName, typeExpected: 'Int', value }
  }
  return undefined
}

/**
 * Validates that the field is a reference to an existing item.
 */
export async function ${detectDelta._utils.validation.requiredDependency.name}<
  Model extends ${dto.genericModel.name}<ID>,
  ID extends ${dto.idType.name},
  RelatedModelName extends string,
  RelatedField extends string,
  RelatedModel extends ${dto.genericModel.name}<RelatedID>,
  RelatedID extends ${dto.idType.name},
>({
  item,
  relatedModel,
  relatedField,
  getReferencedItems,
}: {
  item: Model
  relatedModel: RelatedModelName
  relatedField: RelatedField
  getReferencedItems: (id: ID) => Promise<RelatedModel[]>
}): Promise<
  ${errors.isRequiredDependency.type.name}<RelatedModelName, RelatedField, RelatedModel, RelatedID> | undefined
> {
  const relatedItems = await getReferencedItems(item.id)
  if (relatedItems.length > 0) {
    return undefined
  }
  return { error: '${errors.isRequiredDependency.discriminant}', relatedModel, relatedField, relatedItems }
}`;
}
//# sourceMappingURL=detect-delta-functions.generator.js.map