import refreshTokenHelper from "../../utils/refreshTokenHelper"
import forceLogoutHelper from "../../utils/forceLogoutHelper"
import errorHandlingHelper from "../../utils/errorHandlingHelper"
import maybeZugferdPdf from "../../utils/zugferd/maybeZugferdPdf"
import toIdempiereDate from "../../utils/idempiereDate"
import { lexFetch, lexAuthHeaders, createVoucher, uploadFileToVoucher } from "../../utils/lexofficeClient"
import { findInvoiceOriginalPdf } from "../../utils/invoiceOriginalPdf"

/**
 * Throughput design (measured on prod, see git history of this file):
 * one invoice = ~1.2 s iDempiere PDF render + ~2.5 s of Lexware calls, and Lexware's
 * upload calls simply take ~2 s each on their side (the 202 is returned before their
 * async OCR — there is nothing to wait for or poll). So instead of processing invoices
 * strictly one after another with fixed sleeps, INVOICE_CONCURRENCY invoices are
 * worked on at once: while one is waiting on Lexware, the others render their PDFs.
 * All Lexware traffic goes through `lexFetch`, whose process-wide throttle enforces
 * the 2 req/s limit (that throttle replaced the former hard-coded 500 ms sleeps).
 */
const INVOICE_CONCURRENCY = 3

/**
 * Invoice ids currently being uploaded by THIS process. Two overlapping requests
 * (double-click, two tabs, two users) that both pass the `isUploadToLexoffice`
 * check before either has marked the invoice would otherwise upload it twice.
 * Process-local; prod runs one PM2 instance so this covers the realistic case.
 */
const inProgress = new Set<number>()

const round2 = (n: number): number => Math.round((Number(n) || 0) * 100) / 100

/**
 * Sentinel categoryId (from LexofficeCategoryModal.vue's "Mit Standardkategorie hochladen")
 * meaning "file this as a real voucher, but use a generic catch-all category" rather than
 * either a specific hand-picked category or the true uncategorized Files-drop. Resolved per
 * invoice to one of the two IDs below, based on that invoice's own AR/AP direction.
 */
const DEFAULT_CATEGORY_SENTINEL = '__default__'
/** Lexoffice posting-category "Einnahmen" (generic income) — catch-all default for AR/AR-CM. */
const DEFAULT_INCOME_CATEGORY_ID = '8f8664a1-fd86-11e1-a21f-0800200c9a66'
/** Lexoffice posting-category "Sonstige Ausgaben" (generic expense) — catch-all default for AP/AP-CM. */
const DEFAULT_OUTGO_CATEGORY_ID = '16d04a28-fd91-11e1-a21f-0800200c9a66'

// Log collector for debugging
const createLogger = () => {
  const logs: string[] = []
  return {
    log: (message: string) => {
      const timestamp = new Date().toISOString()
      logs.push(`[${timestamp}] ${message}`)
    },
    getLogs: () => logs
  }
}
type Logger = ReturnType<typeof createLogger>

/** Prefixes every line with the invoice id so interleaved concurrent logs stay readable. */
const childLogger = (parent: Logger, invoiceId: number | string): Logger => ({
  log: (message: string) => parent.log(`[inv ${invoiceId}] ${message}`),
  getLogs: parent.getLogs,
})

// Helper to make lexoffice JSON API calls (throttled via lexFetch)
const lexofficeRequest = async (endpoint: string, method: string, body?: any, logger?: Logger) => {
  const options: RequestInit = {
    method,
    headers: lexAuthHeaders(!!body),
  }
  if (body) {
    options.body = JSON.stringify(body)
  }

  logger?.log(`Lexoffice API: ${method} ${endpoint}`)

  const response = await lexFetch(endpoint, options)

  if (!response.ok) {
    const errorText = await response.text()
    logger?.log(`Lexoffice API error (${response.status}): ${errorText}`)
    throw new Error(`Lexoffice API error (${response.status}): ${errorText}`)
  }

  const contentType = response.headers.get('content-type')
  if (contentType && contentType.includes('application/json')) {
    const data = await response.json()
    logger?.log(`Lexoffice API response: ${JSON.stringify(data).substring(0, 200)}...`)
    return data
  }

  const locationHeader = response.headers.get('Location')
  if (locationHeader) {
    const id = locationHeader.split('/').pop()
    logger?.log(`Lexoffice API: Created resource with ID ${id}`)
    return { id, status: response.status }
  }

  return { status: response.status, statusText: response.statusText }
}

// Query lexoffice contacts by name
const findContact = async (name: string, logger?: Logger): Promise<any | null> => {
  try {
    if (name && name.length >= 3) {
      logger?.log(`Searching for contact: "${name}"`)
      const result = await lexofficeRequest(`/v1/contacts?name=${encodeURIComponent(name)}`, 'GET', undefined, logger)
      if (result?.content?.length > 0) {
        logger?.log(`Found existing contact: ${result.content[0].id}`)
        return result.content[0]
      }
      logger?.log(`No contact found for: "${name}"`)
    }
    return null
  } catch (err: any) {
    logger?.log(`Error searching contact: ${err.message}`)
    return null
  }
}

// Create a new contact in lexoffice
const createLexofficeContact = async (partner: any, logger?: Logger): Promise<any> => {
  const partnerName = partner.Name || ''
  const partnerName2 = partner.Name2 || ''
  const isCustomer = partner.IsCustomer === true || partner.IsCustomer === 'Y'
  const isVendor = partner.IsVendor === true || partner.IsVendor === 'Y'

  logger?.log(`Creating contact for: "${partnerName}" (Customer: ${isCustomer}, Vendor: ${isVendor})`)

  const roles: any = {}
  if (isCustomer) roles.customer = {}
  if (isVendor) roles.vendor = {}
  if (!isCustomer && !isVendor) roles.customer = {}

  let contactData: any = {
    version: 0,
    roles
  }

  if (partner.EMail) {
    contactData.emailAddresses = { business: [partner.EMail] }
  }

  // Use "LogShip" instead of "iDempiere", include customer number (Value)
  contactData.note = `LogShip Customer#: ${partner.Value || ''} (ID: ${partner.id || ''})`

  if (partnerName2) {
    const nameParts = partnerName2.trim().split(' ')
    contactData.company = {
      name: partnerName,
      contactPersons: [{
        firstName: nameParts[0] || '',
        lastName: nameParts.slice(1).join(' ') || partnerName2
      }]
    }
  } else {
    const companyIndicators = ['GmbH', 'AG', 'KG', 'OHG', 'Ltd', 'Inc', 'LLC', 'UG', 'e.K.', 'Co.', '&', 'mbH']
    const isCompany = companyIndicators.some(indicator => partnerName.includes(indicator))

    if (isCompany) {
      contactData.company = { name: partnerName }
    } else {
      const nameParts = partnerName.trim().split(' ')
      if (nameParts.length > 1) {
        contactData.person = {
          firstName: nameParts[0],
          lastName: nameParts.slice(1).join(' ')
        }
      } else {
        contactData.person = { lastName: partnerName }
      }
    }
  }

  logger?.log(`Contact data: ${JSON.stringify(contactData)}`)
  const result = await lexofficeRequest('/v1/contacts', 'POST', contactData, logger)
  return result
}

// Upload file to lexoffice as voucher (generic Files endpoint — plain, uncategorized drop).
// Returns 202 as soon as the file is stored; Lexware's OCR runs asynchronously afterwards.
const uploadFileToLexoffice = async (fileBuffer: Buffer, fileName: string, logger?: Logger): Promise<any> => {
  logger?.log(`Uploading file: ${fileName} (${fileBuffer.length} bytes)`)

  // Use native FormData and Blob for Node.js 18+
  const blob = new Blob([fileBuffer], { type: 'application/pdf' })
  const formData = new FormData()
  formData.append('file', blob, fileName)
  formData.append('type', 'voucher')

  const response = await lexFetch('/v1/files', {
    method: 'POST',
    headers: lexAuthHeaders(false),
    body: formData
  })

  if (!response.ok) {
    const errorText = await response.text()
    logger?.log(`File upload failed (${response.status}): ${errorText}`)
    throw new Error(`File upload failed (${response.status}): ${errorText}`)
  }

  const locationHeader = response.headers.get('Location')
  let responseData: any = {}
  try {
    responseData = await response.json()
  } catch (e) {}

  const fileId = responseData.id || locationHeader?.split('/').pop()
  logger?.log(`File uploaded successfully: ${fileId}`)

  return {
    id: fileId,
    ...responseData
  }
}

/**
 * Resolve (find-or-create) the Lexoffice contact for a partner and persist its UUID on
 * c_bpartner. Returns undefined when no contact could be resolved (fail-soft — the
 * caller then falls back to the plain, contact-less file drop).
 */
const resolveLexofficeContact = async (event: any, token: any, partner: any, partnerId: any, logger: Logger): Promise<string | undefined> => {
  if (partner.lexware_contact_uuid) {
    logger.log(`Partner already has lexware_contact_uuid: ${partner.lexware_contact_uuid}`)
    return partner.lexware_contact_uuid
  }

  let lexofficeContactId: string | undefined = undefined
  // Try to find existing contact by name in Lexoffice
  const existingContact = await findContact(partner.Name, logger)
  if (existingContact) {
    lexofficeContactId = existingContact.id
  } else {
    // Create new contact
    try {
      const newContact = await createLexofficeContact(partner, logger)
      lexofficeContactId = newContact.id
    } catch (contactErr: any) {
      logger.log(`Could not create contact for ${partner.Name}: ${contactErr.message}`)
    }
  }

  // Save lexoffice contact UUID back to C_BPartner
  if (lexofficeContactId) {
    try {
      logger.log(`Saving lexware_contact_uuid "${lexofficeContactId}" to c_bpartner ${partnerId}`)
      await event.context.fetch(
        `models/c_bpartner/${partnerId}`,
        'PUT',
        token,
        {
          lexware_contact_uuid: lexofficeContactId
        }
      )
      logger.log(`Successfully saved lexware_contact_uuid to c_bpartner`)
    } catch (saveErr: any) {
      logger.log(`Could not save lexware_contact_uuid to c_bpartner: ${saveErr.message}`)
    }
  }
  return lexofficeContactId
}

const handleFunc = async (event: any, authToken: any = null) => {
  const logger = createLogger()
  let data: any = {}
  const token = authToken ?? await getTokenHelper(event)
  const body = await readBody(event)
  // Dedupe: with concurrent workers a duplicated id in the request would otherwise
  // be picked up by two workers at once (serial processing used to mask this).
  const invoiceIds: number[] = [...new Set((body.ids || []).map((id: any) => Number(id)).filter((id: number) => id > 0))]
  // Optional Lexoffice posting-category UUID (from the "set a category?" modal). When
  // present, invoices are booked as a categorized+tax-rated voucher instead of a plain
  // file drop — see the categoryId branch below. Applies uniformly to every invoice in
  // this call (single or bulk upload share the same modal/category selection).
  // DEFAULT_CATEGORY_SENTINEL means "use the generic catch-all category" — resolved
  // per invoice further down since it depends on that invoice's own AR/AP direction.
  // No categoryId at all ("Ohne Kategorie hochladen") keeps the true legacy behaviour:
  // the plain, uncategorized Files-drop.
  const rawCategoryId: string | undefined = body.categoryId || undefined

  logger.log(`Starting Lexoffice upload for invoice IDs: ${JSON.stringify(invoiceIds)}${rawCategoryId ? ` with category ${rawCategoryId}` : ''}`)

  if (!invoiceIds.length) {
    return { status: 400, message: 'No invoice IDs provided', logs: logger.getLogs() }
  }

  const errors: string[] = []
  const skipped: string[] = []
  // Per-id outcome so bulk callers (e.g. the settlement pages) can update their
  // rows in place without re-fetching everything.
  const uploadedIds: number[] = []
  const skippedIds: number[] = []
  let uploadedCount = 0

  // Per-partner contact resolution shared across the concurrent workers: several
  // invoices of the same partner (typical for settlement runs) must resolve/create the
  // Lexoffice contact exactly ONCE, otherwise concurrent workers would each search,
  // miss, and create duplicate contacts before the UUID is saved on the partner.
  const contactByPartner = new Map<string, Promise<string | undefined>>()
  const resolveContactOnce = (partner: any, partnerId: any, log: Logger) => {
    const key = String(partnerId)
    let pending = contactByPartner.get(key)
    if (!pending) {
      pending = resolveLexofficeContact(event, token, partner, partnerId, log)
      contactByPartner.set(key, pending)
    }
    return pending
  }

  const processInvoice = async (invoiceId: number) => {
    const log = childLogger(logger, invoiceId)
    if (inProgress.has(invoiceId)) {
      const msg = `Invoice ${invoiceId} is already being uploaded by another request - skipped`
      log.log(msg)
      errors.push(msg)
      return
    }
    inProgress.add(invoiceId)
    try {
      await processInvoiceInner(invoiceId, log)
    } finally {
      inProgress.delete(invoiceId)
    }
  }

  const processInvoiceInner = async (invoiceId: number, log: Logger) => {
    try {
      log.log(`--- Processing invoice ID: ${invoiceId} ---`)

      // Fetch invoice (no $expand)
      const invoice: any = await event.context.fetch(
        `models/c_invoice/${invoiceId}`,
        'GET',
        token,
        null
      )

      if (!invoice) {
        const msg = `Invoice ${invoiceId} not found`
        log.log(msg)
        errors.push(msg)
        return
      }

      log.log(`Invoice found: ${invoice.DocumentNo} (DocBaseType: ${invoice.DocBaseType?.id || invoice.DocBaseType})`)

      // Check if already uploaded to Lexoffice
      if (invoice.isUploadToLexoffice === true || invoice.isUploadToLexoffice === 'Y') {
        const msg = `Invoice ${invoice.DocumentNo} already uploaded to Lexoffice - skipping`
        log.log(msg)
        skipped.push(msg)
        skippedIds.push(Number(invoiceId))
        return
      }

      // Fetch partner separately
      const partnerId = invoice.C_BPartner_ID?.id || invoice.C_BPartner_ID
      if (!partnerId) {
        const msg = `Invoice ${invoice.DocumentNo}: No business partner found`
        log.log(msg)
        errors.push(msg)
        return
      }

      log.log(`Fetching partner ID: ${partnerId}`)
      const partner: any = await event.context.fetch(
        `models/c_bpartner/${partnerId}`,
        'GET',
        token,
        null
      )

      if (!partner) {
        const msg = `Invoice ${invoice.DocumentNo}: Could not fetch partner`
        log.log(msg)
        errors.push(msg)
        return
      }

      log.log(`Partner: ${partner.Name} (Value: ${partner.Value}, IsCustomer: ${partner.IsCustomer}, IsVendor: ${partner.IsVendor})`)

      // Step 1: Find or create contact in Lexoffice (once per partner per run)
      const lexofficeContactId = await resolveContactOnce(partner, partnerId, log)

      // Step 2: Fetch invoice PDF and upload to Lexoffice
      try {
        // Purchase invoices (API/APC): the document the bookkeeping needs is the
        // VENDOR's original (e.g. the Amazon Ads "Steuerrechnung" attached by the
        // Amazon Ads import / the Eingangsrechnungen inbox), not our internal Jasper
        // print of the record. Use the attached PDF when there is one; callers that
        // must never send a substitute pass `requireOriginal: true` (the Amazon Ads
        // page) and get a per-invoice error instead of the Jasper fallback.
        const preDocBaseType = invoice.DocBaseType?.id || invoice.DocBaseType?.identifier || invoice.DocBaseType || ''
        const isPurchaseDoc = preDocBaseType === 'API' || preDocBaseType === 'APC'
        let originalPdf: { buffer: Buffer; fileName: string } | null = null
        if (isPurchaseDoc) {
          originalPdf = await findInvoiceOriginalPdf(event, Number(invoiceId))
          log.log(originalPdf ? `Original vendor PDF found: ${originalPdf.fileName} (${originalPdf.buffer.length} bytes)` : 'No original vendor PDF attached')
          if (!originalPdf && body?.requireOriginal) {
            const msg = `Invoice ${invoice.DocumentNo}: kein Original-PDF des Lieferanten angehängt — Upload übersprungen`
            log.log(msg)
            errors.push(msg)
            return
          }
        }

        let pdfResponse: any = null
        if (!originalPdf) {
          log.log(`Fetching PDF for invoice ${invoiceId}`)
          pdfResponse = await event.context.fetch(
            `models/c_invoice/${invoiceId}/print?$report_type=PDF`,
            'GET',
            token,
            null
          )
        }

        if (originalPdf || pdfResponse?.reportFile) {
          const pdfBuffer = originalPdf ? originalPdf.buffer : Buffer.from(pdfResponse.reportFile, 'base64')
          log.log(`PDF ${originalPdf ? 'loaded' : 'generated'}: ${pdfBuffer.length} bytes`)

          // Lexoffice always gets the ZUGFeRD hybrid PDF, regardless of the partner's
          // IsZugPferdInvoice flag (force: true). Still fail-soft: falls back to the
          // plain PDF on any fetch/build error. A vendor's original is uploaded as-is
          // (no XML of OUR record may be embedded into THEIR document).
          const uploadBuffer = originalPdf ? pdfBuffer : await maybeZugferdPdf(event, token, invoiceId, pdfBuffer, { force: true })
          if (uploadBuffer !== pdfBuffer) {
            log.log(`ZUGFeRD hybrid PDF generated: ${uploadBuffer.length} bytes`)
          }

          // Determine type from DocBaseType
          const docBaseType = invoice.DocBaseType?.id || invoice.DocBaseType?.identifier || invoice.DocBaseType || ''
          const isOutgoing = docBaseType === 'ARI' || docBaseType === 'ARC'
          const typePrefix = isOutgoing ? 'AR' : 'AP'
          const fileName = `${typePrefix}_${invoice.DocumentNo}_${partner.Name || 'Unknown'}.pdf`

          log.log(`DocBaseType: ${docBaseType}, Type: ${typePrefix}`)

          // Resolve the sentinel to this invoice's own direction — a bulk upload can mix
          // AR and AP invoices in one call, so the default category can't be picked once
          // for the whole batch like an explicit categoryId is.
          const effectiveCategoryId = rawCategoryId === DEFAULT_CATEGORY_SENTINEL
            ? (isOutgoing ? DEFAULT_INCOME_CATEGORY_ID : DEFAULT_OUTGO_CATEGORY_ID)
            : rawCategoryId

          // Step 2a (optional): book a categorized, tax-rated voucher via the Vouchers
          // API instead of a plain file drop, when the caller opted into a category —
          // specific or the generic catch-all default (the "set a category?" modal).
          // The tax rate/breakdown is ALWAYS read from iDempiere's own c_invoicetax rows
          // — never picked manually — so it matches what iDempiere and the ZUGFeRD XML
          // already show (see maybeZugferdPdf.ts's matching fix for why c_invoicetax,
          // not a recomputed rate*basis, is used).
          let uploadResult: { id: string } | null = null
          if (effectiveCategoryId) {
            try {
              const invTaxRes: any = await event.context.fetch(
                `models/c_invoicetax?$filter=C_Invoice_ID eq ${invoiceId}&$expand=C_Tax_ID&$top=100`,
                'GET', token, null
              )
              const voucherItems = (invTaxRes?.records || [])
                .map((t: any) => {
                  const rate = Number(t?.C_Tax_ID?.Rate) || 0
                  const taxAmount = round2(Number(t?.TaxAmt) || 0)
                  const basis = round2(Number(t?.TaxBaseAmt) || 0)
                  return { amount: round2(basis + taxAmount), taxAmount, taxRatePercent: rate, categoryId: effectiveCategoryId }
                })
                .filter((item: any) => item.amount > 0 || item.taxAmount > 0)

              if (!voucherItems.length) {
                throw new Error('No c_invoicetax rows found — cannot build a tax-rate breakdown')
              }
              if (!lexofficeContactId) {
                throw new Error('No Lexoffice contact resolved for this partner')
              }

              const voucherType = docBaseType === 'ARC' ? 'salescreditnote' : docBaseType === 'APC' ? 'purchasecreditnote' : (isOutgoing ? 'salesinvoice' : 'purchaseinvoice')
              const totalGrossAmount = round2(voucherItems.reduce((s: number, i: any) => s + i.amount, 0))
              const totalTaxAmount = round2(voucherItems.reduce((s: number, i: any) => s + i.taxAmount, 0))

              log.log(`Booking voucher (type=${voucherType}) with category ${effectiveCategoryId}: gross=${totalGrossAmount}, tax=${totalTaxAmount}, items=${JSON.stringify(voucherItems)}`)

              const voucher = await createVoucher({
                type: voucherType,
                voucherStatus: 'open',
                voucherNumber: invoice.DocumentNo,
                voucherDate: new Date(invoice.DateInvoiced || invoice.DateAcct || Date.now()).toISOString(),
                contactId: lexofficeContactId,
                totalGrossAmount,
                totalTaxAmount,
                taxType: 'gross',
                remark: `LogShip invoice ${invoice.DocumentNo}`,
                voucherItems,
              })
              await uploadFileToVoucher(voucher.id, uploadBuffer, fileName)
              log.log(`Booked voucher ${voucher.id} for invoice ${invoice.DocumentNo} with category ${effectiveCategoryId}`)
              uploadResult = { id: voucher.id }
            } catch (voucherErr: any) {
              log.log(`Category booking failed for invoice ${invoice.DocumentNo}, falling back to plain upload: ${voucherErr.message}`)
            }
          }

          // Plain file upload — today's default path, and the fallback when the
          // category booking above wasn't requested or failed (fail-soft: the document
          // must still reach Lexoffice even when it can't be pre-categorized).
          if (!uploadResult) {
            uploadResult = await uploadFileToLexoffice(uploadBuffer, fileName, log)
          }

          // Step 3: Mark invoice as uploaded in iDempiere
          try {
            // no milliseconds — iDempiere rejects ".123Z" datetimes
            const uploadDate = toIdempiereDate(new Date())
            log.log(`Marking invoice ${invoiceId} as uploaded to Lexoffice`)
            await event.context.fetch(
              `models/c_invoice/${invoiceId}`,
              'PUT',
              token,
              {
                isUploadToLexoffice: true,
                UploadDateLexoffice: uploadDate
              }
            )
            log.log(`Successfully marked invoice as uploaded`)
          } catch (markErr: any) {
            log.log(`Could not mark invoice as uploaded: ${markErr.message}`)
          }

          log.log(`SUCCESS: Invoice ${invoice.DocumentNo} uploaded to Lexoffice with file ID: ${uploadResult.id}`)
          uploadedCount++
          uploadedIds.push(Number(invoiceId))
        } else {
          const msg = `Invoice ${invoice.DocumentNo}: Could not generate PDF (reportFile is missing)`
          log.log(msg)
          errors.push(msg)
        }
      } catch (pdfErr: any) {
        const msg = `Invoice ${invoice.DocumentNo}: ${pdfErr.message || 'Failed to upload'}`
        log.log(msg)
        errors.push(msg)
      }
    } catch (err: any) {
      const msg = `Error processing invoice ${invoiceId}: ${err.message || 'Unknown error'}`
      log.log(msg)
      errors.push(msg)
    }
  }

  // Worker pool: INVOICE_CONCURRENCY invoices in flight, each fully isolated
  // (processInvoice never throws — every failure is recorded per invoice).
  const queue = [...invoiceIds]
  const workerCount = Math.min(INVOICE_CONCURRENCY, queue.length)
  await Promise.all(Array.from({ length: workerCount }, async () => {
    while (queue.length) {
      const nextId = queue.shift()
      if (nextId === undefined) break
      await processInvoice(nextId)
    }
  }))

  // Keep the per-id outcome lists in the caller's input order (workers finish in
  // arbitrary order).
  const inputOrder = new Map(invoiceIds.map((id, idx) => [Number(id), idx]))
  const byInput = (a: number, b: number) => (inputOrder.get(a) ?? 0) - (inputOrder.get(b) ?? 0)
  uploadedIds.sort(byInput)
  skippedIds.sort(byInput)

  logger.log(`=== SUMMARY: Uploaded ${uploadedCount} invoice(s), ${skipped.length} skipped, ${errors.length} error(s) ===`)

  // Output all logs to console for debugging
  console.log('\n========== LEXOFFICE UPLOAD LOGS ==========')
  logger.getLogs().forEach(log => console.log(log))
  console.log('============================================\n')

  if (uploadedCount > 0) {
    data = {
      status: 200,
      message: `Successfully uploaded ${uploadedCount} invoice(s) to Lexoffice${skipped.length > 0 ? `, ${skipped.length} skipped (already uploaded)` : ''}`,
      uploadedCount,
      uploadedIds,
      skippedCount: skipped.length,
      skippedIds,
      uploadDate: new Date().toISOString(),
      errors: errors.length > 0 ? errors : undefined,
      skipped: skipped.length > 0 ? skipped : undefined,
      logs: logger.getLogs()
    }
  } else if (skipped.length > 0 && errors.length === 0) {
    data = {
      status: 200,
      message: `All ${skipped.length} invoice(s) were already uploaded to Lexoffice`,
      uploadedCount: 0,
      uploadedIds,
      skippedCount: skipped.length,
      skippedIds,
      skipped,
      logs: logger.getLogs()
    }
  } else {
    data = {
      status: 400,
      message: 'No invoices were uploaded to Lexoffice',
      uploadedIds,
      skippedIds,
      errors,
      skipped: skipped.length > 0 ? skipped : undefined,
      logs: logger.getLogs()
    }
  }

  return data
}

export default defineEventHandler(async (event) => {
  let data: any = {}

  try {
    data = await handleFunc(event)
  } catch (err: any) {
    try {
      let authToken: any = await refreshTokenHelper(event)
      data = await handleFunc(event, authToken)
    } catch (error) {
      data = errorHandlingHelper(err?.data ?? err, error?.data ?? error)
      forceLogoutHelper(event, data)
    }
  }

  return data
})
