/**
 * Unit tracking on goods receipt — shared client logic for the desktop page
 * (integrations/receive-desktop), the mobile page (mobile/receive) and the
 * capture modal. Pure helpers over a per-line `tracking` object; the server
 * (`server/utils/attributeTracking.ts`, called from POST /api/inouts/store)
 * turns the payload into ASIs + iDempiere lines.
 *
 *   line.tracking = {
 *     attributeSetId, mode: 'none'|'serial'|'charge'|'mhd'|'both',
 *     isSerNo, isLot, isGuaranteeDate, setName,
 *     serials: string[],                       // one per unit
 *     charges: [{ lot, mhd (ISO), qty }]       // batches
 *   }
 */
export type TrackingMode = 'none' | 'serial' | 'charge' | 'mhd' | 'both'

export interface ChargeGroup { lot: string; mhd: string; qty: number }
export interface LineTracking {
  attributeSetId: number | null
  mode: TrackingMode
  isSerNo: boolean
  isLot: boolean
  isGuaranteeDate: boolean
  setName: string
  serials: string[]
  charges: ChargeGroup[]
}

export const TRACKING_MODE_LABELS: Record<TrackingMode, string> = {
  none: 'Kein Tracking',
  serial: 'Seriennummer',
  charge: 'Charge (Lot + MHD)',
  mhd: 'Nur MHD (ohne Charge)',
  both: 'Seriennummer + Charge'
}

export const createTracking = (info?: any): LineTracking => ({
  attributeSetId: Number(info?.attributeSetId) || null,
  mode: (info?.mode as TrackingMode) || 'none',
  isSerNo: !!info?.isSerNo,
  isLot: !!info?.isLot,
  isGuaranteeDate: !!info?.isGuaranteeDate,
  setName: String(info?.setName ?? ''),
  serials: [],
  charges: []
})

/** Apply a fresh server `tracking` info (after enabling a mode) without losing captured data. */
export const applyTrackingInfo = (t: LineTracking, info: any) => {
  t.attributeSetId = Number(info?.attributeSetId) || null
  t.mode = (info?.mode as TrackingMode) || 'none'
  t.isSerNo = !!info?.isSerNo
  t.isLot = !!info?.isLot
  t.isGuaranteeDate = !!info?.isGuaranteeDate
  t.setName = String(info?.setName ?? info?.name ?? '')
  return t
}

export const isBatchTracked = (t?: LineTracking | null) => !!t && (t.isLot || t.isGuaranteeDate)
export const isTracked = (t?: LineTracking | null) => !!t && (t.isSerNo || t.isLot || t.isGuaranteeDate)

/** Serials are opaque: trim, strip line breaks, collapse inner whitespace — never change case. */
export const normalizeSerial = (raw: any) => String(raw ?? '').replace(/[\r\n]/g, '').replace(/\s+/g, ' ').trim()

/** All serials captured on the receipt (for the cross-line duplicate check). */
export const allSerials = (lines: any[]) => {
  const out = new Map<string, any>()
  for (const l of lines ?? []) for (const s of l?.tracking?.serials ?? []) if (!out.has(s)) out.set(s, l)
  return out
}

export const addSerial = (line: any, raw: any, lines: any[] = []): { ok: boolean; reason?: 'empty' | 'duplicate'; serial: string; line?: any } => {
  const serial = normalizeSerial(raw)
  if (!serial) return { ok: false, reason: 'empty', serial }
  const t: LineTracking = line.tracking
  if (t.serials.includes(serial)) return { ok: false, reason: 'duplicate', serial, line }
  const other = allSerials(lines).get(serial)
  if (other && other !== line) return { ok: false, reason: 'duplicate', serial, line: other }
  t.serials.push(serial)
  return { ok: true, serial }
}

export const removeSerial = (line: any, index: number) => {
  const t: LineTracking = line.tracking
  if (index >= 0 && index < t.serials.length) t.serials.splice(index, 1)
}

export const addCharge = (line: any, group: Partial<ChargeGroup> = {}) => {
  const t: LineTracking = line.tracking
  const qty = group.qty ?? Math.max(remainder(line), 0)
  const g: ChargeGroup = { lot: String(group.lot ?? ''), mhd: String(group.mhd ?? ''), qty: Number(qty) || 0 }
  t.charges.push(g)
  return g
}

export const removeCharge = (line: any, index: number) => {
  const t: LineTracking = line.tracking
  if (index >= 0 && index < t.charges.length) t.charges.splice(index, 1)
}

export const chargeSum = (line: any) => (line?.tracking?.charges ?? []).reduce((a: number, c: ChargeGroup) => a + (Number(c.qty) || 0), 0)

export const chargeErrors = (line: any): string[] => {
  const out: string[] = []
  const t: LineTracking | undefined = line?.tracking
  if (!t) return out
  t.charges.forEach((c, i) => {
    if (!(Number(c.qty) > 0)) out.push(`Charge ${i + 1}: Menge muss größer 0 sein`)
    if (!String(c.lot ?? '').trim() && !c.mhd) out.push(`Charge ${i + 1}: weder Lot noch MHD angegeben`)
  })
  return out
}

/**
 * Units covered by tracking data. For set "both" the charge groups DESCRIBE the
 * serialised units (serials are assigned to charges sequentially, see
 * buildTrackingPayload), so they must not be double-counted.
 */
export const trackedUnits = (line: any) => {
  const t: LineTracking | undefined = line?.tracking
  const sn = t?.serials?.length ?? 0
  const ch = chargeSum(line)
  return t?.isSerNo && isBatchTracked(t) ? Math.max(sn, ch) : sn + ch
}
export const remainder = (line: any) => Number(line?.qtyEntered ?? 0) - trackedUnits(line)

export type TrackingStatus = 'complete' | 'partial' | 'over' | 'none'
export const trackingStatus = (line: any): TrackingStatus => {
  const t: LineTracking | undefined = line?.tracking
  if (!t || !isTracked(t)) return 'none'
  const used = trackedUnits(line)
  const qty = Number(line?.qtyEntered ?? 0)
  if (used === 0) return 'none'
  if (used > qty) return 'over'
  return used === qty ? 'complete' : 'partial'
}

/** Short chip text, e.g. "SN 3/5", "Charge 12/12", "SN 2 · Ch 3 / 5", "Tracking…". */
export const trackingBadge = (line: any): string => {
  const t: LineTracking | undefined = line?.tracking
  if (!t || !isTracked(t)) return 'Tracking…'
  const qty = Number(line?.qtyEntered ?? 0)
  const sn = t.serials.length
  const ch = chargeSum(line)
  if (t.isSerNo && isBatchTracked(t)) {
    if (!sn && !ch) return 'SN / Charge'
    return `SN ${sn}${ch ? ` · Ch ${ch}` : ''} / ${qty}`
  }
  if (t.isSerNo) return `SN ${sn}/${qty}`
  return `${t.isLot ? 'Charge' : 'MHD'} ${ch}/${qty}`
}

export interface TrackingSummary {
  incomplete: { productName: string; productValue: string; tracked: number; qty: number }[]
  over: { productName: string; productValue: string; tracked: number; qty: number }[]
  errors: string[]
  totalUnits: number
}

export const summarizeTracking = (lines: any[]): TrackingSummary => {
  const s: TrackingSummary = { incomplete: [], over: [], errors: [], totalUnits: 0 }
  for (const l of lines ?? []) {
    const t: LineTracking | undefined = l?.tracking
    if (!t || !isTracked(t)) continue
    const used = trackedUnits(l)
    const qty = Number(l?.qtyEntered ?? 0)
    s.totalUnits += used
    const row = { productName: l.productName, productValue: l.productValue, tracked: used, qty }
    if (used > qty) s.over.push(row)
    else if (used < qty) s.incomplete.push(row)
    for (const e of chargeErrors(l)) s.errors.push(`${l.productValue}: ${e}`)
  }
  return s
}

/** Server payload for one line — undefined when nothing was captured (plain line). */
export const buildTrackingPayload = (line: any) => {
  const t: LineTracking | undefined = line?.tracking
  if (!t || !t.attributeSetId || !isTracked(t)) return undefined
  const charges = t.charges
    .filter((c) => Number(c.qty) > 0 && (String(c.lot ?? '').trim() || c.mhd))
    .map((c) => ({ lot: String(c.lot ?? '').trim(), mhd: c.mhd || null, qty: Number(c.qty) }))
  if (!t.serials.length && !charges.length) return undefined

  // Set "both": each serial belongs to a charge — assign sequentially by group
  // qty; the charge groups then only carry their non-serialised remainder.
  if (t.isSerNo && isBatchTracked(t) && charges.length) {
    const serials: any[] = []
    const rest: any[] = []
    let i = 0
    for (const c of charges) {
      let left = c.qty
      while (left > 0 && i < t.serials.length) { serials.push({ serNo: t.serials[i++], lot: c.lot, mhd: c.mhd }); left-- }
      if (left > 0) rest.push({ ...c, qty: left })
    }
    while (i < t.serials.length) serials.push({ serNo: t.serials[i++] })
    return { attributeSetId: t.attributeSetId, serials, charges: rest }
  }
  return { attributeSetId: t.attributeSetId, serials: [...t.serials], charges }
}

// --- dates ------------------------------------------------------------------------

/** dd.mm.yyyy / d.m.yy / yyyy-mm-dd / yyyymmdd / dd/mm/yyyy → ISO, or null when invalid. */
export const parseDateFlexible = (raw: any): string | null => {
  const s = String(raw ?? '').trim()
  if (!s) return ''
  let y: string, mo: string, d: string
  let m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/)
  if (m) { [, y, mo, d] = m }
  else if ((m = s.match(/^(\d{4})(\d{2})(\d{2})$/))) { [, y, mo, d] = m }
  else if ((m = s.match(/^(\d{1,2})[.\/\-\s](\d{1,2})[.\/\-\s](\d{2}|\d{4})$/))) { [, d, mo, y] = m; if (y.length === 2) y = '20' + y }
  else if ((m = s.match(/^(\d{2})(\d{2})(\d{4})$/))) { [, d, mo, y] = m }
  else return null
  const dt = new Date(Date.UTC(+y, +mo - 1, +d))
  if (dt.getUTCFullYear() !== +y || dt.getUTCMonth() !== +mo - 1 || dt.getUTCDate() !== +d) return null
  return `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`
}

export const formatDateDe = (iso: any) => {
  const m = String(iso ?? '').match(/^(\d{4})-(\d{2})-(\d{2})/)
  return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
}

// --- CSV import -------------------------------------------------------------------

export interface CsvTrackingRow { serial: string; lot: string; mhd: string; qty: number; line: number }
export interface CsvParseResult { rows: CsvTrackingRow[]; errors: { line: number; message: string }[]; delimiter: string; hadHeader: boolean }

const HEADER_SERIAL = /^(serial|seriennummer|serialnumber|serial[_ ]?no|sn|seriennr)/i
const HEADER_LOT = /^(lot|charge|batch|chargennummer|lot[_ ]?no)/i
const HEADER_MHD = /^(mhd|bbd|expiry|expires|verfall|verfallsdatum|guarantee|haltbar|best[_ ]?before|ablauf)/i
const HEADER_QTY = /^(qty|menge|quantity|anzahl|stk)/i

const splitCsvLine = (line: string, delimiter: string): string[] => {
  const out: string[] = []
  let cur = ''
  let quoted = false
  for (let i = 0; i < line.length; i++) {
    const ch = line[i]
    if (ch === '"') {
      if (quoted && line[i + 1] === '"') { cur += '"'; i++ } else quoted = !quoted
    } else if (ch === delimiter && !quoted) {
      out.push(cur); cur = ''
    } else cur += ch
  }
  out.push(cur)
  return out.map((c) => c.trim())
}

/**
 * Parse a serial / charge CSV. Delimiter autodetected (; , tab) on the first
 * non-empty line; header optional; columns positional `serial[;lot[;mhd[;qty]]]`
 * or by header name. Rows without a serial but with lot/MHD become charge rows.
 */
export const parseTrackingCsv = (text: string): CsvParseResult => {
  const res: CsvParseResult = { rows: [], errors: [], delimiter: ';', hadHeader: false }
  const lines = String(text ?? '').replace(/^﻿/, '').split(/\r?\n/)
  const first = lines.find((l) => l.trim())
  if (!first) return res
  const counts = { ';': (first.match(/;/g) || []).length, ',': (first.match(/,/g) || []).length, '\t': (first.match(/\t/g) || []).length }
  res.delimiter = (Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] as string) || ';'
  if (counts[res.delimiter as keyof typeof counts] === 0) res.delimiter = ';'

  let col = { serial: 0, lot: 1, mhd: 2, qty: 3 }
  let startAt = 0
  const head = splitCsvLine(first, res.delimiter)
  const headIdx = lines.indexOf(first)
  if (head.some((h) => HEADER_SERIAL.test(h) || HEADER_LOT.test(h) || HEADER_MHD.test(h))) {
    res.hadHeader = true
    startAt = headIdx + 1
    col = { serial: -1, lot: -1, mhd: -1, qty: -1 }
    head.forEach((h, i) => {
      if (col.serial < 0 && HEADER_SERIAL.test(h)) col.serial = i
      else if (col.lot < 0 && HEADER_LOT.test(h)) col.lot = i
      else if (col.mhd < 0 && HEADER_MHD.test(h)) col.mhd = i
      else if (col.qty < 0 && HEADER_QTY.test(h)) col.qty = i
    })
    // a trailing unlabelled column after the header is treated as the quantity
    if (col.qty < 0) col.qty = head.length
  } else {
    startAt = headIdx
  }

  for (let i = startAt; i < lines.length; i++) {
    const raw = lines[i]
    if (!raw.trim()) continue
    const cells = splitCsvLine(raw, res.delimiter)
    const pick = (idx: number) => (idx >= 0 && idx < cells.length ? cells[idx] : '')
    const serial = normalizeSerial(pick(col.serial))
    const lot = String(pick(col.lot) ?? '').trim()
    const mhdRaw = pick(col.mhd)
    const qtyRaw = pick(col.qty)
    const mhd = parseDateFlexible(mhdRaw)
    if (mhd === null) { res.errors.push({ line: i + 1, message: `Ungültiges Datum "${mhdRaw}"` }); continue }
    const qty = qtyRaw ? Number(String(qtyRaw).replace(',', '.')) : 1
    if (!Number.isFinite(qty) || qty <= 0) { res.errors.push({ line: i + 1, message: `Ungültige Menge "${qtyRaw}"` }); continue }
    if (!serial && !lot && !mhd) { res.errors.push({ line: i + 1, message: 'Leere Zeile (weder Seriennummer noch Lot/MHD)' }); continue }
    res.rows.push({ serial, lot, mhd: mhd || '', qty, line: i + 1 })
  }
  return res
}

/**
 * Merge parsed CSV rows into a line: serial rows → serials (duplicates reported),
 * serial-less rows → charge groups aggregated by (lot, mhd).
 */
export const applyCsvRows = (line: any, rows: CsvTrackingRow[], lines: any[] = []) => {
  const t: LineTracking = line.tracking
  const added: string[] = []
  const duplicates: string[] = []
  const charges = new Map<string, ChargeGroup>()
  for (const r of rows) {
    if (r.serial) {
      if (!t.isSerNo) { duplicates.push(`${r.serial} (Artikel ohne Seriennummern-Tracking)`); continue }
      const a = addSerial(line, r.serial, lines)
      if (a.ok) added.push(r.serial); else if (a.reason === 'duplicate') duplicates.push(r.serial)
      // for set "both" a serial row may also carry its lot/MHD → keep as charge info
      if (a.ok && isBatchTracked(t) && (r.lot || r.mhd)) {
        const key = `${r.lot}|${r.mhd}`
        const g = charges.get(key) ?? { lot: r.lot, mhd: r.mhd, qty: 0 }
        g.qty += 1
        charges.set(key, g)
      }
      continue
    }
    if (!isBatchTracked(t)) continue
    const key = `${r.lot}|${r.mhd}`
    const g = charges.get(key) ?? { lot: r.lot, mhd: r.mhd, qty: 0 }
    g.qty += r.qty
    charges.set(key, g)
  }
  for (const g of charges.values()) {
    const existing = t.charges.find((c) => c.lot === g.lot && c.mhd === g.mhd)
    if (existing) existing.qty += g.qty; else t.charges.push(g)
  }
  return { added, duplicates, chargeGroups: charges.size }
}

/** Pre-flight against existing stock; returns the server's `known` list (fail-soft → []). */
export const checkSerialsOnServer = async (serials: string[], organizationId?: any) => {
  const list = Array.from(new Set(serials.map(normalizeSerial).filter(Boolean)))
  if (!list.length) return [] as { serNo: string; asiId: number; inStock: boolean; lot: string; mhd: string }[]
  try {
    const res: any = await $fetch('/api/inouts/check-serials', {
      method: 'POST',
      headers: useRequestHeaders(['cookie']),
      body: { organizationId: organizationId || undefined, serials: list }
    })
    return (res?.known ?? []) as { serNo: string; asiId: number; inStock: boolean; lot: string; mhd: string }[]
  } catch {
    return []
  }
}

/** Enable / change tracking on a product; returns the server's tracking info or throws with the iDempiere message. */
export const setProductTrackingMode = async (productId: any, mode: TrackingMode) => {
  const res: any = await $fetch(`/api/materials/products/${productId}/tracking`, {
    method: 'POST',
    headers: useRequestHeaders(['cookie']),
    body: { mode }
  })
  if (Number(res?.status) !== 200) throw new Error(res?.message || 'Tracking konnte nicht gesetzt werden')
  return { attributeSetId: res.attributeSetId, mode: res.mode, ...(res.flags ?? {}), setName: res.name }
}

export const useReceiptTracking = () => ({
  TRACKING_MODE_LABELS,
  createTracking, applyTrackingInfo, isTracked, isBatchTracked,
  normalizeSerial, addSerial, removeSerial, allSerials,
  addCharge, removeCharge, chargeSum, chargeErrors,
  trackedUnits, remainder, trackingStatus, trackingBadge, summarizeTracking, buildTrackingPayload,
  parseDateFlexible, formatDateDe, parseTrackingCsv, applyCsvRows,
  checkSerialsOnServer, setProductTrackingMode
})
