"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateExcelIoService = generateExcelIoService;
function generateExcelIoService(context) {
    const models = [...context.models.values()].sort((a, b) => a.name.localeCompare(b.name));
    const keyFieldMapEntries = models
        .flatMap((model) => [
        `  ${model._conjugated.camelCase}: '${model.keyField.name}',`,
        `  ${model._conjugated.camelCasePlural}: '${model.keyField.name}',`,
    ])
        .join('\n');
    const modelNames = models.map((model) => `'${model._conjugated.camelCase}'`).join(',\n  ');
    const allModelsDataModel = models
        .map((model) => `      ${model._conjugated.camelCasePlural}: await this.viewService.${model.view.service.variableName}.getList(authorization),`)
        .join('\n');
    const modelCases = models
        .map((model) => `
      case '${model._conjugated.camelCase}': {
        const filters = ${model.types.filter.decoder.name}.parse(rawFilters ?? {})
        const sort = ${model.types.sort.decoder.name}.optional().parse(rawSort)
        const items = await this.viewService.${model.view.service.variableName}.getFiltered({ filters, sort, authorization })
        return this.renderExcel({
          dataModel: { ${model._conjugated.camelCasePlural}: items },
          filename: \`${'${timeStamp()}'} ${model._conjugated.kebabCasePlural}.xlsx\`,
          templateIds: ['${model._conjugated.camelCase}.xlsx', ...ALL_MODELS_TEMPLATE_IDS],
        })
      }`)
        .join('');
    const filterSortImports = models
        .map((model) => `  ${model.types.filter.decoder.name},\n  ${model.types.sort.decoder.name},`)
        .join('\n');
    return /*ts*/ `
import { DispatcherService } from '@actions/dispatcher.service'
import { flattenExcelDataModel, type DataModel, type RecordParseError } from '@decoders/data-model.decoder'
import type { Action_Import, ImportExecutionSummary } from '@import/import.service'
import { ImportService } from '@import/import.service'
import type { Delta } from '@import/detect-delta/models.detect-delta'
import { extractErrors } from '@import/detect-delta/models.detect-delta'
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'
import type { AuthorizationContext } from '@types'
import {
${filterSortImports}
} from '@types'
import { ViewService } from '@view/view.service'
import { XlPortService } from '@xlport/xlport.service'

import { randomUUID } from 'node:crypto'
import { Readable } from 'node:stream'
import z from 'zod'

import { ExhaustiveSwitchCheck, timeStamp } from '@postxl/utils'

const modelNameDecoder = z.enum([
  ${modelNames}
])

export type ExportModelName = z.infer<typeof modelNameDecoder>
const ALL_MODELS_TEMPLATE_IDS = ['data-model.xlsx'] as const

const PREVIEW_TTL_MS = 30 * 60 * 1000 // 30 minutes
const PREVIEW_MAX_ENTRIES = 50

const KEY_FIELD_MAP: Record<string, string> = {
${keyFieldMapEntries}
}

type StoredPreview = {
  delta: Delta
  parseErrors: RecordParseError[]
  includeDetailedUnchangedRecords: boolean
  filename: string
  createdAt: number
}

export type ImportPreviewModelSummary = {
  create: number
  update: number
  delete: number
  unchanged: number
  errors: number
  items: ImportPreviewItem[]
}

export type ImportPreviewItem = {
  type: 'create' | 'update' | 'delete' | 'unchanged' | 'errors'
  deltaIndex?: number
  id: string
  keyFieldValue: string
  input: Record<string, unknown>
  delta?: Record<string, { old: unknown; new: unknown }>
  errors?: unknown[]
}

export type ImportPreviewResponse = {
  previewSessionId: string
  filename: string
  summary: {
    create: number
    update: number
    delete: number
    unchanged: number
    errors: number
  }
  models: Record<string, ImportPreviewModelSummary>
  processingErrors?: string[]
}

export type ImportPreviewSessionResponse = {
  previewSessionId: string
}

export type ImportPreviewOptions = {
  includeDetailedUnchangedRecords?: boolean
}

export const zImportPreviewSessionRequestDecoder = z.object({
  previewSessionId: z.string().min(1, 'Preview session id is required'),
})

export type ImportPreviewSessionRequest = z.infer<typeof zImportPreviewSessionRequestDecoder>

export const zImportExecuteRequestDecoder = z.object({
  previewSessionId: z.string().min(1, 'Preview session id is required'),
  ignoreErrors: z.boolean().default(false),
  confirmDeletes: z.boolean().default(false),
  selectedDeltaIndicesByModel: z.record(z.string(), z.array(z.number().int().min(0))).default({}),
})

export type ImportExecuteRequest = z.infer<typeof zImportExecuteRequestDecoder>

@Injectable()
export class ExcelIoService {
  private readonly logger = new Logger(ExcelIoService.name)
  private readonly previewStore = new Map<string, StoredPreview>()

  constructor(
    private readonly viewService: ViewService,
    private readonly xlPortService: XlPortService,
    @Inject(forwardRef(() => DispatcherService)) private readonly dispatcherService: DispatcherService,
    @Inject(forwardRef(() => ImportService)) private readonly importService: ImportService,
  ) {}

  public parseModelName(model: string): ExportModelName {
    return modelNameDecoder.parse(model)
  }

  public async exportAllModelsToExcel(authorization: AuthorizationContext) {
    this.logger.log('Exporting all models to Excel')

    const dataModel: DataModel = {
${allModelsDataModel}
    }

    return this.renderExcel({ dataModel, filename: \`${'${timeStamp()}'} all-models.xlsx\` })
  }

  public async exportModelToExcel({
    model,
    rawFilters,
    rawSort,
    authorization,
  }: {
    model: ExportModelName
    rawFilters?: unknown
    rawSort?: unknown
    authorization: AuthorizationContext
  }) {
    this.logger.log(\`Exporting model ${'${model}'} to Excel\`)

    switch (model) {${modelCases}
      default:
        throw new ExhaustiveSwitchCheck(model)
    }
  }

  public async importExcel({
    data,
    filename,
    authorization,
  }: {
    data: Buffer
    filename: string
    authorization: AuthorizationContext
  }) {
    this.logger.log('Importing uploaded Excel file through import action')

    const action: Action_Import = {
      scope: 'import',
      type: 'excel',
      payload: {
        filename,
        data,
      },
    }

    return this.dispatcherService.dispatch({
      action,
      user: authorization.user,
      userRoles: authorization.userRoles,
    })
  }

  public async previewImportExcel({
    data,
    filename,
    includeDetailedUnchangedRecords = false,
  }: {
    data: Buffer
    filename: string
    includeDetailedUnchangedRecords?: boolean
  }): Promise<ImportPreviewSessionResponse> {
    this.logger.log('Previewing Excel import (read-only)')

    try {
      const { delta, parseErrors } = await this.importService.previewExcelImport(data)
      const previewSessionId = randomUUID()

      this.cleanupExpiredPreviews()
      this.previewStore.set(previewSessionId, {
        delta,
        parseErrors,
        includeDetailedUnchangedRecords,
        filename,
        createdAt: Date.now(),
      })

      return { previewSessionId }
    } catch (error) {
      this.logger.error('Error during preview delta detection or schema parsing', error)
      throw new Error(error instanceof Error ? error.message : String(error))
    }
  }

  public getImportPreviewBySessionId({ previewSessionId }: ImportPreviewSessionRequest): ImportPreviewResponse {
    this.logger.log(\`Retrieving import preview session: \${previewSessionId}\`)
    const stored = this.getStoredPreview(previewSessionId)
    return buildPreviewResponse(
      previewSessionId,
      stored.filename,
      stored.delta,
      stored.parseErrors,
      stored.includeDetailedUnchangedRecords,
    )
  }

  public async executeImportFromPreview({
    previewSessionId,
    ignoreErrors,
    confirmDeletes,
    selectedDeltaIndicesByModel,
    authorization,
  }: ImportExecuteRequest & { authorization: AuthorizationContext }): Promise<ImportExecutionSummary> {
    this.logger.log(\`Executing import from preview session: \${previewSessionId}\`)
    const stored = this.getStoredPreview(previewSessionId)
    const selectedDelta = filterDeltaBySelectedIndices(stored.delta, selectedDeltaIndicesByModel)

    if (!deltaHasExecutableChanges(selectedDelta)) {
      throw new Error('No executable rows selected. Select at least one create, update, or delete row.')
    }

    const hasDeletes = deltaHasDeletes(selectedDelta)
    if (hasDeletes && !confirmDeletes) {
      throw new Error('Import contains delete operations. You must confirm deletes before executing.')
    }

    const hasErrors = Object.keys(extractErrors(selectedDelta)).length > 0
    if (hasErrors && !ignoreErrors) {
      throw new Error('Import contains errors. Enable "Ignore errors" to skip errored rows, or fix the data.')
    }

    const action: Action_Import = {
      scope: 'import',
      type: 'reviewedExcel',
      payload: {
        filename: stored.filename,
        delta: selectedDelta,
        ignoreErrors,
      },
    }

    const result = await this.dispatcherService.dispatch({
      action,
      user: authorization.user,
      userRoles: authorization.userRoles,
    })

    this.previewStore.delete(previewSessionId)

    return result as ImportExecutionSummary
  }

  private getStoredPreview(previewSessionId: string): StoredPreview {
    this.cleanupExpiredPreviews()

    const stored = this.previewStore.get(previewSessionId)
    if (!stored) {
      throw new Error('Invalid or expired preview session id')
    }

    if (Date.now() - stored.createdAt > PREVIEW_TTL_MS) {
      this.previewStore.delete(previewSessionId)
      throw new Error('Preview session has expired. Please upload and preview again.')
    }

    return stored
  }

  private cleanupExpiredPreviews() {
    const now = Date.now()
    for (const [key, value] of this.previewStore.entries()) {
      if (now - value.createdAt > PREVIEW_TTL_MS) {
        this.previewStore.delete(key)
      }
    }

    if (this.previewStore.size >= PREVIEW_MAX_ENTRIES) {
      const oldest = [...this.previewStore.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt)
      const toRemove = oldest.slice(0, this.previewStore.size - PREVIEW_MAX_ENTRIES + 1)
      for (const [key] of toRemove) {
        this.previewStore.delete(key)
      }
    }
  }

  private async renderExcel({
    dataModel,
    filename,
    templateIds = ALL_MODELS_TEMPLATE_IDS,
  }: {
    dataModel: DataModel
    filename: string
    templateIds?: readonly string[]
  }) {
    const data = flattenExcelDataModel(dataModel)
    const file = await this.exportWithTemplateIds(data, templateIds)

    return { filename, file }
  }

  private async exportWithTemplateIds(
    data: ReturnType<typeof flattenExcelDataModel>,
    templateIds: readonly string[],
  ): Promise<Buffer> {
    const errors: string[] = []

    for (const templateId of templateIds) {
      try {
        const raw = await this.xlPortService.exportToFile({ templateId, data })
        const buffer = await toBuffer(raw)
        const validated = parseXlPortExportBuffer(buffer)
        this.logger.log(\`Excel export succeeded via templateId(\${templateId})\`)
        return validated
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error)
        errors.push(\`templateId(\${templateId}): \${message}\`)
        this.logger.warn(\`Excel export attempt failed (templateId=\${templateId}): \${message}\`)
      }
    }

    throw new Error(
      \`Unable to export Excel via xlport templateId lookup. Tried \${templateIds.join(', ')}. Details: \${errors.join(' | ')}\`,
    )
  }
}

async function toBuffer(value: unknown): Promise<Buffer> {
  if (Buffer.isBuffer(value)) {
    return value
  }

  if (value instanceof Uint8Array) {
    return Buffer.from(value)
  }

  if (typeof value === 'string') {
    return Buffer.from(value)
  }

  if (value instanceof Readable) {
    const asyncValue = asAsyncIterable(value)
    const chunks: Buffer[] = []
    for await (const chunk of asyncValue) {
      if (Buffer.isBuffer(chunk)) {
        chunks.push(chunk)
      } else if (chunk instanceof Uint8Array) {
        chunks.push(Buffer.from(chunk))
      } else {
        chunks.push(Buffer.from(String(chunk)))
      }
    }

    const buffer = Buffer.concat(chunks as any) // NOSONAR - type assertion to Buffer[] as else Frontend types does not match.
    if (buffer.length === 0) {
      throw new Error('xlport returned an empty Excel stream')
    }
    return buffer
  }

  throw new Error('Unexpected xlport export response type')
}

function asAsyncIterable(value: unknown): AsyncIterable<unknown> {
  if (!value || typeof value !== 'object') {
    throw new TypeError('xlport stream value is not iterable')
  }

  const asyncIterator = (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator]
  if (typeof asyncIterator !== 'function') {
    throw new TypeError('xlport stream value is not async iterable')
  }

  return value as AsyncIterable<unknown>
}

type PreviewTotals = {
  create: number
  update: number
  delete: number
  unchanged: number
  errors: number
}

function createPreviewTotals(): PreviewTotals {
  return { create: 0, update: 0, delete: 0, unchanged: 0, errors: 0 }
}

function createModelSummary(): ImportPreviewModelSummary {
  return {
    create: 0,
    update: 0,
    delete: 0,
    unchanged: 0,
    errors: 0,
    items: [],
  }
}

function ensureModelSummary(
  models: Record<string, ImportPreviewModelSummary>,
  modelName: string,
): ImportPreviewModelSummary {
  const existing = models[modelName]
  if (existing) {
    return existing
  }

  const created = createModelSummary()
  models[modelName] = created
  return created
}

function toPreviewText(value: unknown): string {
  if (value === null || value === undefined) {
    return ''
  }
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
    return String(value) // NOSONAR
  }
  if (value instanceof Date) {
    return value.toISOString()
  }
  if (typeof value === 'object') {
    try {
      return JSON.stringify(value)
    } catch {
      return '[object]'
    }
  }
  return String(value) // NOSONAR
}

function getKeyFieldValue(modelName: string, inputRecord: Record<string, unknown>): string {
  const keyFieldName = KEY_FIELD_MAP[modelName]
  if (!keyFieldName) {
    return ''
  }
  return toPreviewText(inputRecord[keyFieldName] ?? inputRecord[excelNameFor(keyFieldName)])
}

function incrementPreviewTotals(
  modelSummary: ImportPreviewModelSummary,
  totals: PreviewTotals,
  type: ImportPreviewItem['type'],
): void {
  switch (type) {
    case 'create':
      modelSummary.create++
      totals.create++
      return
    case 'update':
      modelSummary.update++
      totals.update++
      return
    case 'delete':
      modelSummary.delete++
      totals.delete++
      return
    case 'unchanged':
      modelSummary.unchanged++
      totals.unchanged++
      return
    case 'errors':
      modelSummary.errors++
      totals.errors++
      return
  }
}

type DeltaPreviewItemLike = {
  type: ImportPreviewItem['type']
  input: unknown
  delta?: unknown
  errors?: unknown[]
}

function appendDeltaPreviewItem(
  modelName: string,
  item: DeltaPreviewItemLike,
  deltaIndex: number,
  includeDetailedUnchangedRecords: boolean,
  modelSummary: ImportPreviewModelSummary,
  totals: PreviewTotals,
): void {
  incrementPreviewTotals(modelSummary, totals, item.type)

  if (item.type === 'unchanged' && !includeDetailedUnchangedRecords) {
    return
  }

  const inputRecord = item.input as Record<string, unknown>
  const previewItem: ImportPreviewItem = {
    type: item.type,
    deltaIndex,
    id: toPreviewText(inputRecord.id),
    keyFieldValue: getKeyFieldValue(modelName, inputRecord),
    input: inputRecord,
  }

  if (item.type === 'update') {
    previewItem.delta = item.delta as Record<string, { old: unknown; new: unknown }>
  }
  if (item.type === 'errors' && 'errors' in item) {
    previewItem.errors = item.errors as unknown[]
  }

  modelSummary.items.push(previewItem)
}

function appendParseErrorItem(
  parseError: RecordParseError,
  models: Record<string, ImportPreviewModelSummary>,
  totals: PreviewTotals,
): void {
  const modelSummary = ensureModelSummary(models, parseError.model)
  incrementPreviewTotals(modelSummary, totals, 'errors')
  modelSummary.items.push({
    type: 'errors',
    id: '',
    keyFieldValue: getKeyFieldValue(parseError.model, parseError.raw),
    input: parseError.raw,
    errors: parseError.issues.map((issue) => ({ error: 'parse-error', message: issue })),
  })
}

function buildPreviewResponse(
  previewSessionId: string,
  filename: string,
  delta: Delta,
  parseErrors: RecordParseError[] = [],
  includeDetailedUnchangedRecords = false,
): ImportPreviewResponse {
  const models: Record<string, ImportPreviewModelSummary> = {}
  const totals = createPreviewTotals()

  for (const [modelName, items] of Object.entries(delta)) {
    if (!items?.length) {
      continue
    }

    const modelSummary = ensureModelSummary(models, modelName)
    for (const [deltaIndex, item] of items.entries()) {
      appendDeltaPreviewItem(
        modelName,
        item as DeltaPreviewItemLike,
        deltaIndex,
        includeDetailedUnchangedRecords,
        modelSummary,
        totals,
      )
    }
  }

  // Inject per-record parse errors as 'errors' type items in the corresponding model
  for (const parseError of parseErrors) {
    appendParseErrorItem(parseError, models, totals)
  }

  return {
    previewSessionId,
    filename,
    summary: {
      create: totals.create,
      update: totals.update,
      delete: totals.delete,
      unchanged: totals.unchanged,
      errors: totals.errors,
    },
    models,
  }
}

/**
 * Parse error raw data uses Excel column names (PascalCase/space-separated),
 * but KEY_FIELD_MAP uses camelCase field names. This helper converts a camelCase
 * field name to a likely Excel column name for lookup in raw data.
 */
function excelNameFor(fieldName: string): string {
  // Convert camelCase to Title Case with spaces (e.g., 'blogTitle' -> 'Blog Title', 'slug' -> 'Slug')
  return fieldName
    .replaceAll(/[A-Z]/g, ' $&')
    .replace(/^./, s => s.toUpperCase())
    .trim()
}

function deltaHasDeletes(delta: Delta): boolean {
  for (const items of Object.values(delta)) {
    if (items?.some((item: { type: string }) => item.type === 'delete')) {
      return true
    }
  }
  return false
}

function deltaHasExecutableChanges(delta: Delta): boolean {
  for (const items of Object.values(delta)) {
    if (!items?.length) {
      continue
    }

    for (const item of items) {
      if (item.type === 'create' || item.type === 'update' || item.type === 'delete') {
        return true
      }
    }
  }
  return false
}

function filterDeltaBySelectedIndices(delta: Delta, selectedDeltaIndicesByModel: Record<string, number[]>): Delta {
  const hasSelectionMap = Object.keys(selectedDeltaIndicesByModel).length > 0
  if (!hasSelectionMap) {
    return delta
  }

  const filteredEntries = Object.entries(delta).map(([modelName, items]) => {
    if (!items?.length) {
      return [modelName, items]
    }

    const selectedIndices = selectedDeltaIndicesByModel[modelName] ?? []
    const selectedSet = new Set(selectedIndices)
    const filteredItems = items.filter((item, index) => {
      if (item.type !== 'create' && item.type !== 'update' && item.type !== 'delete') {
        return false
      }
      return selectedSet.has(index)
    })
    return [modelName, filteredItems]
  })

  return Object.fromEntries(filteredEntries) as Delta
}

function parseXlPortExportBuffer(buffer: Buffer): Buffer {
  if (buffer.length >= 2 && buffer[0] === 0x50 && buffer[1] === 0x4b) {
    return buffer
  }

  const text = buffer.toString('utf8').trim()
  if (text.startsWith('{')) {
    try {
      const parsed = JSON.parse(text) as { message?: string; status?: string }
      throw new Error(\`xlport returned JSON error (\${parsed.status ?? 'unknown'}): \${parsed.message ?? text}\`)
    } catch (error) {
      if (error instanceof SyntaxError) {
        throw new Error(\`xlport returned non-xlsx payload: \${text.slice(0, 300)}\`)
      }
      throw error
    }
  }

  throw new Error(\`xlport returned non-xlsx payload (\${buffer.length} bytes)\`)
}
`;
}
//# sourceMappingURL=excel-io-service.generator.js.map