"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.generateModelViewService = generateModelViewService;
exports.generateModelViewService_NoRepository = generateModelViewService_NoRepository;
const Generator = __importStar(require("@postxl/generator"));
const model_type_generator_1 = require("../types/generators/model-type.generator");
function generateModelViewService({ model, context }) {
    const imports = Generator.ImportGenerator.from(model.view.service.location);
    imports.addType(model.types);
    imports.addType(model.types.viewModel);
    imports.addType(context.types.authorizationContext);
    imports.addType(model.types.id);
    imports.addType(model.types.filter.type);
    imports.addType(model.types.filter.fieldType);
    imports.add(model.types.filter.config);
    imports.add(context.view.service);
    imports.add(context.actions.authorization.policy);
    // Import StringFilter and SortState types
    imports.addImport({
        items: [Generator.toTypeName('StringFilter'), Generator.toTypeName('SortState')],
        from: context.types.location,
    });
    imports.addImport({
        items: [
            Generator.toFunctionName('applyRangeFilters'),
            Generator.toFunctionName('applyStringFilters'),
            Generator.toFunctionName('getMatches'),
            Generator.toFunctionName('getOptions'),
            Generator.toFunctionName('formatAndSortOptions'),
            Generator.toFunctionName('compareFieldValues'),
        ],
        from: Generator.toBackendModuleLocation('@view/query.utils'),
    });
    imports.addImport({
        items: [Generator.toFunctionName('omit')],
        from: Generator.toBackendModuleLocation('@postxl/utils'),
    });
    // Import PaginatedResult type from shared @types for tRPC type inference compatibility
    imports.addImport({
        items: [Generator.toTypeName('PaginatedResult')],
        from: context.types.location,
    });
    // Import paginate from query.utils (same directory)
    imports.addImport({
        items: [Generator.toFunctionName('paginate')],
        from: Generator.toBackendModuleLocation('@view/query.utils'),
    });
    if (model.repository.kind === 'NoRepository') {
        return generateModelViewService_NoRepository({ model, context, imports });
    }
    else {
        return generateModelViewService_HasRepository({ model, context, imports });
    }
}
function generateModelViewService_HasRepository({ model, context, imports, }) {
    if (model.repository.kind === 'NoRepository') {
        return '';
    }
    imports.add(model.repository);
    const filteringMethods = generateFilteringMethods({ model, context, imports });
    return `
import { ForbiddenException, Inject, Injectable, forwardRef } from '@nestjs/common'

${imports.generate()}

@Injectable()
export class ${model.view.service.name} {
  constructor(
    public readonly data: ${model.repository.name},
    private readonly authorizationPolicyService: ${context.actions.authorization.policy.name},
    @Inject(forwardRef(() => ${context.view.service.name}))
        private readonly viewService: ${context.view.service.name},
  ) {}

  /**
   * Transforms a ${model.userFriendlyName} to its view model.
   * Extend this method to add computed fields (e.g., fetching related data asynchronously).
   */
  public async ${model.view.service.toViewModel}(item: ${model.types.name}): Promise<${model.types.viewModel.name}> {
    return Promise.resolve(item) // NOSONAR - async to allow overrides with async operations
  }

  /**
   * Transforms an array of ${model.userFriendlyNamePlural} to view models.
   */
  public async ${model.view.service.toViewModels}(items: ${model.types.name}[]): Promise<${model.types.viewModel.name}[]> {
    return Promise.all(items.map(item => this.${model.view.service.toViewModel}(item)))
  }

  /**
   * Transforms a map of ${model.userFriendlyNamePlural} to view models.
   */
  public async ${model.view.service.toViewModelsMap}(items: Map<${model.types.id.name}, ${model.types.name}>): Promise<Map<${model.types.id.name}, ${model.types.viewModel.name}>> {
    const result = new Map<${model.types.id.name}, ${model.types.viewModel.name}>()
    for (const [id, item] of items.entries()) {
      result.set(id, await this.${model.view.service.toViewModel}(item))
    }
    return result
  }

  /**
   * Returns the raw ${model.userFriendlyName} with the given id or null if it does not exist.
   * Raw: The ${model.userFriendlyName} only contains linked Ids, not the linked items themselves.
   */
  public async get({ id, authorization }: { id: ${model.types.id.name}; authorization: AuthorizationContext }): Promise<${model.types.viewModel.name} | null> {
    const item = await this.data.get(id)
    if (!item) {
      return null
    }

    await this.authorize({ item, authorization })

    return await this.${model.view.service.toViewModel}(item)
  }

  ${generateGetByKeyMethod({ model })}

  /**
   * Returns a map of all ${model.userFriendlyNamePlural}.
   */
  public async getAll(authorization: AuthorizationContext): Promise<Map<${model.types.id.name}, ${model.types.viewModel.name}>> {
        const result = await this.data.getAll()

        // Removing non-authorized items from the result.
        for (const [key, value] of result.entries()) {
          try {
            await this.authorize({ item: value, authorization })
          } catch (error) {
            if (error instanceof ForbiddenException) {
              result.delete(key)
              continue
            }
            throw error
          }
        }

        return await this.${model.view.service.toViewModelsMap}(result)
  }

  /**
   * Returns a list of all ${model.userFriendlyNamePlural}.
   */
  public async getList(authorization: AuthorizationContext): Promise<${model.types.viewModel.name}[]> {
        const result: ${model.types.name}[] = []

        // Removing non-authorized items from the result.
        for (const item of (await this.data.getAll()).values()) {
          try {
            await this.authorize({ item, authorization })
            result.push(item)
          } catch (error) {
            if (error instanceof ForbiddenException) {
              continue
            }
            throw error
          }
        }

        return await this.${model.view.service.toViewModels}(result)
  }

  ${filteringMethods}

  ${generateAuthorizeMethod({ model, context })}
}
`;
}
function generateModelViewService_NoRepository({ model, context, imports, }) {
    if (model.repository.kind !== 'NoRepository') {
        return '';
    }
    // imports.addImports({
    //   [model.types.importPath]: [toAnnotatedTypeName(toTypeName('User'))],
    // })
    return `
/* eslint-disable @typescript-eslint/no-unused-vars */
import z from 'zod'

import { ForbiddenException, Inject, Injectable, forwardRef } from '@nestjs/common'

${imports.generate()}

@Injectable()
export class ${model.view.service.name} {
  constructor(
    private readonly authorizationPolicyService: ${context.actions.authorization.policy.name},
    @Inject(forwardRef(() => ${context.view.service.name}))
        private readonly viewService: ${context.view.service.name},
  ) {}

  /**
   * Transforms a ${model.userFriendlyName} to its view model.
   * TODO: Extend this method to add computed fields (e.g., fetching related data asynchronously).
   */
  public async ${model.view.service.toViewModel}(item: ${model.types.name}): Promise<${model.types.viewModel.name}> {
    return Promise.resolve(item) // NOSONAR - async to allow overrides with async operations
  }

  /**
   * Transforms an array of ${model.userFriendlyNamePlural} to view models.
   */
  public async ${model.view.service.toViewModels}(items: ${model.types.name}[]): Promise<${model.types.viewModel.name}[]> {
    return Promise.all(items.map(item => this.${model.view.service.toViewModel}(item)))
  }

  /**
   * Transforms a map of ${model.userFriendlyNamePlural} to view models.
   */
  public async ${model.view.service.toViewModelsMap}(items: Map<${model.types.id.name}, ${model.types.name}>): Promise<Map<${model.types.id.name}, ${model.types.viewModel.name}>> {
    const result = new Map<${model.types.id.name}, ${model.types.viewModel.name}>()
    for (const [id, item] of items.entries()) {
      result.set(id, await this.${model.view.service.toViewModel}(item))
    }
    return result
  }

  /**
   * Returns the raw ${model.userFriendlyName} with the given id or null if it does not exist.
   * Raw: The ${model.userFriendlyName} only contains linked Ids, not the linked items themselves.
  */
  public async get({ id, authorization }: { id: ${model.types.id.name}; authorization: AuthorizationContext }): Promise<${model.types.viewModel.name} | null> {
    // TODO: Implement this method.
    const item = await Promise.resolve(null) // NOSONAR - adhere to the same interface

    if (!item) {
      return null
    }

    await this.authorize({ item, authorization })

    return await this.${model.view.service.toViewModel}(item)
  }

  /**
   * Returns the ${model.userFriendlyName} with the given key or null if it does not exist.
   * TODO: Implement this method.
   */
  public async getByKey({ key: _key, authorization: _authorization }: { key: string; authorization: AuthorizationContext }): Promise<${model.types.viewModel.name} | null> {
    // TODO: Implement this method.
    return Promise.resolve(null) // NOSONAR - adhere to the same interface
  }

  /**
   * Returns a map of all ${model.userFriendlyNamePlural}.
   */
  public async getAll(authorization: AuthorizationContext): Promise<Map<${model.types.id.name}, ${model.types.viewModel.name}>> {
        // TODO: Implement this method.
        const result = await Promise.resolve(new Map()) // NOSONAR - adhere to the same interface

        // Removing non-authorized items from the result.
        for (const [key, value] of result.entries()) {
          try {
            await this.authorize({ item: value, authorization })
          } catch (error) {
            if (error instanceof ForbiddenException) {
              result.delete(key)
              continue
            }
            throw error
          }
        }

        return await this.${model.view.service.toViewModelsMap}(result)
  }

  /**
   * Returns a filtered and sorted list of ${model.userFriendlyNamePlural}.
   */
  public async getFiltered({ filters, sort, authorization }: {
    filters: ${model.types.filter.type.name}
    sort?: SortState<${model.types.filter.fieldType.name}>
    authorization: AuthorizationContext
  }): Promise<${model.types.viewModel.name}[]> {
    // TODO: Implement this method.
    return this.getList(authorization)
  }

  /**
   * Returns a list of all ${model.userFriendlyNamePlural}.
   */
  public async getList(_authorization: AuthorizationContext): Promise<${model.types.viewModel.name}[]> {
    // TODO: Implement this method.
    return []
  }

  /**
   * Returns the filter options for a given field.
   */
  public async getFilterOptions({
    field,
    filters,
    authorization: _authorization,
  }: {
    field: ${model.types.filter.field.name}
    filters: ${model.types.filter.type.name}
    authorization: AuthorizationContext
  }): Promise<any> {
    // TODO: Implement this method.
    return []
  }

  ${generateAuthorizeMethod({ model, context })}
}`;
}
function generateAuthorizeMethod({ model }) {
    return `/**
  * Authorizes access to ${model.userFriendlyName}.
  */
  public async authorize({ item, authorization }: { item: ${model.types.name}; authorization: AuthorizationContext }): Promise<void> {
    const isAllowed = this.authorizationPolicyService.canRead({
      authorization,
      modelName: '${model.name}',
      id: item.id,
      item,
    })
    if (!isAllowed) {
      throw new ForbiddenException('Forbidden')
    }
    return Promise.resolve() // NOSONAR - adhere to the same interface
  }`;
}
function generateGetByKeyMethod({ model }) {
    const keyFieldName = model.keyField.name;
    const isKeyFieldUnique = model.keyField.isUnique;
    // If keyField is the id field, delegate to get() method
    if (keyFieldName === model.idField.name) {
        return `/**
   * Returns the ${model.userFriendlyName} with the given key or null if it does not exist.
   * The key field for this model is '${keyFieldName}' (the id field).
   */
  public async getByKey({ key, authorization }: { key: string; authorization: AuthorizationContext }): Promise<${model.types.viewModel.name} | null> {
    return this.get({ id: key as ${model.types.id.name}, authorization })
  }`;
    }
    // For unique keyFields, use the repository's getByKey method
    if (isKeyFieldUnique) {
        return `/**
   * Returns the ${model.userFriendlyName} with the given key or null if it does not exist.
   * The key field for this model is '${keyFieldName}' (unique).
   */
  public async getByKey({ key, authorization }: { key: string; authorization: AuthorizationContext }): Promise<${model.types.viewModel.name} | null> {
    const item = await this.data.getByKey(key)
    if (!item) {
      return null
    }
    await this.authorize({ item, authorization })
    return await this.${model.view.service.toViewModel}(item)
  }`;
    }
    // Non-unique keyField - use repository's getByKey with TODO comment about scoping
    return `/**
   * Returns the first ${model.userFriendlyName} with the given key or null if it does not exist.
   * The key field for this model is '${keyFieldName}' (non-unique).
   *
   * TODO: The keyField '${keyFieldName}' is not unique. This means multiple entities can have the same key.
   * In practice, keys are often unique within a scope (e.g., within a parent entity).
   * Consider extending this method to accept additional scope parameters to filter by parent entity.
   * For example: getByKey({ key, parentId, user }) where parentId identifies the scope.
   *
   * Current implementation returns the first matching entity found, which may not be deterministic.
   * This should be replaced with a properly scoped lookup once the scoping rules are defined.
   */
  public async getByKey({ key, authorization }: { key: string; authorization: AuthorizationContext }): Promise<${model.types.viewModel.name} | null> {
    // TODO: Implement proper scoped lookup. See method documentation for details.
    const item = await this.data.getByKey(key)
    if (!item) {
      return null
    }
    try {
      await this.authorize({ item, authorization })
      return await this.${model.view.service.toViewModel}(item)
    } catch (error) {
      if (error instanceof ForbiddenException) {
        // User not authorized for this item
        return null
      }
      throw error
    }
  }`;
}
function generateFilteringMethods({ model, context, imports, }) {
    const filterFields = [];
    const usedMatchMethods = new Set();
    // Collect all relation fields (including nested in DU)
    const relationFields = collectRelationFields(model, context);
    // Collect all filter field keys for building accessors map
    const filterFieldKeys = collectFilterFieldKeys(model);
    // Generate accessors map definition (placed outside the filter loop for efficiency)
    const accessorsMapInit = `const accessors = new Map<string, (item: any) => any>([
      ${filterFieldKeys.map((key) => `['${key}', this.getFieldAccessor('${key}')]`).join(',\n      ')}
    ])`;
    // Add global search condition at the beginning (pass referencedItemsMap for relation field label resolution + accessors for nested fields)
    const globalSearchCondition = `
      if (filters.globalSearch) {
        if (!matchGlobalSearch(filters.globalSearch, item, Object.values(${model.types.filter.config.name}), referencedItemsMap, accessors)) {
          return false
        }
      }`;
    filterFields.push(globalSearchCondition);
    usedMatchMethods.add('matchGlobalSearch');
    // Generate filter conditions for all fields, including nested DU fields
    for (const field of model.fields.values()) {
        const conditions = getFilterConditionsForField({
            field,
            model,
            usedMatchMethods,
            relationFields,
        });
        filterFields.push(...conditions);
    }
    if (usedMatchMethods.size > 0) {
        imports.addImport({
            items: Array.from(usedMatchMethods).map((m) => Generator.toFunctionName(m)),
            from: Generator.toBackendModuleLocation('@view/query.utils'),
        });
    }
    // Import hasTextMatching only when there are relation fields (to avoid unused import warning)
    if (relationFields.length > 0) {
        imports.addImport({
            items: [Generator.toFunctionName('hasTextMatching')],
            from: Generator.toBackendModuleLocation('@view/query.utils'),
        });
    }
    // Generate the relation maps helper methods
    const allRelationKeys = relationFields.map((r) => `'${r.filterKey}'`).join(', ');
    const relationMapsMethods = relationFields.length > 0
        ? `
  /**
   * Determines which relation maps need to be loaded based on active filters and sort fields.
   * Maps are needed for globalSearch, text matching (contains/startsWith/endsWith/exclude), or sorting relation fields.
   */
  private getRequiredRelationMaps(
    filters: ${model.types.filter.type.name},
    sort?: SortState<${model.types.filter.fieldType.name}>,
  ): Set<string> {
    const relationFields = new Set([${allRelationKeys}])

    // If globalSearch is set, we need all relation maps to search labels
    if (filters.globalSearch) {
      return relationFields
    }

    const required = new Set<string>()
${relationFields.map((r) => `    if (hasTextMatching(filters.${r.filterKey})) {\n      required.add('${r.filterKey}')\n    }`).join('\n')}

    // Also include relation fields that are being sorted (to resolve labels for sorting)
    if (sort) {
      for (const { field } of sort) {
        if (relationFields.has(field)) {
          required.add(field)
        }
      }
    }

    return required
  }

  /**
   * Loads the required relation maps based on active filters and sort fields.
   * @param additionalField - Optional field to always include (used in getFilterOptions to load the field's map for labels)
   *
   * Note: Errors from individual view service calls are caught and logged, allowing
   * the query to continue with degraded functionality (missing labels) rather than failing entirely.
   */
  private async loadRequiredRelationMaps(
    filters: ${model.types.filter.type.name},
    sort: SortState<${model.types.filter.fieldType.name}> | undefined,
    authorization: AuthorizationContext,
    additionalField?: ${model.types.filter.fieldType.name},
  ): Promise<Map<string, Map<string, any> | undefined>> {
    const requiredMaps = this.getRequiredRelationMaps(filters, sort)
    const referencedItemsMap = new Map<string, Map<string, any> | undefined>()
    const viewServiceMap = this.viewService as unknown as Record<
      string,
      { getAll: (authorization: AuthorizationContext) => Promise<Map<string, unknown>> } | undefined
    >
    ${relationFields
            .map((r) => `if (requiredMaps.has('${r.filterKey}') || additionalField === '${r.filterKey}') {\n      await this.loadRelationMap(\n        referencedItemsMap,\n        '${r.filterKey}',\n        async () => viewServiceMap.${r.viewServiceName}?.getAll(authorization),\n      )\n    }`)
            .join('\n    ')}
    return referencedItemsMap
  }

  /**
   * Loads a single relation map and degrades gracefully on missing permissions.
   */
  private async loadRelationMap(
    referencedItemsMap: Map<string, Map<string, any> | undefined>,
    field: ${model.types.filter.fieldType.name},
    load: () => Promise<Map<string, unknown> | undefined>,
  ): Promise<void> {
    try {
      const data = await load()
      referencedItemsMap.set(field, data)
    } catch (error) {
      if (error instanceof ForbiddenException) {
        // Missing permission for related model: continue without labels.
        referencedItemsMap.set(field, undefined)
      } else {
        throw error
      }
    }
  }`
        : '';
    // Generate the call to load relation maps
    const loadRelationMapsCall = relationFields.length > 0
        ? `const referencedItemsMap = await this.loadRequiredRelationMaps(filters, sort, authorization)`
        : `const referencedItemsMap = new Map<string, Map<string, any> | undefined>()`;
    const loadRelationMapsCallForOptions = relationFields.length > 0
        ? `const referencedItemsMap = await this.loadRequiredRelationMaps(reducedFilters, undefined, authorization, field)`
        : `const referencedItemsMap = new Map<string, Map<string, any> | undefined>()`;
    const defaultSort = model.defaultSort
        .map((fieldName) => `{ field: '${fieldName}' as ${model.types.filter.fieldType.name}, direction: 'asc' as const }`)
        .join(', ');
    return `
  /**
   * Returns a filtered and sorted list of ${model.userFriendlyNamePlural}.
   * For really large datasets, consider implementing server-side filtering in the repository instead.
   */
  public async getFiltered({ filters, sort, authorization }: {
    filters: ${model.types.filter.type.name}
    sort?: SortState<${model.types.filter.fieldType.name}>
    authorization: AuthorizationContext
  }): Promise<${model.types.viewModel.name}[]> {
    const allItems = await this.getList(authorization)
    ${loadRelationMapsCall}
    const filteredItems = this.filter${model._conjugated.PascalCasePlural}(allItems, filters, referencedItemsMap)
    return this.sort${model._conjugated.PascalCasePlural}(filteredItems, sort, referencedItemsMap)
  }

  /* TODO: Issues to potentially address later (for large datasets):
    - Paginate filter options in dropdowns (currently all options render at once)
    - Cache filtered/sorted results across pagination requests
    - Move filtering/sorting to database level to avoid loading full dataset in memory
    - Resolve relation labels in backend to reduce frontend computation (not to critical due to using maps there now though) */
  /**
   * Returns a paginated, filtered and sorted list of ${model.userFriendlyNamePlural}.
   * Calls getFiltered first, then paginates the result.
   * @param cursor - Page number (1-indexed). tRPC will pass this from getNextPageParam.
   * @param limit - Items per page (default: 100)
   */
  public async getFilteredPaginated({ filters, sort, cursor, limit, authorization }: {
    filters: ${model.types.filter.type.name}
    sort?: SortState<${model.types.filter.fieldType.name}>
    cursor: number
    limit: number
    authorization: AuthorizationContext
  }): Promise<PaginatedResult<${model.types.viewModel.name}>> {
    const filteredAndSorted = await this.getFiltered({ filters, sort, authorization })
    return paginate(filteredAndSorted, cursor, limit)
  }

  /**
   * Returns the filter options for a given field.
   * For really large datasets, consider implementing server-side filtering in the repository instead.
   *
   * SECURITY NOTE: Options are generated from user-accessible rows only (via getList).
   * However, for relation fields, labels are resolved from separately authorized data.
   * If stricter isolation is needed, override this method.
   */
  public async getFilterOptions({
    field,
    filters,
    authorization,
  }: {
    field: ${model.types.filter.fieldType.name}
    filters: ${model.types.filter.type.name}
    authorization: AuthorizationContext
  }): Promise<{ value: string; label: string; hasMatches: boolean }[]> {
    const allItems = await this.getList(authorization)
    const reducedFilters = omit(filters, field)
    ${loadRelationMapsCallForOptions}
    const filteredItems = this.filter${model._conjugated.PascalCasePlural}(allItems, reducedFilters, referencedItemsMap)

    const config = ${model.types.filter.config.name}[field]
    const accessor = this.getFieldAccessor(field)

    // If we are getting options for a number/date field, we want to respect the range filter (so options outside the range are grayed out), but still ignore the values filter (so we can see all options).
    const itemsWithRange = applyRangeFilters(filteredItems, accessor, config, filters)

    // Get referenced items for the current field (for relation fields)
    const referencedItems = referencedItemsMap.get(field)
    // For string filters, also apply text filters (contains, startsWith, endsWith, exclude) to grey out non-matching options
    let itemsWithStringFilters = itemsWithRange
    if (config.valueType === 'string') {
      const filter = filters[config.filterKey] as StringFilter | undefined
      itemsWithStringFilters = applyStringFilters(itemsWithRange, accessor, filter, config.getLabel, referencedItems)
    }

    const { matches, options } = {
      matches: getMatches(itemsWithStringFilters, accessor),
      options: getOptions(allItems, accessor, config, referencedItems),
    }

    return formatAndSortOptions(options, matches)
  }

  /**
   * Helper method to filter ${model.userFriendlyNamePlural} based on provided filters.
   */
  private filter${model._conjugated.PascalCasePlural}(
    ${model._conjugated.camelCasePlural}: ${model.types.viewModel.name}[],
    filters: ${model.types.filter.type.name},
    referencedItemsMap: Map<string, Map<string, any> | undefined>,
  ): ${model.types.viewModel.name}[] {
    // Build accessors map once (for global search with nested fields like DU members)
    ${accessorsMapInit}

    return ${model._conjugated.camelCasePlural}.filter(/* NOSONAR */ (item) => {
    ${filterFields.join('')}
      return true
    })
  }

  ${relationMapsMethods}

  /**
   * Sorts items based on the provided sort configuration.
   * Supports multi-column sorting (sorted in array order).
   * Defaults to model defaultSort when no explicit sort is provided.
   * Always appends id ascending as a stable tiebreaker when missing.
   * For relation fields, sorts by the displayed label instead of the raw ID.
   */
  private sort${model._conjugated.PascalCasePlural}(
    items: ${model.types.viewModel.name}[],
    sort: SortState<${model.types.filter.fieldType.name}> | undefined,
    referencedItemsMap: Map<string, Map<string, any> | undefined>,
  ): ${model.types.viewModel.name}[] {
    const defaultSort: SortState<${model.types.filter.fieldType.name}> = [${defaultSort}]
    const baseSort = sort && sort.length > 0 ? sort : defaultSort
    const effectiveSort = baseSort.some(({ field }) => field === 'id')
      ? baseSort
      : [...baseSort, { field: 'id' as ${model.types.filter.fieldType.name}, direction: 'asc' as const }]
    return items.toSorted((a, b) => {
      for (const { field, direction } of effectiveSort) {
        const accessor = this.getFieldAccessor(field)
        const config = ${model.types.filter.config.name}[field]
        const result = compareFieldValues(accessor(a), accessor(b), direction, config, referencedItemsMap.get(field))
        if (result !== 0) return result
      }
      return 0
    })
  }

  ${generateGetFieldAccessorMethod({ model })}
`;
}
function generateGetFieldAccessorMethod({ model }) {
    const accessors = [];
    // Generate accessor cases for all fields, including nested DU fields
    for (const field of model.fields.values()) {
        const cases = generateFieldAccessorCases({
            field,
            model,
        });
        accessors.push(...cases);
    }
    const defaultAccessor = `return (item: ${model.types.viewModel.name}) => (item as any)[field]`;
    // Add default case
    accessors.push(`      default:
        ${defaultAccessor}`);
    return `/**
   * Returns an accessor function for the given filter field.
   */
  private getFieldAccessor(field: ${model.types.filter.fieldType.name}): (item: ${model.types.viewModel.name}) => any {
  ${accessors.length > 1
        ? `switch (field) {
    ${accessors.join('\n')}
    }`
        : defaultAccessor}
  }`;
}
function generateFieldAccessorCases(params) {
    const { field, model, duParentFieldName, duMemberType } = params;
    const result = [];
    // Handle discriminated union: add the discriminant field plus all nested fields
    if (field.kind === 'discriminatedUnion') {
        // Add the discriminant field itself
        const caseString = generateFieldAccessorCase({ field, model, duParentFieldName, duMemberType });
        if (caseString) {
            result.push(caseString);
        }
        // Add common fields
        for (const commonField of field.commonFields.values()) {
            result.push(...generateFieldAccessorCases({
                field: commonField,
                model,
                duParentFieldName: field.name,
            }));
        }
        // Add member-specific fields
        for (const member of field.members.values()) {
            for (const memberField of member.fields.values()) {
                result.push(...generateFieldAccessorCases({
                    field: memberField,
                    model,
                    duParentFieldName: field.name,
                    duMemberType: member.type,
                }));
            }
        }
    }
    else {
        // Regular field
        const caseString = generateFieldAccessorCase({ field, model, duParentFieldName, duMemberType });
        if (caseString) {
            result.push(caseString);
        }
    }
    return result;
}
function generateFieldAccessorCase({ field, model, duParentFieldName, duMemberType, }) {
    // For discriminated union fields at top level, we need to return the discriminant (type) value
    if (field.kind === 'discriminatedUnion' && !duParentFieldName) {
        return `      case '${field.name}':
        return (item: ${model.types.viewModel.name}) => item.${field.name}?.type`;
    }
    // Skip generating a case for simple top-level fields - the default case handles them
    if (!duParentFieldName) {
        return null;
    }
    const caseKey = `'${(0, model_type_generator_1.getFilterFieldName)(field.name, duParentFieldName)}'`;
    let accessorExpression;
    if (duMemberType) {
        // Member-specific field: only return value if the member type matches
        accessorExpression = `item.${duParentFieldName}?.type === '${duMemberType}' ? item.${duParentFieldName}?.${field.name} : undefined`;
    }
    else {
        // Common field in DU
        accessorExpression = `item.${duParentFieldName}?.${field.name}`;
    }
    return `      case ${caseKey}:
        return (item: ${model.types.viewModel.name}) => ${accessorExpression}`;
}
/**
 * Collects all relation fields from a model (including nested in DU).
 */
function collectRelationFields(model, context) {
    const relationFields = [];
    function processField(field, duParentFieldName) {
        if (field.kind === 'relation') {
            const referencedModel = context.models.get(field.referencedModelName);
            if (referencedModel) {
                const filterKey = (0, model_type_generator_1.getFilterFieldName)(field.name, duParentFieldName);
                relationFields.push({
                    fieldName: duParentFieldName ? `${duParentFieldName}_${field.name}` : field.name,
                    filterKey,
                    viewServiceName: Generator.toVariableName(referencedModel._conjugated.camelCasePlural),
                    labelField: referencedModel.labelField.name,
                });
            }
        }
        else if (field.kind === 'discriminatedUnion') {
            // Process common fields
            for (const commonField of field.commonFields.values()) {
                processField(commonField, field.name);
            }
            // Process member-specific fields
            for (const member of field.members.values()) {
                for (const memberField of member.fields.values()) {
                    processField(memberField, field.name);
                }
            }
        }
    }
    for (const field of model.fields.values()) {
        processField(field);
    }
    return relationFields;
}
/**
 * Collects all filter field keys from a model (including nested DU fields).
 */
function collectFilterFieldKeys(model) {
    const filterKeys = [];
    function processField(field, duParentFieldName) {
        const filterKey = (0, model_type_generator_1.getFilterFieldName)(field.name, duParentFieldName);
        filterKeys.push(filterKey);
        if (field.kind === 'discriminatedUnion') {
            // Process common fields
            for (const commonField of field.commonFields.values()) {
                processField(commonField, field.name);
            }
            // Process member-specific fields
            for (const member of field.members.values()) {
                for (const memberField of member.fields.values()) {
                    processField(memberField, field.name);
                }
            }
        }
    }
    for (const field of model.fields.values()) {
        processField(field);
    }
    return filterKeys;
}
/**
 * Returns all filter conditions for a field, including nested DU fields.
 */
function getFilterConditionsForField(params) {
    const { field, model, usedMatchMethods, relationFields, duParentFieldName, duMemberType } = params;
    const result = [];
    // Handle discriminated union: add the discriminant field plus all nested fields
    if (field.kind === 'discriminatedUnion') {
        // Add the discriminant field itself
        result.push(generateFilterCondition({ field, model, usedMatchMethods, relationFields, duParentFieldName, duMemberType }));
        // Add common fields
        for (const commonField of field.commonFields.values()) {
            result.push(...getFilterConditionsForField({
                field: commonField,
                model,
                usedMatchMethods,
                relationFields,
                duParentFieldName: field.name,
            }));
        }
        // Add member-specific fields
        for (const member of field.members.values()) {
            for (const memberField of member.fields.values()) {
                result.push(...getFilterConditionsForField({
                    field: memberField,
                    model,
                    usedMatchMethods,
                    relationFields,
                    duParentFieldName: field.name,
                    duMemberType: member.type,
                }));
            }
        }
    }
    else {
        // Regular field
        result.push(generateFilterCondition(params));
    }
    return result;
}
/**
 * Generates a single filter condition for a field.
 */
function generateFilterCondition({ field, model, usedMatchMethods, relationFields, duParentFieldName, duMemberType, }) {
    const valueType = (0, model_type_generator_1.getFilterValueType)(field);
    let matchMethod = 'matchFilter';
    if (valueType === 'boolean') {
        matchMethod = 'matchBooleanFilter';
    }
    else if (valueType === 'number') {
        matchMethod = 'matchNumberFilter';
    }
    else if (valueType === 'date') {
        matchMethod = 'matchDateFilter';
    }
    else if (valueType === 'string') {
        // Use StringFilter for all string-type fields (scalar, enum, id, relation, discriminatedUnion)
        matchMethod = 'matchStringFilter';
    }
    usedMatchMethods.add(matchMethod);
    // Build the accessor path for the field value
    let accessor;
    if (duParentFieldName) {
        if (duMemberType) {
            // Member-specific field: only check if the member type matches
            accessor = `item.${duParentFieldName}?.type === '${duMemberType}' ? item.${duParentFieldName}?.${field.name} : null`;
        }
        else {
            // Common field in DU
            accessor = `item.${duParentFieldName}?.${field.name}`;
        }
    }
    else if (field.kind === 'discriminatedUnion') {
        // DU field at top level: filter by the discriminant (type) value
        accessor = `item.${field.name}?.type`;
    }
    else {
        accessor = `item.${field.name}`;
    }
    const filterKey = (0, model_type_generator_1.getFilterFieldName)(field.name, duParentFieldName);
    // For JSON fields, stringify the value before passing to matchStringFilter
    if (field.kind === 'json' && matchMethod === 'matchStringFilter') {
        const jsonAccessor = `(() => { const v = ${accessor}; if (v == null) return null; try { return typeof v === 'string' ? v : JSON.stringify(v); } catch { return null; } })()`;
        return `
      if (!${matchMethod}(${jsonAccessor}, filters.${filterKey})) {
        return false
      }`;
    }
    // For relation fields, resolve ID to label using config.getLabel for text filter matching
    if (field.kind === 'relation' && matchMethod === 'matchStringFilter') {
        const relationInfo = relationFields.find((r) => r.filterKey === filterKey);
        if (relationInfo) {
            return `
      if (!${matchMethod}(${accessor}, filters.${filterKey}, ${model.types.filter.config.name}.${filterKey}.getLabel?.(${accessor}, referencedItemsMap.get('${filterKey}')))) {
        return false
      }`;
        }
    }
    return `
      if (!${matchMethod}(${accessor}, filters.${filterKey})) {
        return false
      }`;
}
//# sourceMappingURL=model-view-service.generator.js.map