/**
 * Book ONE Amazon Ads invoice as an iDempiere AP invoice (c_invoice,
 * IsSOTrx=false) and attach the original Amazon PDF to it.
 *
 * Port of the Eingangsrechnungen confirm route (accounting/incoming-invoices/
 * [id]/confirm.post.ts) reduced to what this vendor needs:
 *   - vendor is the FIXED Amazon Ads partner (AMAZON_ADS_VENDOR_BPARTNER_ID,
 *     default 1043801 "Amazon Online Germany GmbH", org * → usable from every
 *     org). Self-heals the partner on first use: IsVendor, TaxID (only when
 *     empty), a bill-to location from the invoice's issuer address;
 *   - one CHARGE line per campaign line (no products) + an adjustment line when
 *     the campaign sum differs from the invoice net (promotions/rounding), so the
 *     booked net always equals the document;
 *   - line tax resolved by RATE (19 % on a German Ads invoice) so GrandTotal
 *     equals the document's gross — the bank reconciliation proposes the gross;
 *   - DocumentNo = POReference = the Amazon invoice number; duplicate guard on
 *     (vendor, DocumentNo, IsSOTrx=false); VO/RE predecessors get their number
 *     freed (freeVoidedInvoiceNumbers) so a voided invoice can be re-imported;
 *   - complete (doc-action CO) fail-soft; attach the PDF under PO_Invoice
 *     (the table the procurements invoice page + the Lexoffice upload read).
 * EXACT VAT (cent differences): Amazon rounds the VAT DOWN (74,66 × 19 % = 14,1854 → 14,18), iDempiere
 * rounds half-up (14,19) — and with a DOCUMENT-LEVEL tax rate it recomputes the VAT at completion, so a
 * value we send is overwritten and GrandTotal ends up one cent off the Amazon document. iDempiere keeps a
 * manually entered line TaxAmt only on PURCHASE documents whose tax rate is NOT document level
 * (MInvoiceLine.beforeSave / MInvoiceTax.calculateTaxFromLines, "manual tax should never be amended").
 * So the importer prefers a purchase tax of the right rate with IsDocumentLevel = N (a dedicated rate the
 * user creates, e.g. "19% Vorsteuer (Zeilensteuer)" — the shared "19% Mwst." must stay document level for
 * sales) and then writes the VAT from the PDF on every line; without such a rate it behaves as before and
 * reports the cent difference as a warning.
 *
 * Never throws for business errors — returns { status, message } so the batch
 * route can continue with the next invoice.
 *
 * SELLER FEE invoices (inv.kind === 'fee', parseSellerFeeInvoicePdf.ts) run through
 * the same importer with three differences:
 *   - the vendor is NOT fixed: several Amazon entities issue them (Amazon EU
 *     S.à r.l. Niederlassung Deutschland, Amazon Services Europe S.à r.l., …), so
 *     it is resolved by the issuer's VAT id (resolveFeeVendor) and created under
 *     org * on first use;
 *   - the tax is resolved PER LINE (rows of one invoice can carry different rates);
 *   - CREDIT NOTES (Amazon "Steuergutschrift", inv.isCreditNote / negative totals —
 *     refunds of seller fees) are booked as a vendor credit memo: doctype with
 *     DocBaseType 'APC' ("AP CreditMemo" 1000006) and POSITIVE amounts — the
 *     document type carries the sign, same as the existing APC documents. Amazon
 *     is the supplier correcting its own fee invoice, so it is a PURCHASE credit
 *     note (we get money back), never an AR credit memo. OPOS and /sales/invoices
 *     count 1000006 as customer-side only for partners flagged IsCustomer, so these
 *     vendor documents stay on the payables side.
 */
import { string } from 'alga-js'
import fetchHelper from '../fetchHelper'
import { attachToStrapi } from '../inbox/strapiAttach'
import { freeVoidedInvoiceNumbers } from '../inbox/freeInvoiceNo'
import type { AdsInvoice } from './types'
import { PAYMENT_METHOD_LABELS, round2 } from './types'

const fkId = (v: any) => (v && typeof v === 'object') ? (v.id ?? null) : (v ?? null)
const enc = (s: string) => string.urlEncode(s)
const isYes = (v: any) => v === true || v === 'Y' || v === 'true'

export interface ImportOptions {
  organizationId: number
  vendorId: number
  chargeId: number
  taxId?: number | null
  pdf?: Buffer | null
  pdfName?: string | null
  user?: string | null
}

export interface ImportResult {
  status: number
  message?: string
  cInvoiceId?: number
  documentNo?: string
  completed?: boolean
  attached?: boolean
  warnings?: string[]
}

/* ---- seller-fee invoices: vendor by issuer VAT id (find → create) ---- */
const feeVendorCache = new Map<string, { id: number; at: number }>()
export const findFeeVendorId = async (event: any, token: string, vatId?: string | null): Promise<number | null> => {
  const vat = String(vatId || '').replace(/[^A-Za-z0-9]/g, '').toUpperCase()
  if (!vat) return null
  const hit = feeVendorCache.get(vat)
  if (hit && Date.now() - hit.at < 10 * 60 * 1000) return hit.id
  const res: any = await fetchHelper(event, `models/c_bpartner?$filter=${enc(`TaxID eq '${vat}' AND IsActive eq true`)}&$top=10`, 'GET', token, null)
  const recs: any[] = res?.records || []
  // prefer an existing vendor, then the shared (org *) record
  const pick = recs.find(r => isYes(r.IsVendor) && fkId(r.AD_Org_ID) === 0) || recs.find(r => isYes(r.IsVendor)) || recs.find(r => fkId(r.AD_Org_ID) === 0) || recs[0]
  if (!pick?.id) return null
  feeVendorCache.set(vat, { id: pick.id, at: Date.now() })
  return pick.id
}

export const resolveFeeVendor = async (event: any, token: string, organizationId: number, inv: AdsInvoice, warnings: string[]): Promise<number> => {
  const vat = String(inv.issuer?.vatId || '').replace(/[^A-Za-z0-9]/g, '').toUpperCase()
  if (!vat) throw new Error(`Rechnung ${inv.invoiceNo}: keine USt-ID des Leistungserbringers erkannt — Lieferant kann nicht zugeordnet werden`)
  const found = await findFeeVendorId(event, token, vat)
  if (found) return found
  // shared across orgs like the Ads vendor → org * first, login org as fallback
  let lastErr: any = null
  for (const orgId of [0, organizationId]) {
    try {
      const res: any = await fetchHelper(event, 'models/c_bpartner', 'POST', token, {
        AD_Org_ID: { id: orgId, tableName: 'AD_Org' },
        isActive: true,
        name: (inv.issuer?.name || 'Amazon').slice(0, 120),
        isVendor: true, isCustomer: false, isProspect: false,
        taxID: vat,
        tableName: 'C_Bpartner'
      })
      if (res?.id) {
        feeVendorCache.set(vat, { id: res.id, at: Date.now() })
        warnings.push(`Lieferant „${inv.issuer?.name}" (${vat}) neu angelegt: #${res.id}`)
        return res.id
      }
    } catch (e: any) { lastErr = e }
  }
  throw new Error(`Lieferant „${inv.issuer?.name}" (${vat}) konnte nicht angelegt werden: ${lastErr?.data?.detail || lastErr?.message || 'unbekannter Fehler'}`)
}

/* ---- vendor self-heal ---- */
const ensureVendor = async (event: any, token: string, organizationId: number, vendorId: number, inv: AdsInvoice, warnings: string[]) => {
  const p: any = await fetchHelper(event, `models/c_bpartner/${vendorId}`, 'GET', token, null)
  if (!p?.id) throw new Error(`Amazon-Lieferant C_BPartner_ID ${vendorId} nicht gefunden`)
  const patch: any = {}
  if (!isYes(p.IsVendor)) patch.isVendor = true
  if (!p.TaxID && inv.issuer?.vatId) patch.taxID = inv.issuer.vatId
  if (Object.keys(patch).length) {
    try { await fetchHelper(event, `models/c_bpartner/${vendorId}`, 'PUT', token, patch) }
    catch (e: any) { warnings.push(`Lieferant konnte nicht ergänzt werden (${Object.keys(patch).join(', ')}): ${e?.data?.detail || e?.message || e}`) }
  }
  // bill-to location
  let locationId: number | null = null
  try {
    const res: any = await fetchHelper(event, `models/c_bpartner_location?$filter=${enc(`C_BPartner_ID eq ${vendorId} AND (IsActive eq true OR IsActive eq false)`)}`, 'GET', token, null)
    const recs: any[] = res?.records || []
    locationId = (recs.find(r => isYes(r.IsBillTo)) || recs[0])?.id || null
  } catch {}
  if (!locationId) {
    const a = inv.issuer || {}
    let countryId: number | null = null
    try {
      const cres: any = await fetchHelper(event, `models/c_country?$filter=${enc(`CountryCode eq '${(a.countryCode || 'DE').replace(/'/g, '')}'`)}&$top=1`, 'GET', token, null)
      countryId = cres?.records?.[0]?.id || null
    } catch {}
    // the partner lives at org * — try the shared org first, fall back to the login org
    for (const orgId of [0, organizationId]) {
      try {
        const loc: any = await fetchHelper(event, 'models/c_location', 'POST', token, {
          AD_Org_ID: { id: orgId, tableName: 'AD_Org' }, isActive: true,
          address1: a.address1 || 'Marcel-Breuer-Str. 12', city: a.city || 'München', postal: a.postal || '80807',
          ...(countryId ? { C_Country_ID: { id: countryId, tableName: 'C_Country' } } : {}),
          tableName: 'C_Location'
        })
        if (!loc?.id) continue
        const bpLoc: any = await fetchHelper(event, 'models/c_bpartner_location', 'POST', token, {
          AD_Org_ID: { id: orgId, tableName: 'AD_Org' }, isActive: true, name: a.city || 'Rechnungsanschrift',
          isBillTo: true, isShipTo: true, isPayFrom: true, isRemitTo: true,
          C_BPartner_ID: { id: vendorId, tableName: 'C_BPartner' },
          C_Location_ID: { id: loc.id, tableName: 'C_Location' },
          tableName: 'C_BPartner_Location'
        })
        if (bpLoc?.id) { locationId = bpLoc.id; break }
      } catch (e: any) { warnings.push(`Adresse unter Org ${orgId} nicht anlegbar: ${e?.data?.detail || e?.message || e}`) }
    }
  }
  return { partner: p, locationId }
}

/* ---- tax by rate ---- */
const isLineLevelTax = (t: any) => !(t?.IsDocumentLevel === true || t?.IsDocumentLevel === 'Y')
const sopoOf = (t: any) => t?.SOPOType?.id || t?.SOPOType || 'B'
/** Right rate, usable for purchases — a LINE-LEVEL rate first (keeps the VAT from the PDF, see header). */
const pickPurchaseTax = (taxes: any[], rate: number) => {
  const cands = taxes.filter(t => Math.abs(Number(t.Rate ?? 0) - rate) <= 0.6 && !(t.IsSummary === true || t.IsSummary === 'Y'))
  const purchase = cands.filter(t => ['B', 'P'].includes(sopoOf(t)))
  return purchase.find(t => isLineLevelTax(t) && sopoOf(t) === 'P') || purchase.find(isLineLevelTax) || purchase[0] || cands[0] || null
}

const resolveTax = async (event: any, token: string, inv: AdsInvoice, taxId?: number | null) => {
  const res: any = await fetchHelper(event, `models/c_tax?$filter=${enc('IsActive eq true')}&$top=100`, 'GET', token, null)
  const taxes: any[] = res?.records || []
  const rate = inv.taxRate != null ? Number(inv.taxRate) : (inv.net > 0 ? (inv.tax / inv.net) * 100 : 0)
  if (taxId) {
    const chosen = taxes.find(t => t.id === Number(taxId))
    if (chosen && Math.abs(Number(chosen.Rate ?? 0) - rate) <= 0.6) return { id: chosen.id, name: chosen.Name, lineLevel: isLineLevelTax(chosen) }
  }
  // prefer purchase-side / both taxes, then any with the right rate
  const pick = pickPurchaseTax(taxes, rate)
  if (!pick) throw new Error(`Kein aktiver Steuersatz mit ${rate.toFixed(1)} % gefunden`)
  return { id: pick.id, name: pick.Name, lineLevel: isLineLevelTax(pick) }
}

/** Seller-fee invoices: one tax per distinct line rate (rows can differ). */
const resolveTaxesByRate = async (event: any, token: string, rates: number[]) => {
  const res: any = await fetchHelper(event, `models/c_tax?$filter=${enc('IsActive eq true')}&$top=100`, 'GET', token, null)
  const taxes: any[] = res?.records || []
  const out = new Map<number, number>()
  let allLineLevel = true
  for (const rate of rates) {
    const pick = pickPurchaseTax(taxes, rate)
    if (!pick) throw new Error(`Kein aktiver Steuersatz mit ${rate.toFixed(1)} % gefunden`)
    out.set(rate, pick.id)
    if (!isLineLevelTax(pick)) allLineLevel = false
  }
  return Object.assign(out, { allLineLevel })
}

const resolveFks = async (event: any, token: string, organizationId: number, partner: any, currency: string, docBaseType: 'API' | 'APC' = 'API') => {
  const out: any = { priceListId: fkId(partner?.PO_PriceList_ID) || null, paymentTermId: fkId(partner?.PO_PaymentTerm_ID) || fkId(partner?.C_PaymentTerm_ID) || null, doctypeId: null, currencyId: null }
  if (!out.priceListId) {
    try {
      const res: any = await fetchHelper(event, `models/m_pricelist?$filter=${enc(`IsSOPriceList eq false AND IsActive eq true`)}&$top=20`, 'GET', token, null)
      const recs: any[] = res?.records || []
      out.priceListId = (recs.find(r => fkId(r.AD_Org_ID) === Number(organizationId)) || recs.find(r => fkId(r.AD_Org_ID) === 0) || recs[0])?.id || null
    } catch {}
  }
  try {
    const res: any = await fetchHelper(event, `models/c_doctype?$filter=${enc(`IsSOTrx eq false AND DocBaseType eq '${docBaseType}' AND IsActive eq true`)}`, 'GET', token, null)
    out.doctypeId = res?.records?.[0]?.id || null
  } catch {}
  try {
    const res: any = await fetchHelper(event, `models/c_currency?$filter=${enc(`ISO_Code eq '${(currency || 'EUR').replace(/'/g, '')}'`)}&$top=1`, 'GET', token, null)
    out.currencyId = res?.records?.[0]?.id || (currency === 'EUR' ? 102 : null)
  } catch { out.currencyId = currency === 'EUR' ? 102 : null }
  return out
}

const fmtDe = (d?: string | null) => d ? d.split('-').reverse().join('.') : ''
const money = (n: number, cur: string) => `${round2(n).toFixed(2).replace('.', ',')} ${cur}`

export const buildAdsInvoiceLines = (inv: AdsInvoice, organizationId: number, chargeId: number, taxId: number, taxByRate?: Map<number, number>, exactTax = false) => {
  const cur = inv.currency || 'EUR'
  const taxOf = (l: any) => (l?.taxRate != null && taxByRate?.get(Number(l.taxRate))) || taxId
  const src = inv.lines?.length ? inv.lines : [{ campaignName: `${inv.kind === 'fee' ? (inv.isCreditNote ? 'Erstattung Amazon Gebühren' : 'Amazon Gebühren') : 'Amazon Ads Werbekosten'} ${fmtDe(inv.periodFrom)}${inv.periodTo && inv.periodTo !== inv.periodFrom ? ' – ' + fmtDe(inv.periodTo) : ''}`, amount: inv.net } as any]
  const lines: any[] = src.map((l: any, idx: number) => {
    const parts = [l.campaignName]
    if (l.program) parts.push(String(l.program).replace(/_/g, ' '))
    if (l.costEventCount != null) parts.push(`${l.costEventCount} ${(l.costEventType || 'CLICKS') === 'IMPRESSIONS' ? 'Impressionen' : 'Klicks'}${l.costPerUnit != null ? ' × ' + money(l.costPerUnit, cur) : ''}`)
    return {
      AD_Org_ID: { id: organizationId, tableName: 'AD_Org' },
      line: (idx + 1) * 10,
      isActive: true,
      description: parts.filter(Boolean).join(' · ').slice(0, 255),
      qtyEntered: 1, qtyInvoiced: 1,
      priceEntered: round2(l.amount), priceActual: round2(l.amount), lineNetAmt: round2(l.amount),
      C_Charge_ID: { id: chargeId, tableName: 'C_Charge' },
      C_Tax_ID: { id: taxOf(l), tableName: 'C_Tax' }
    }
  })
  const sum = round2(lines.reduce((s, l) => s + l.lineNetAmt, 0))
  const diff = round2(inv.net - sum)
  if (Math.abs(diff) >= 0.005) {
    lines.push({
      AD_Org_ID: { id: organizationId, tableName: 'AD_Org' },
      line: (lines.length + 1) * 10, isActive: true,
      description: inv.kind === 'fee' ? 'Rundungsdifferenz lt. Rechnung' : (diff < 0 ? 'Werbeaktion / Gutschrift lt. Rechnung' : 'Anpassung / Gebühren lt. Rechnung'),
      qtyEntered: 1, qtyInvoiced: 1, priceEntered: diff, priceActual: diff, lineNetAmt: diff,
      C_Charge_ID: { id: chargeId, tableName: 'C_Charge' },
      C_Tax_ID: { id: taxId, tableName: 'C_Tax' }
    })
  }
  // exact VAT from the PDF (only with a line-level purchase tax — otherwise iDempiere overwrites it):
  // per-line VAT where the PDF has it, else proportional; the remainder goes on the largest line so the
  // sum equals the document's VAT to the cent. A zero line VAT is left to iDempiere (0 = "calculate").
  if (exactTax && lines.length) {
    const total = round2(inv.tax)
    const pdfTax = src.map((l: any) => (l.tax != null ? round2(l.tax) : null))
    const usePdf = pdfTax.length === src.length && pdfTax.every((v: any) => v != null)
    const taxes: number[] = lines.map((l, i) => (usePdf && i < src.length) ? (pdfTax[i] as number) : round2(inv.net ? total * (l.lineNetAmt / inv.net) : 0))
    const rest = round2(total - taxes.reduce((a, b) => a + b, 0))
    if (Math.abs(rest) >= 0.005) {
      let big = 0
      lines.forEach((l, i) => { if (Math.abs(l.lineNetAmt) > Math.abs(lines[big].lineNetAmt)) big = i })
      taxes[big] = round2(taxes[big] + rest)
    }
    lines.forEach((l, i) => { if (Math.abs(taxes[i]) >= 0.005) l.taxAmt = taxes[i] })
  }
  return lines
}

/** A credit note arrives with negative totals/lines; an APC document is entered with POSITIVE amounts. */
const absInvoice = (inv: AdsInvoice): AdsInvoice => ({
  ...inv,
  net: Math.abs(inv.net), tax: Math.abs(inv.tax), gross: Math.abs(inv.gross),
  lines: (inv.lines || []).map(l => ({ ...l, amount: Math.abs(l.amount), tax: l.tax != null ? Math.abs(l.tax) : l.tax }))
})

export const importAdsInvoice = async (event: any, token: string, source: AdsInvoice, opts: ImportOptions): Promise<ImportResult> => {
  const warnings: string[] = []
  const isCredit = source.kind === 'fee' && (!!source.isCreditNote || source.gross < 0 || source.net < 0)
  const inv = isCredit ? absInvoice(source) : source
  const docNo = String(inv.invoiceNo || '').replace(/'/g, '').trim()
  if (!docNo) return { status: 422, message: 'Rechnungsnummer fehlt' }
  if (!(inv.gross > 0) && !(inv.net > 0)) return { status: 422, message: `Rechnung ${docNo}: kein Betrag` }
  if (!opts.chargeId) return { status: 422, message: 'Bitte eine Charge (Kostenart) für die Buchung wählen' }
  const isFee = inv.kind === 'fee'
  if (!isCredit && (inv.gross < 0 || inv.net < 0)) return { status: 422, message: `${docNo}: negativer Betrag auf einer Rechnung, die keine Gutschrift ist — bitte manuell prüfen.` }

  // 1. vendor
  const { partner, locationId } = await ensureVendor(event, token, opts.organizationId, opts.vendorId, inv, warnings)
  if (!locationId) return { status: 422, message: `Rechnung ${docNo}: der Amazon-Lieferant hat keine Rechnungsanschrift und sie konnte nicht angelegt werden`, warnings }

  // 2. duplicate guard (same rule as the inbox confirm)
  try {
    const dup: any = await fetchHelper(event, `models/c_invoice?$filter=${enc(`C_BPartner_ID eq ${opts.vendorId} AND DocumentNo eq '${docNo}' AND IsSOTrx eq false`)}`, 'GET', token, null)
    const recs: any[] = dup?.records || []
    const blocking = recs.find(r => !['VO', 'RE'].includes(r?.DocStatus?.id || r?.DocStatus))
    if (blocking) return { status: 409, message: `Rechnung ${docNo} ist bereits als Eingangsrechnung erfasst (Beleg #${blocking.id}, ${blocking?.DocStatus?.identifier || blocking?.DocStatus?.id || ''})`, cInvoiceId: blocking.id, documentNo: docNo }
    const voided = recs.filter(r => ['VO', 'RE'].includes(r?.DocStatus?.id || r?.DocStatus))
    if (voided.length) {
      const freed = await freeVoidedInvoiceNumbers({ bpartnerId: opts.vendorId, documentNo: docNo })
      if (!freed.available) return { status: 409, message: `Rechnung ${docNo}: die stornierte Rechnung #${voided[0].id} belegt die Nummer noch und die Freigabe ist auf diesem Server nicht möglich (keine Datenbankverbindung)`, cInvoiceId: voided[0].id, documentNo: docNo }
      if (freed.freed < voided.length) return { status: 409, message: `Rechnung ${docNo}: Nummer noch durch stornierte Rechnung #${voided[0].id} belegt`, cInvoiceId: voided[0].id, documentNo: docNo }
    }
  } catch (e: any) { warnings.push(`Duplikatprüfung fehlgeschlagen: ${e?.message || e}`) }

  // 3. tax / FKs / lines
  // fee invoices: tax per line rate (a mixed-rate document has no single matching rate → resolveTax would throw)
  const lineRates = isFee ? [...new Set((inv.lines || []).map(l => l.taxRate).filter((r): r is number => r != null).map(Number))] : []
  const taxByRate = lineRates.length ? await resolveTaxesByRate(event, token, lineRates) : undefined
  const tax: any = taxByRate ? { id: taxByRate.get(lineRates[0])!, name: '', lineLevel: (taxByRate as any).allLineLevel === true } : await resolveTax(event, token, inv, opts.taxId)
  const exactTax = tax.lineLevel === true
  const fks = await resolveFks(event, token, opts.organizationId, partner, inv.currency, isCredit ? 'APC' : 'API')
  if (!fks.doctypeId) return { status: 422, message: isCredit ? 'Kein Belegtyp "AP CreditMemo" (DocBaseType APC) gefunden' : 'Kein Belegtyp "AP Invoice" gefunden', warnings }
  const lines = buildAdsInvoiceLines(inv, opts.organizationId, opts.chargeId, tax.id, taxByRate, exactTax)
  const pmLabel = inv.paymentMethodLabel || (inv.paymentMethod ? (PAYMENT_METHOD_LABELS[inv.paymentMethod] || inv.paymentMethod) : '')
  const period = inv.periodFrom ? `${fmtDe(inv.periodFrom)}${inv.periodTo && inv.periodTo !== inv.periodFrom ? ' – ' + fmtDe(inv.periodTo) : ''}` : ''

  const header: any = {
    AD_Org_ID: { id: opts.organizationId, tableName: 'AD_Org' },
    isActive: true,
    isSOTrx: false,
    documentNo: docNo,
    dateInvoiced: inv.invoiceDate, dateOrdered: inv.invoiceDate, dateAcct: inv.invoiceDate,
    description: [`${isCredit ? 'Amazon Steuergutschrift' : (isFee ? 'Amazon Gebührenrechnung' : 'Amazon Ads Rechnung')} ${docNo}`, isCredit && inv.originalInvoiceNo ? `zu Rechnung ${inv.originalInvoiceNo}` : '', period ? `Zeitraum ${period}` : '', pmLabel ? `Zahlung: ${pmLabel}` : ''].filter(Boolean).join(' · ').slice(0, 255),
    isTaxIncluded: false,
    grandTotal: round2(inv.gross), totalLines: round2(inv.net),
    POReference: docNo,
    DocStatus: { id: 'DR' },
    C_DocType_ID: { id: fks.doctypeId, tableName: 'C_DocType' },
    C_DocTypeTarget_ID: { id: fks.doctypeId, tableName: 'C_DocTypeTarget' },
    C_BPartner_ID: { id: opts.vendorId, tableName: 'C_BPartner' },
    C_BPartner_Location_ID: { id: locationId, tableName: 'C_BPartner_Location' },
    ...(fks.priceListId ? { M_PriceList_ID: { id: fks.priceListId, tableName: 'M_PriceList' } } : {}),
    ...(fks.currencyId ? { C_Currency_ID: { id: fks.currencyId, tableName: 'C_Currency' } } : {}),
    ...(fks.paymentTermId ? { C_PaymentTerm_ID: { id: fks.paymentTermId, tableName: 'C_PaymentTerm' } } : {}),
    C_InvoiceLine: lines,
    tableName: 'C_Invoice'
  }
  let created: any
  try {
    created = await fetchHelper(event, 'models/c_invoice', 'POST', token, header)
  } catch (e: any) {
    return { status: Number(e?.status || e?.statusCode) || 500, message: `Rechnung ${docNo}: ${e?.data?.detail || e?.data?.title || e?.message || 'Anlage fehlgeschlagen'}`, warnings }
  }
  if (!created?.id) return { status: 500, message: `Rechnung ${docNo}: iDempiere hat keine ID zurückgegeben`, warnings }

  // 4. complete — fail-soft (draft kept); must not throw into the token-refresh retry
  let completed = false
  try {
    await fetchHelper(event, `models/c_invoice/${created.id}`, 'PUT', token, { 'doc-action': 'CO' })
    completed = true
  } catch (e: any) { warnings.push(`Fertigstellen (CO) fehlgeschlagen — Entwurf bleibt: ${e?.data?.detail || e?.data?.title || e?.message || e}`) }

  // 4b. the booked total must equal the Amazon document — report a difference instead of hiding it
  try {
    const chk: any = await fetchHelper(event, `models/c_invoice?$filter=${enc(`C_Invoice_ID eq ${created.id}`)}&$select=GrandTotal,TotalLines&$top=1`, 'GET', token, null)
    const booked = Number(chk?.records?.[0]?.GrandTotal)
    if (Number.isFinite(booked) && Math.abs(booked - round2(inv.gross)) >= 0.005) {
      warnings.push(`Bruttobetrag weicht ab: iDempiere ${money(booked, inv.currency)} / Amazon ${money(inv.gross, inv.currency)}` +
        (exactTax ? '' : ' — der verwendete Steuersatz ist ein Belegsteuersatz, iDempiere berechnet die USt. neu (Amazon rundet ab). Für centgenaue Beträge einen Einkaufs-Steuersatz mit „Belegebene = Nein“ anlegen.'))
    }
  } catch {}

  // 5. original PDF → attachment (PO_Invoice = purchase-invoice attachment table)
  let attached = false
  if (opts.pdf?.length) {
    try {
      await attachToStrapi(event, token, {
        tableName: 'PO_Invoice', recordId: created.id, recordUu: created.uid,
        buffer: opts.pdf, filename: opts.pdfName || (isCredit ? `Amazon-Gutschrift-${docNo}.pdf` : (isFee ? `Amazon-Gebuehren-${docNo}.pdf` : `Amazon-Ads-INVOICE-${docNo}.pdf`)), mimeType: 'application/pdf'
      })
      attached = true
    } catch (e: any) { warnings.push(`PDF konnte nicht angehängt werden: ${e?.message || e}`) }
  } else {
    warnings.push('Kein Amazon-PDF vorhanden — Rechnung ohne Anhang angelegt (Lexoffice-Upload wird sie überspringen).')
  }

  return { status: 200, cInvoiceId: created.id, documentNo: created.DocumentNo || docNo, completed, attached, warnings }
}

/** Void a booked Ads invoice and free its number for a later re-import. */
export const voidAdsInvoice = async (event: any, token: string, cInvoiceId: number): Promise<ImportResult> => {
  const warnings: string[] = []
  const inv: any = await fetchHelper(event, `models/c_invoice/${cInvoiceId}`, 'GET', token, null)
  if (!inv?.id) return { status: 404, message: `Rechnung #${cInvoiceId} nicht gefunden` }
  const st = inv.DocStatus?.id || inv.DocStatus
  if (!['VO', 'RE'].includes(st)) {
    try { await fetchHelper(event, `models/c_invoice/${cInvoiceId}`, 'PUT', token, { 'doc-action': 'VO' }) }
    catch (e: any) { return { status: 500, message: `Storno fehlgeschlagen: ${e?.data?.detail || e?.data?.title || e?.message || e}` } }
  }
  const freed = await freeVoidedInvoiceNumbers({ invoiceId: cInvoiceId })
  if (!freed.available) warnings.push('Rechnungsnummer konnte nicht freigegeben werden (keine direkte Datenbankverbindung auf diesem Server) — ein erneuter Import scheitert, bis sie umbenannt ist.')
  else if (!freed.freed) warnings.push('Rechnungsnummer war bereits freigegeben oder der Beleg ist noch nicht storniert.')
  return { status: 200, cInvoiceId, documentNo: inv.DocumentNo, warnings }
}
