"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateModelTableComponent = generateModelTableComponent;
const generator_1 = require("@postxl/generator");
const utils_1 = require("@postxl/utils");
const model_type_generator_1 = require("../../types/generators/model-type.generator");
function generateModelTableComponent({ model, context, }) {
    const filterFieldArrayName = `${model.types.filter.field.name}Array`;
    const imports = generator_1.ImportGenerator.from(model.table.componentWithoutFilters.location)
        .addType(model.types.viewModel)
        .addType(model.types.id)
        .addType(model.types.filter.type)
        .addType(model.types.filter.fieldType)
        .add(model.types.filter.config)
        .add({
        name: model.types.filter.field.name,
        alias: filterFieldArrayName,
        location: model.types.filter.field.location,
    })
        .addImport({ from: (0, generator_1.toPackageName)('@types'), items: [(0, generator_1.toTypeName)('FilterValue'), (0, generator_1.toTypeName)('SortState')] });
    const relatedModels = Array.from(model.relatedModelNames).map((n) => context.models.get(n));
    // Generate label map names for column dependencies
    const labelMapDependencies = [];
    for (const relatedModel of relatedModels) {
        imports.addType(relatedModel.types.viewModel);
        imports.addType(relatedModel.types.id);
        labelMapDependencies.push(`${relatedModel.table.propName}LabelMap`);
    }
    const fieldDefinitions = generateFieldsDefinitions({ model, context });
    return `
import { ColumnDef } from '@tanstack/react-table'

import { useMemo, useEffect, useRef, useCallback } from 'react'

import { TableFilter } from '@components/admin/table-filter'
import { ColumnHeaderStateIcon } from '@components/admin/column-header-state-icon'
import { createSelectColumn, DataGrid, DataGridViewMenu, Input, useDataGrid } from '@postxl/ui-components'
import { useTRPC } from '@lib/trpc'
import { ParentSize } from '@visx/responsive'

${imports.generate()}

/**
 * Component that renders a table for ${model.userFriendlyName} model.
  */
export function ${model.table.component.name}({
  tableHeight,
  data,
  filters,
  onFilterChange,
  searchInput,
  onSearchInputChange,
  sort,
  onSortChange,
  onDataEdit,
  onCellUpdate,
  handleAddRow,
  handleRemoveRows,
  onCellChange,
  onRowSelectionChange,
  selectedKey,
  onRowClick,
  keyField,
  hasNextPage,
  fetchNextPage,
  total,
  ${relatedModels.map((rm) => `${rm.table.propName}`).join(',\n  ')}
}: Readonly<{
  tableHeight?: number
  data: ${model.types.viewModel.name}[]
  filters: ${model.types.filter.type.name}
  onFilterChange: (filterKey: ${model.types.filter.fieldType.name}, value: FilterValue) => void
  searchInput: string
  onSearchInputChange: (value: string) => void
  sort?: SortState<${model.types.filter.fieldType.name}>
  onSortChange?: (sort: SortState<${model.types.filter.fieldType.name}>) => void
  onDataEdit?: (data: ${model.types.viewModel.name}[]) => void
  onCellUpdate?: (args: { rowIndex: number; columnId: string; value: unknown }) => void
  ${relatedModels.map((rm) => `${rm.table.propName}?: Map<${rm.types.id.name}, ${rm.types.viewModel.name}>`).join('\n  ')}
  handleAddRow?: () => void
  handleRemoveRows?: (countries: ${model.types.id.name}[]) => void
  onCellChange?: (args: { rowIndex: number; columnId: string }) => void
  onRowSelectionChange?: (rowIds: string[]) => void
  // KeyField-based row selection (for URL routing)
  selectedKey?: string
  onRowClick?: (key: string | null) => void
  keyField?: string
  // Infinite scroll props
  hasNextPage?: boolean
  fetchNextPage?: () => Promise<unknown>
  total?: number
}>) {
  const trpc = useTRPC()
  
  ${relatedModels.length > 0 ? '// Create label maps for relation fields (O(1) lookups in cells)' : ''}
  ${relatedModels
        .map((rm) => `  const ${rm.table.propName}LabelMap = useMemo(
    () => new Map(Array.from(${rm.table.propName}?.entries() ?? []).map(([key, entity]) => [key, entity.${rm.labelField.name}])),
    [${rm.table.propName}],
  )`)
        .join('\n')}

  // Find the selected row index based on selectedKey and keyField
  const selectedRowIndex = useMemo(() => {
    if (!selectedKey || !keyField) return -1
    return data.findIndex((row) => {
      const value = (row as Record<string, unknown>)[keyField]
      return value != null && String(value) === selectedKey
    })
  }, [data, selectedKey, keyField])

  // Handle row click - extract keyField value and call onRowClick
  const handleRowClick = useCallback((rowIndex: number) => {
    if (!onRowClick || !keyField) return
    const row = data[rowIndex]
    if (row) {
      const value = (row as Record<string, unknown>)[keyField]
      if (value == null) {
        return
      }
      const keyValue = String(value)
      // Always navigate to the row, even if already selected (no toggle behavior)
      onRowClick(keyValue)
    }
  }, [data, keyField, onRowClick])

  const columns = useMemo<ColumnDef<${model.types.viewModel.name}>[]>(
    () => [
      createSelectColumn<${model.types.viewModel.name}>({
        onRemoveRows: handleRemoveRows,
      }),
      ${fieldDefinitions.map((def) => generateColumn({ ...def, filterFieldArrayName, filterConfigName: model.types.filter.config.name, trpcRouteName: model.trpcRoute.name })).join(',\n')}
    ],
    [filters, onFilterChange, handleRemoveRows, trpc.${model.trpcRoute.name}, sort, ${labelMapDependencies.join(', ')}],
  )

  const onRowAdd = () => {
    handleAddRow?.()
    // Return null to prevent scrolling - the modal handles the create flow
    // and the table will refresh via query invalidation when the item is created
    return null
  }

  // Convert server sort state to TanStack Table format (only when server-side sorting is enabled)
  const tanstackSorting = useMemo(
    () => sort?.map((s) => ({ id: s.field, desc: s.direction === 'desc' })) ?? [],
    [sort],
  )

  // Handle sorting change - convert TanStack format back to server format
  const handleSortingChange = useMemo(() => {
    if (!onSortChange) return undefined
    return (updater: any) => {
      const newSorting = typeof updater === 'function' ? updater(tanstackSorting) : updater
      onSortChange(
        newSorting.map((s: { id: string; desc: boolean }) => ({
          field: s.id as ${model.types.filter.fieldType.name},
          direction: s.desc ? 'desc' : 'asc',
        })),
      )
    }
  }, [onSortChange, tanstackSorting])

  const { table, ...dataGridProps } = useDataGrid({
    columns,
    data,
    // Only use onDataChange for bulk operations (paste) when onCellUpdate is not provided
    onDataChange: onCellUpdate ? undefined : onDataEdit,
    onCellFocus: onCellChange,
    onRowAdd: handleAddRow ? onRowAdd : undefined,
    enableSearch: true,
    // Infinite scroll: trigger loading more when scrolling near the end
    onLoadMore: hasNextPage ? fetchNextPage : undefined,
    // Total row count for proper scrollbar sizing (reflects full dataset)
    totalRowCount: total,
    // KeyField-based row selection (for URL routing)
    highlightedRowIndex: selectedRowIndex,
    onRowClick: handleRowClick,
    // Server-side sorting: only enable when onSortChange is provided
    // Otherwise, keep default client-side sorting behavior
    ...(onSortChange && {
      manualSorting: true,
      sorting: tanstackSorting,
      onSortingChange: handleSortingChange,
    }),
    // Pass individual cell update handler via meta for direct mutations
    meta: onCellUpdate ? { onCellChange: onCellUpdate } : undefined,
  })

  // Track row selection changes and notify parent
  const prevSelectedRowIds = useRef<string[]>([])
  const rowSelectionState = table.getState().rowSelection
  useEffect(() => {
    const selectedRowIds = Object.keys(rowSelectionState).filter(key => rowSelectionState[key])
    const prevIds = prevSelectedRowIds.current

    // Only notify if selection actually changed
    const hasChanged = selectedRowIds.length !== prevIds.length ||
      selectedRowIds.some(id => !prevIds.includes(id))

    if (hasChanged) {
      prevSelectedRowIds.current = selectedRowIds
      // Map row indices to entity IDs
      const entityIds: string[] = []
      for (const rowIndex of selectedRowIds) {
        const row = table.getRowModel().rows[Number(rowIndex)]
        if (row?.original?.id) {
          entityIds.push(row.original.id)
        }
      }
      onRowSelectionChange?.(entityIds)
    }
  }, [rowSelectionState, table, onRowSelectionChange])

  return (
    <div className="flex flex-col h-full min-h-0 gap-4">
      <div className="flex items-center gap-4">
        <div className="flex-1" />
        <Input
          type="text"
          placeholder="Search..."
          value={searchInput}
          onChange={(e) => onSearchInputChange(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Escape') {
              onSearchInputChange('')
            }
          }}
          className="w-64 h-8"
        />
        <DataGridViewMenu table={table} />
      </div>
      <div className="h-full overflow-hidden">
        <ParentSize>
          {({ height }) => <DataGrid {...dataGridProps} table={table} height={tableHeight ?? height} />}
        </ParentSize>
      </div>
    </div>
  )
}
`;
}
function generateFieldsDefinitions({ model, context, }) {
    const result = [];
    for (const field of model.fields.values()) {
        result.push(...getFieldDefinitionsForField({ field, model, context }));
    }
    return result;
}
function quote(str) {
    const singleQuote = String.raw `\'`;
    return `'${str.replaceAll("'", singleQuote)}'`;
}
function generateColumn({ filterFieldArrayName, filterConfigName, trpcRouteName, column: { id, accessor, header, variant, size, readonly }, options, }) {
    let optionsString = '';
    let hasSearchString = '';
    switch (options?.kind) {
        case 'static': {
            const optionsArr = `[${options.options
                .map((o) => {
                const label = quote(o.label);
                const value = quote(o.value);
                return `{ label: ${label}, value: ${value} }`;
            })
                .join(', ')}]`;
            optionsString = `, options: ${optionsArr}`;
            break;
        }
        case 'dynamic': {
            optionsString = `, options: ${options.expression}`;
            hasSearchString = variant === 'select' ? ', hasSearch: true' : '';
            break;
        }
        case 'optionsMap': {
            // Use optionsMap for O(1) lookups - pass the pre-built label Map directly
            optionsString = `, optionsMap: ${options.mapExpression}`;
            hasSearchString = variant === 'select' ? ', hasSearch: true' : '';
            break;
        }
    }
    const cellString = `  cell: { variant: '${variant}'${optionsString}${hasSearchString} },\n`;
    const editableString = readonly ? '  editable: false, \n' : '';
    // Column ID is already the filter field name (e.g., 'authorEmail' not 'email')
    const filterString = `  ...${filterFieldArrayName}.includes('${id}' as any) && {
      headerMenuFooter: ({ open }) => (
        <TableFilter
          open={open}
          field={'${id}'}
          filters={filters}
          onChange={(val) => onFilterChange('${id}', val)}
          config={${filterConfigName}}
          getFilterOptions={(args) => trpc.${trpcRouteName}.getFilterOptions.queryOptions(args)}
        />
      ),
      headerCustomComponent: <ColumnHeaderStateIcon filterValue={filters.${id}} sortState={sort?.find(s => s.field === '${id}')} />,
    }\n`;
    return `{
    id: '${id}',
    ${accessor},
    header: ${quote(header)},
    meta: {${cellString}${editableString}${filterString}},
    size: ${size},
  }`;
}
function calculateWidth(title) {
    const minWidth = 150;
    const maxWidth = 300;
    const titleWidth = title.length * 8;
    return Math.min(maxWidth, Math.max(minWidth, titleWidth));
}
function generateColumnHeader({ field, duParentField, duMember }) {
    if (duParentField) {
        const memberLabel = duMember ? `${duMember.label}: ` : '';
        return `${duParentField.label ?? ''}: ${memberLabel}${field.label}`;
    }
    return field.label;
}
function generateCellVariant({ field, }) {
    switch (field.kind) {
        case 'id':
            return 'short-text';
        case 'scalar':
            return generateCellVariantForScalar(field);
        case 'relation':
            return 'select';
        case 'enum':
            return 'select';
        case 'discriminatedUnion':
            return 'select';
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
}
function generateCellVariantForScalar(field) {
    switch (field.type) {
        case 'number':
            return 'number';
        case 'string':
            return (field.validations.maxLength ?? 0) > 200 ? 'long-text' : 'short-text';
        case 'Date':
            return 'date';
        case 'boolean':
            return 'checkbox';
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
}
function getFieldDefinitionsForField(params) {
    if (params.field.kind === 'discriminatedUnion') {
        return getFieldDiscriminatedUnionColumns(params);
    }
    return [getFieldDefinitionForField(params)];
}
function getFieldDefinitionForField(params) {
    // Use filter field name as column ID so sorting maps correctly to backend filter fields
    const columnId = (0, model_type_generator_1.getFilterFieldName)(params.field.name, params.duParentField?.name);
    return {
        column: {
            id: columnId,
            accessor: generateFieldAccessor(params),
            header: generateColumnHeader(params),
            variant: generateCellVariant(params),
            readonly: params.model.isReadonly || (params.field.kind === 'scalar' && params.field.isReadonly),
            size: calculateWidth(params.field.name),
            duParentFieldName: params.duParentField?.name,
        },
        options: getOptions(params),
    };
}
function generateRelationAccessor({ field, model, duParentField, duMember }) {
    if (duParentField === undefined) {
        return `accessorKey: '${field.name}'`;
    }
    if (duMember) {
        // Field that only belongs to one DU member
        const memberCondition = `row.${duParentField.name}?.type === '${duMember.type}'`;
        return `accessorFn: (row: ${model.types.viewModel.name}) => ${memberCondition} ? row.${duParentField.name}?.${field.name} ?? null : null`;
    }
    return `accessorFn: (row: ${model.types.viewModel.name}) => row.${duParentField.name}?.${field.name} ?? null`;
}
function generateFieldAccessor({ field, model, context, duParentField, duMember }) {
    switch (field.kind) {
        case 'relation':
            return generateRelationAccessor({ field, model, duParentField, duMember, context });
        case 'discriminatedUnion':
            return `accessorFn: (row: ${model.types.viewModel.name}) => row.${field.name}?.type ?? ''`;
        default: {
            const duMemberTypeString = duMember ? `?.type === '${duMember.type}'` : '';
            return duParentField === undefined
                ? `accessorKey: '${field.name}'`
                : `accessorFn: (row: ${model.types.viewModel.name}) => row.${duParentField.name}${duMemberTypeString} ? row.${duParentField.name}?.${field.name} ?? '' : ''`;
        }
    }
}
function getOptions({ field, context }) {
    switch (field.kind) {
        case 'enum': {
            const enum_ = context.enums.get(field.enumName);
            return {
                kind: 'static',
                options: [...enum_.members.values()].map((m) => ({
                    label: String(m.value),
                    value: String(m.value),
                })),
            };
        }
        case 'discriminatedUnion': {
            const options = [];
            for (const member of field.members.values()) {
                options.push({
                    label: member.label,
                    value: member.type,
                });
            }
            return { kind: 'static', options };
        }
        case 'relation': {
            const relatedModel = context.models.get(field.referencedModelName);
            // Use optionsMap for O(1) lookups - reference the pre-built label map
            const mapExpression = `${relatedModel.table.propName}LabelMap`;
            return { kind: 'optionsMap', mapExpression };
        }
        default:
            return undefined;
    }
}
function getFieldDiscriminatedUnionColumns(params) {
    const field = params.field;
    const fieldDefinitions = [getFieldDefinitionForField({ ...params, field })];
    for (const commonField of field.commonFields.values()) {
        fieldDefinitions.push(...getFieldDefinitionsForField({ ...params, field: commonField, duParentField: field }));
    }
    for (const member of field.members.values()) {
        for (const memberField of member.fields.values()) {
            fieldDefinitions.push(...getFieldDefinitionsForField({
                ...params,
                field: memberField,
                duParentField: field,
                duMember: member,
            }));
        }
    }
    return fieldDefinitions;
}
//# sourceMappingURL=model-table.generator.js.map