import { promisify } from 'node:util'
import child_process from 'node:child_process'
import { existsSync, mkdirSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { jsPDF } from 'jspdf'

import fetchHelper from '../../utils/fetchHelper'
import getTokenHelper from '../../utils/getTokenHelper'
import { drawCode128, fitTextLine } from '../../utils/labelBarcode'
import { resolveLabelMedia, type ResolvedMedia } from '../../utils/labelMedia'
import { mediaOption } from '../../utils/cupsMedia'

// ---------------------------------------------------------------------------
// Product barcode labels — POST /api/print/product
//
// Body: { pathName, fileName, labelEntries: [[labelPrinterId(a..f), [entry…]]],
//         customSettings?, downloadOnly? }
// entry: { product, productSKU, productValue, productUPC, organizationId,
//          qtyEntered, labelBarcode (PNG dataURL, fallback only), labelPrinterId }
//
// Page size per printer group = the REAL label size, resolved by
// `resolveLabelMedia()` (app printer registry → CUPS queue default), so
// `lp -o fit-to-page` is a 1:1 mapping. Before, every page was A8 (74×52) and
// CUPS shrank it by height onto e.g. the 35×17 mm labels of labelprinter-2 —
// a third of the label width stayed blank and the barcode was ~20 mm wide.
//
// Two size-driven layouts (barcode = vector CODE128, see labelBarcode.ts):
//   compact (h < 40 mm)  name / SKU + org / barcode / cleartext value
//   table   (h ≥ 40 mm)  wrapped name / SKU · Art.-Nr. + EAN · org + date / barcode
// `customSettings` (Shopify stock page) keeps the legacy fixed-coordinate
// layouts on the legacy page formats — their numbers are tuned to A8.
// ---------------------------------------------------------------------------

const LABEL_PRINTERS: Record<string, string> = {
  a: 'labelprinter-1',
  b: 'labelprinter-2',
  c: 'labelprinter-3',
  d: 'labelprinter-4',
  e: 'labelprinter-5',
  f: 'labelprinter-6'
}
const DEFAULT_QUEUE = 'labelprinter-2'

// Legacy page formats when no media can be resolved (e.g. dev without CUPS).
const LEGACY_FORMAT: Record<string, any> = { b: [35, 17], f: [54, 52] }
const COMPACT_MAX_HEIGHT = 40

const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(max, v))
const PT_MM = 0.3528

const berlinNow = () => new Date().toLocaleString('de-DE', {
  day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin'
}).replace(',', '')

const parseCustomFormat = (format: any): any => {
  if (!format) return null
  const str = String(format)
  if (/^\d+,\d+$/.test(str)) {
    const [w, h] = str.split(',').map(Number)
    return [w, h]
  }
  return str
}

const pageFormatFor = (labelPrinterId: string, media: ResolvedMedia | null, customSettings: any): any => {
  const custom = parseCustomFormat(customSettings?.format)
  if (custom) return custom
  if (media) return [media.w, media.h]
  return LEGACY_FORMAT[labelPrinterId] ?? 'a8'
}

const orientationFor = (format: any): 'l' | 'p' => {
  if (Array.isArray(format)) return format[0] >= format[1] ? 'l' : 'p'
  return 'l'
}

const orgShort = (entry: any, orgNames: Record<string, string>) =>
  orgNames[entry.organizationId] || (entry.organizationId ? String(entry.organizationId).slice(-2) : '')

// Vector barcode with the client PNG as last-resort fallback.
const drawBarcode = (doc: any, entry: any, x: number, y: number, w: number, h: number, maxModuleMm = 0.5) => {
  const code = String(entry.productValue ?? '').trim() || '0000'
  try {
    drawCode128(doc, code, x, y, w, h, { quietMm: 2.5, maxModuleMm })
  } catch (e) {
    if (entry.labelBarcode) {
      doc.addImage(entry.labelBarcode, 'image/png', x + 2.5, y, w - 5, h)
    }
  }
}

// ---------------------------------------------------------------------------
// Compact layout — small labels (35×17 on labelprinter-2, 54×29 on -6).
// Geometry scales with the label height (s = 1 at 17 mm).
//                     ┌──────────────────────────────┬─┐
//   name  (bold)      │ Produktname Zeile 1          │M│
//   name  line 2      │ Produktname Zeile 2 …        │u│  (only if the name wraps)
//   sku               │ SKU: ABC-123                 │s│  (skipped if SKU = Value / empty)
//   barcode (vector)  │ ▌▌ ▌▌▌ ▌ ▌▌ ▌▌▌ ▌ ▌▌ ▌▌▌ ▌   │t│  full width, ≥2.5 mm quiet zones
//   value (bold)      │           1003278            │…│  human-readable barcode content
//                     └──────────────────────────────┴─┘
//   right edge: the org's company name, rotated 90° (bottom → top)
// ---------------------------------------------------------------------------
const drawCompactProductLabel = (doc: any, entry: any, W: number, H: number, orgNames: Record<string, string>) => {
  const s = clamp(H / 17, 1, 1.5)
  const mx = clamp(W * 0.03, 1.0, 3)          // left margin
  const mr = 0.5 * s                             // right margin (company name sits at the edge)
  const my = 0.6 * s
  const name = String(entry.product ?? '').replace('FOREVER ', '')
  const value = String(entry.productValue ?? '').trim()
  const sku = String(entry.productSKU ?? '').trim()
  const showSku = !!sku && sku !== value
  const orgName = String(orgNames[entry.organizationId] ?? '').trim()

  // Right edge — company name rotated 90° (reads bottom → top), vertically centred.
  let cw = W - mx - mr
  if (orgName) {
    doc.setFont('helvetica', 'normal')
    const orgFit = fitTextLine(doc, orgName, H - 2 * my, 5 * s, 4 * s)
    const orgCap = orgFit.fontSize * 0.72 * PT_MM
    const textW = doc.getTextWidth(orgFit.text)
    doc.text(orgFit.text, W - mr, H / 2 + textW / 2, { angle: 90 })
    cw = W - mx - (mr + orgCap + 0.7 * s)
  }

  // Rows 1–2 — product name, up to two lines
  const nameFs = 6.2 * s
  const nameLh = nameFs * PT_MM * 1.08
  doc.setFont('helvetica', 'bold')
  doc.setFontSize(nameFs)
  const lines: string[] = doc.splitTextToSize(name, cw)
  let y = my + nameFs * PT_MM * 0.75
  doc.text(lines[0] || '', mx, y)
  if (lines.length > 1) {
    y += nameLh
    doc.text(fitTextLine(doc, lines.slice(1).join(' '), cw, nameFs, nameFs).text, mx, y)
  }

  // Row 3 — SKU (optional)
  if (showSku) {
    doc.setFont('helvetica', 'normal')
    const skuFs = 5 * s
    y += skuFs * PT_MM * 1.15
    doc.text(fitTextLine(doc, 'SKU: ' + sku, cw, skuFs, 4.5 * s).text, mx, y)
  }

  // Row 5 — human-readable value; Row 4 — barcode fills what is left (≤ 30 mm)
  doc.setFont('helvetica', 'bold')
  const valueFit = fitTextLine(doc, value || '0000', cw, 6 * s, 5 * s)
  const valueCapMm = valueFit.fontSize * 0.72 * PT_MM
  const barcodeTop = y + 1.2 * s                 // clear gap under the text rows
  let valueBaseline = H - 0.5 * s
  let barcodeH = valueBaseline - valueCapMm - 0.35 * s - barcodeTop
  if (barcodeH > 30) {
    barcodeH = 30
    valueBaseline = barcodeTop + barcodeH + 0.35 * s + valueCapMm
  }
  doc.text(valueFit.text, mx + cw / 2, valueBaseline, { align: 'center' })
  drawBarcode(doc, entry, mx, barcodeTop, cw, barcodeH)
}

// ---------------------------------------------------------------------------
// Table layout — larger labels (56×75 on labelprinter-3, 102×152 on -1/-4/-5).
// Adaptive version of the former c/d/e/f fixed layouts (reference: A8 74×52 →
// 12 pt name, 9 pt table with 4 mm rows, 70×20 mm barcode).
// ---------------------------------------------------------------------------
const drawTableProductLabel = (doc: any, entry: any, W: number, H: number, orgNames: Record<string, string>) => {
  const sw = clamp(W / 74, 0.7, 1.6)
  const m = clamp(W * 0.02, 1, 2.5)
  const cw = W - 2 * m
  const name = String(entry.product ?? '').replace('FOREVER ', '')

  // Name — up to 3 wrapped lines
  const nameFs = clamp(12 * sw, 8, 16)
  doc.setFont('helvetica', 'bold')
  doc.setFontSize(nameFs)
  const nameLines: string[] = doc.splitTextToSize(name, cw).slice(0, 3)
  if (nameLines.length === 3) nameLines[2] = fitTextLine(doc, nameLines[2], cw, nameFs, nameFs).text
  const lineH = nameFs * PT_MM * 1.15
  let y = m + nameFs * PT_MM * 0.85
  for (const line of nameLines) { doc.text(line, m, y); y += lineH }

  // Table — 3 rows: SKU | Art.-Nr. + EAN | org + created
  // (start below the last name line's descenders)
  const tableTop = y - lineH + nameFs * PT_MM * 0.3 + 0.8
  const rowH = clamp(4 * sw, 3.2, 6)
  const tableFs = clamp(9 * sw, 6.5, 12)
  const pad = 1
  const textY = (row: number) => tableTop + row * rowH + rowH * 0.72
  const leftW = cw * 0.45
  const narrow = W < 60
  doc.setFont('helvetica', 'normal')
  doc.setFontSize(tableFs)
  doc.setDrawColor(0, 0, 0)
  doc.setLineWidth(0.2)

  doc.rect(m, tableTop, cw, rowH)
  doc.text(fitTextLine(doc, 'SKU: ' + (entry.productSKU ?? ''), cw - 2 * pad, tableFs, tableFs * 0.8).text, m + pad, textY(0))

  doc.rect(m, tableTop + rowH, leftW, rowH)
  doc.setFontSize(tableFs)
  doc.text(fitTextLine(doc, (narrow ? 'Art: ' : 'Artikelnr.: ') + (entry.productValue ?? ''), leftW - 2 * pad, tableFs, tableFs * 0.8).text, m + pad, textY(1))
  doc.rect(m + leftW, tableTop + rowH, cw - leftW, rowH)
  doc.setFontSize(tableFs)
  doc.text(fitTextLine(doc, 'EAN: ' + (entry.productUPC ?? ''), cw - leftW - 2 * pad, tableFs, tableFs * 0.8).text, m + leftW + pad, textY(1))

  doc.rect(m, tableTop + 2 * rowH, leftW, rowH)
  doc.setFontSize(tableFs)
  doc.text(fitTextLine(doc, orgShort(entry, orgNames), leftW - 2 * pad, tableFs, tableFs * 0.8).text, m + pad, textY(2))
  doc.rect(m + leftW, tableTop + 2 * rowH, cw - leftW, rowH)
  doc.setFontSize(tableFs)
  doc.text(fitTextLine(doc, 'Erst.: ' + berlinNow(), cw - leftW - 2 * pad, tableFs, tableFs * 0.8).text, m + leftW + pad, textY(2))

  // Barcode — directly under the table, height ≤ 30 mm; wider modules are
  // allowed on big labels (4×6 in) so the code doesn't look lost.
  const areaTop = tableTop + 3 * rowH + Math.min(3, Math.max(1.5, H * 0.03))
  const areaH = H - m - areaTop
  const barcodeH = Math.min(areaH, 30)
  const maxModuleMm = clamp(W / 120, 0.5, 0.8)
  if (barcodeH > 3) drawBarcode(doc, entry, m, areaTop, cw, barcodeH, maxModuleMm)
}

// ---------------------------------------------------------------------------
// Legacy fixed layouts — only used when the caller passes `customSettings`
// (Shopify stock page); coordinates are tuned to the A8 / 54×52 page formats.
// ---------------------------------------------------------------------------
const drawLegacyLabel = (doc: any, entry: any, orgNames: Record<string, string>, s: any) => {
  if(entry.labelPrinterId === 'b') {
    doc.setFontSize(s?.productFontSize ?? 18)
    doc.text(String(entry.product.replace("FOREVER ", "")), s?.productX ?? 0, s?.productY ?? 6)
    doc.setFontSize(s?.skuFontSize ?? 15)
    doc.text(String( 'SKU:'+ entry.productSKU+ ' - '+ entry.organizationId?.toString().slice(-2)), s?.skuX ?? 0, s?.skuY ?? 13)
    doc.addImage(entry.labelBarcode, 'image/png', s?.barcodeX ?? 1, s?.barcodeY ?? 16, s?.barcodeW ?? 74, s?.barcodeH ?? 32)
  } else if(entry.labelPrinterId === 'c') {
    doc.setFontSize(s?.productFontSize ?? 12)
    const productText = String(entry.product.replace("FOREVER ", ""))
    const splitProduct = doc.splitTextToSize(productText, s?.productW ?? 70)
    doc.text(splitProduct, s?.productX ?? 0, s?.productY ?? 5)
    const productLines = Math.min(splitProduct.length, 3)
    const tableStart = (s?.productY ?? 5) + (productLines * 5)
    doc.setFontSize(s?.tableFontSize ?? 9)
      doc.rect(1, tableStart, 71, 4)
      doc.text(String('SKU: ' + entry.productSKU), 2, tableStart + 3)
      doc.rect(1, tableStart + 4, 32, 4)
      doc.text(String('Artikelnr.:' + entry.productValue), 2, tableStart + 7)
      doc.rect(33, tableStart + 4, 39, 4)
      doc.text(String('EAN: ' + entry.productUPC), 35, tableStart + 7)
      doc.rect(1, tableStart + 8, 32, 4)
      doc.text(String(orgShort(entry, orgNames)), 2, tableStart + 11)
      doc.rect(33, tableStart + 8, 39, 4)
      doc.text(String(berlinNow()), 35, tableStart + 11)
      doc.setFontSize(12)
    doc.addImage(entry.labelBarcode, 'image/png', s?.barcodeX ?? 2, tableStart + 13, s?.barcodeW ?? 70, s?.barcodeH ?? 20)
  } else if(entry.labelPrinterId === 'd' || entry.labelPrinterId === 'e') {
    doc.setFontSize(s?.productFontSize ?? 12)
    const productTextD = String(entry.product.replace("FOREVER ", ""))
    const splitProductD = doc.splitTextToSize(productTextD, s?.productW ?? 70)
    doc.text(splitProductD, s?.productX ?? 0, s?.productY ?? 5)
    doc.setFontSize(s?.tableFontSize ?? 9)
    const tableStartD = s?.tableStartY ?? 19
      doc.rect(1, tableStartD, 71, 4)
      doc.text(String('SKU: ' + entry.productSKU), 2, tableStartD + 3)
      doc.rect(1, tableStartD + 4, 32, 4)
      doc.text(String('Artikelnr.:' + entry.productValue), 2, tableStartD + 7)
      doc.rect(33, tableStartD + 4, 39, 4)
      doc.text(String('EAN: ' + entry.productUPC), 35, tableStartD + 7)
      doc.rect(1, tableStartD + 8, 32, 4)
      doc.text(String(orgShort(entry, orgNames)), 2, tableStartD + 11)
      doc.rect(33, tableStartD + 8, 39, 4)
      doc.text(String((entry.labelPrinterId === 'e' ? 'Erst.:' : '') + berlinNow()), 35, tableStartD + 11)
      doc.setFontSize(12)
    doc.addImage(entry.labelBarcode, 'image/png', s?.barcodeX ?? 2, s?.barcodeY ?? 32, s?.barcodeW ?? 70, s?.barcodeH ?? 20)
  } else if(entry.labelPrinterId === 'f') {
    doc.setFontSize(s?.productFontSize ?? 12)
    const productTextF = String(entry.product.replace("FOREVER ", ""))
    const splitProductF = doc.splitTextToSize(productTextF, s?.productW ?? 52)
    doc.text(splitProductF, s?.productX ?? 0, s?.productY ?? 6)
    const productLinesF = Math.min(splitProductF.length, 3)
    const tableStartF = (s?.productY ?? 6) + (productLinesF * 5)
    doc.setFontSize(s?.tableFontSize ?? 9)
      doc.rect(1, tableStartF, 51, 4)
      doc.text(String('SKU: ' + entry.productSKU), 2, tableStartF + 3)
      doc.rect(1, tableStartF + 4, 25, 4)
      doc.text(String('Art:' + entry.productValue), 2, tableStartF + 7)
      doc.rect(26, tableStartF + 4, 26, 4)
      doc.text(String('EAN: ' + entry.productUPC), 27, tableStartF + 7)
      doc.rect(1, tableStartF + 8, 25, 4)
      doc.text(String(orgShort(entry, orgNames)), 2, tableStartF + 11)
      doc.rect(26, tableStartF + 8, 26, 4)
      doc.text(String('Erst:' + berlinNow()), 27, tableStartF + 11)
      doc.setFontSize(12)
    doc.addImage(entry.labelBarcode, 'image/png', s?.barcodeX ?? 2, tableStartF + 14, s?.barcodeW ?? 50, s?.barcodeH ?? 18)
  } else {
    doc.setFontSize(18)
    doc.text(String(entry.product.replace("FOREVER ", "")), 0, 8)
    doc.setFontSize(14)
    doc.text(String( 'SKU: '+ entry.productSKU + ' | VAL: ' + entry.productValue+ ' | ORG: '+ entry.organizationId?.toString().slice(-2)), 0, 17)
    doc.addImage(entry.labelBarcode, 'image/png', 9, 20, 60, 28)
  }
}

const drawLabel = (doc: any, entry: any, orgNames: Record<string, string>, customSettings: any) => {
  if (customSettings) {
    drawLegacyLabel(doc, entry, orgNames, customSettings)
    return
  }
  const W = doc.internal.pageSize.getWidth()
  const H = doc.internal.pageSize.getHeight()
  if (H < COMPACT_MAX_HEIGHT) {
    drawCompactProductLabel(doc, entry, W, H, orgNames)
  } else {
    drawTableProductLabel(doc, entry, W, H, orgNames)
  }
}

const serverPrinting = async (item: any) => {
  const exec = promisify(child_process.exec)
  const labelPrinter = LABEL_PRINTERS[item.labelEntry] || DEFAULT_QUEUE

  // Explicit `-o media=` only when the size comes from the app registry (an
  // intentional override of the queue default); a CUPS-detected size IS the
  // queue default already. fit-to-page stays as the safety net.
  const options = ['-o fit-to-page']
  if (item.media?.source === 'registry') options.push(mediaOption(item.media))

  // Report the TRUE outcome. `lp` exits non-zero (→ exec throws) when the queue
  // is unknown/disabled/not-accepting, or when the file can't be written — so a
  // throw here means the label did NOT print. Previously this error was swallowed
  // and the caller counted "PDF generated" as "printed", hiding every failure.
  const result: any = { ok: false, printer: labelPrinter, options: options.join(' '), media: item.media ?? null, stdout: '', stderr: '', error: '' }
  try {
    if(!existsSync(item.filePath)) {
      mkdirSync(item.filePath, { recursive: true })
    }
    await writeFile(`${item.filePath}/${item.fileName}`, Buffer.from(item.fileContent, 'base64'))

    const { stdout, stderr } = await exec(`lp -d ${labelPrinter} ${options.join(' ')} ${item.filePath}/${item.fileName}`);

    result.stdout = stdout || ''
    result.stderr = stderr || ''
    result.ok = true
  } catch(err: any) {
    result.error = err?.stderr || err?.message || err?.data?.message || 'Error when printing via server side'
    result.stderr = err?.stderr || ''
    result.ok = false
  }
  return result
}

export default defineEventHandler(async (event) => {
  let data: any = {}
  const body = await readBody(event)
  const pathName = body.pathName
  const filePath = `/root/storage/${pathName}`
  const fileName = body.fileName
  const labelEntries = body.labelEntries
  const customSettings = body.customSettings || null
  // When true, generate the PDFs server-side and return them, but do NOT
  // send them to the physical label printer. Used by the "Download Labels"
  // button on receipt edit so users can grab the file without reprinting.
  const downloadOnly = body.downloadOnly === true

  data['path'] = filePath
  data['name'] = fileName
  data['contents'] = []
  data['printResults'] = []

  // Fetch org company names once for the entire print session
  const orgNames: Record<string, string> = {}
  try {
    const orgIds = [...new Set(labelEntries.flatMap(([, vals]) => vals.flat().map((e: any) => e.organizationId)).filter(Boolean))]
    if (orgIds.length > 0) {
      const token = await getTokenHelper(event)
      const orgFilter = orgIds.map(id => `AD_Org_ID eq ${id}`).join(' or ')
      const orgRes: any = await fetchHelper(event, `models/ad_org?$filter=${orgFilter}&$select=AD_Org_ID,companyname,Name`, 'GET', token, null)
      for (const org of orgRes?.records || []) {
        orgNames[org.id] = org.companyname || org.Name || ''
      }
    }
  } catch (e) {
    // fallback: orgNames stays empty, label will use organizationId
  }

  for(const [labelEntry, labelValues] of labelEntries) {
    const queue = LABEL_PRINTERS[labelEntry] || DEFAULT_QUEUE
    let media: ResolvedMedia | null = null
    if (!customSettings) {
      try { media = await resolveLabelMedia(event, queue) } catch { media = null }
    }
    const pageFormat = pageFormatFor(labelEntry, media, customSettings)
    const orientation = orientationFor(pageFormat)

    const doc = new jsPDF({
      orientation,
      unit: 'mm',
      format: pageFormat,
      putOnlyUsedFonts: true,
      compress: true
    })

    // The constructor already opened page 1 — draw the first label on it and
    // add a page (same format) for every further label. (The previous
    // addPage()/setPage(no) dance drew each label on the page created for the
    // NEXT one, so the first label of a batch landed on a default-format page.)
    let count = 0
    for(let entry of labelValues.flat()) {
      for(let i = 0; i < Number(entry.qtyEntered || 0); i++) {
        if (count > 0) doc.addPage(pageFormat, orientation)
        drawLabel(doc, entry, orgNames, customSettings)
        count++
      }
    }
    if (count === 0) continue

    const fileContent = doc.output('dataurlstring')?.replace('data:application/pdf;filename=generated.pdf;base64,', '')
    data['contents'].push({
      label: labelEntry,
      printer: queue,
      media,
      pages: count,
      content: fileContent
    })
    if (!downloadOnly) {
      const resPrint = await serverPrinting({
        filePath,
        fileName: fileName.replace('-LABELS-', `-LABELPRINTER-${labelEntry}-`),
        fileContent,
        labelEntry,
        media
      })
      if(resPrint) {
        data['print_result'] = resPrint            // kept for backward-compat (last group)
        data['printResults'].push({ labelEntry, ...resPrint })
      }
    }
  }

  // Honest overall verdict: did EVERY printer group actually reach the printer?
  // (Only meaningful when we actually printed — download-only never prints.)
  if (!downloadOnly) {
    data['printOk'] = data['printResults'].length > 0 && data['printResults'].every((r: any) => r.ok)
  }

  return data
})
