import sql from 'mssql';
import { getPool } from '../db.js';
import { getActiveShopId, getActiveShopSubshopId } from '../shop.js';
import { deliverOrder } from './delivery/index.js';

const config = {
  kBenutzer: Number(process.env.JTL_KBENUTZER) || 1,
  kFirmaHistory: Number(process.env.JTL_KFIRMAHISTORY) || 0,
  kSprache: Number(process.env.JTL_KSPRACHE) || 1,
  kPlattform: Number(process.env.JTL_KPLATTFORM) || 7,
  kVersandArt: Number(process.env.JTL_KVERSANDART) || 0,
  kKundengruppe: Number(process.env.JTL_KKUNDENGRUPPE) || 0,
  // kLaufendeNummer 3 = Auftrag, 6 = Kunde in dbo.tLaufendeNummern
  orderNumberSequence: Number(process.env.JTL_ORDER_NUMBER_SEQUENCE) || 3,
  customerNumberSequence: Number(process.env.JTL_CUSTOMER_NUMBER_SEQUENCE) || 6,
};

/** DB-dependent foreign keys, resolved once on first order. */
let resolvedDefaults = null;

async function getDefaults() {
  if (resolvedDefaults) {
    return resolvedDefaults;
  }

  const result = await getPool().request().input('kPlattform', sql.Int, config.kPlattform).query(`
    SELECT
      (SELECT MAX(kFirmaHistory) FROM dbo.tFirmaHistory) AS kFirmaHistory,
      (SELECT MIN(kVersandArt) FROM dbo.tVersandArt) AS kVersandArt,
      (SELECT TOP 1 kKundenGruppe FROM dbo.tKundenGruppe ORDER BY nStandard DESC, kKundenGruppe) AS kKundengruppe,
      (SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.tPlattform WHERE nPlattform = @kPlattform) THEN @kPlattform ELSE 1 END) AS kPlattform;
  `);
  const row = result.recordset[0] || {};

  resolvedDefaults = {
    kFirmaHistory: config.kFirmaHistory || row.kFirmaHistory || 1,
    kVersandArt: config.kVersandArt || row.kVersandArt || 1,
    kKundengruppe: config.kKundengruppe || row.kKundengruppe || 1,
    kPlattform: row.kPlattform || 1,
  };
  return resolvedDefaults;
}

const COUNTRY_NAMES = {
  DE: 'Deutschland',
  AT: 'Österreich',
  CH: 'Schweiz',
};

function toNumber(value, fallback = 0) {
  const n = parseFloat(String(value ?? '').replace(',', '.'));
  return Number.isFinite(n) ? n : fallback;
}

// order.creationDate comes in as "YYYY-MM-DD HH:mm:ss" (no timezone); treat it as
// local server time, same as `new Date('YYYY-MM-DDTHH:mm:ss')` would. Falls back to
// the server's current time if missing/unparseable.
function parseOrderDate(creationDate) {
  if (creationDate) {
    const parsed = new Date(String(creationDate).replace(' ', 'T'));
    if (!Number.isNaN(parsed.getTime())) {
      return parsed;
    }
  }
  return new Date();
}

// JTL maps VAT rates to Steuerklassen: 19% => 1 (voller Satz), 7% => 2 (ermäßigt)
function steuerklasseForVat(vat) {
  if (vat >= 15) return 1;
  if (vat > 0) return 2;
  return 1;
}

function isoWeek(date) {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
}

/**
 * Resolves the Admin > Nummernkreise placeholders used in tLaufendeNummern's
 * cPrefix/cSuffix columns: J = Jahr, M = Monat, T = Tag, K = Kalenderwoche.
 */
function formatNumberPlaceholders(template, date) {
  if (!template) return '';
  const pad2 = (n) => String(n).padStart(2, '0');
  return template
    .replace(/<J>/g, String(date.getFullYear()))
    .replace(/<M>/g, pad2(date.getMonth() + 1))
    .replace(/<T>/g, pad2(date.getDate()))
    .replace(/<K>/g, pad2(isoWeek(date)));
}

/**
 * Atomically increments nNummer for a dbo.tLaufendeNummern row and returns
 * the fully formatted number (cPrefix + nNummer + cSuffix, with placeholders
 * resolved against `date`).
 */
async function nextNumberFromSequence(transaction, kLaufendeNummer, date) {
  const result = await new sql.Request(transaction).input('kLaufendeNummer', sql.Int, kLaufendeNummer).query(`
    DECLARE @n INT, @cPrefix NVARCHAR(50), @cSuffix NVARCHAR(50);
    UPDATE dbo.tLaufendeNummern
      SET @n = nNummer = nNummer + 1, @cPrefix = cPrefix, @cSuffix = cSuffix
      WHERE kLaufendeNummer = @kLaufendeNummer;
    SELECT @n AS nNummer, @cPrefix AS cPrefix, @cSuffix AS cSuffix;
  `);
  const row = result.recordset[0];
  if (!row || row.nNummer == null) {
    throw new Error(`dbo.tLaufendeNummern has no row ${kLaufendeNummer}`);
  }
  const prefix = formatNumberPlaceholders(row.cPrefix, date);
  const suffix = formatNumberPlaceholders(row.cSuffix, date);
  return `${prefix}${row.nNummer}${suffix}`;
}

/**
 * Allocates the next primary key for a table via dbo.tpk
 * (JTL tables like tZahlung/tZahlungsart have no identity column).
 */
async function allocatePk(transaction, tableName) {
  const result = await new sql.Request(transaction).input('cName', sql.NVarChar, tableName).query(`
    DECLARE @pk INT;
    UPDATE dbo.tpk SET @pk = nummer, nummer = nummer + 1, dChanged = GETDATE() WHERE cName = @cName;
    SELECT @pk AS pk;
  `);
  const pk = result.recordset[0]?.pk;
  if (pk == null) {
    throw new Error(`dbo.tpk has no row for table '${tableName}'`);
  }
  return pk;
}

/**
 * Resolves a Zahlungsart by name; creates it on the fly (including its PK
 * from dbo.tpk) when it does not exist yet.
 */
async function resolveZahlungsart(transaction, name, cache) {
  const key = String(name || 'Bar');
  const cached = cache.get(key.toLowerCase());
  if (cached) {
    return cached;
  }

  const existing = await new sql.Request(transaction)
    .input('cName', sql.NVarChar, key)
    .query('SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = @cName');

  let zahlungsart;
  if (existing.recordset[0]) {
    zahlungsart = {
      kZahlungsart: existing.recordset[0].kZahlungsart,
      cName: existing.recordset[0].cName,
    };
  } else {
    const kZahlungsart = await allocatePk(transaction, 'tZahlungsart');
    await new sql.Request(transaction)
      .input('kZahlungsart', sql.Int, kZahlungsart)
      .input('cName', sql.NVarChar, key)
      .query(`
        INSERT INTO dbo.tZahlungsart
          (kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto,
           nAusliefernVorZahlung, nPrioritaet, nMahnwesenAktiv, fSkontoWert, nSkontoZeitraum,
           nMatchingOptionen, nIstStandard, nAktiv)
        VALUES (@kZahlungsart, @cName, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)
      `);
    zahlungsart = { kZahlungsart, cName: key };
  }

  cache.set(key.toLowerCase(), zahlungsart);
  return zahlungsart;
}

async function nextCustomerNumber(transaction) {
  return nextNumberFromSequence(transaction, config.customerNumberSequence, new Date());
}

/**
 * Creates a Kunde via Kunde.spKundeInsert (which fills tKunde, tAdresse and
 * the search tables). Columns of the table type are named explicitly because
 * their order differs between JTL versions.
 */
async function createCustomer(transaction, { customerNumber, address, defaults }) {
  const a = address || {};
  const iso = (a.countryIso || 'DE').toUpperCase();
  const kKundengruppe = Number(a.customerGroupId) || defaults.kKundengruppe;

  const result = await new sql.Request(transaction)
    .input('cKundenNr', sql.NVarChar, customerNumber)
    .input('cFirma', sql.NVarChar, a.company || '')
    .input('cAnrede', sql.NVarChar, a.salutation || '')
    .input('cTitel', sql.NVarChar, a.title || '')
    .input('cVorname', sql.NVarChar, a.firstName || '')
    .input('cName', sql.NVarChar, a.lastName || 'Laufkunde')
    .input('cStrasse', sql.NVarChar, a.street || '-')
    .input('cPLZ', sql.NVarChar, a.zipCode || '')
    .input('cOrt', sql.NVarChar, a.city || '-')
    .input('cLand', sql.NVarChar, COUNTRY_NAMES[iso] || iso)
    .input('cTel', sql.NVarChar, a.phone || '')
    .input('cFax', sql.NVarChar, a.fax || '')
    .input('cEMail', sql.NVarChar, a.email || '')
    .input('cMobil', sql.NVarChar, a.mobile || '')
    .input('fRabatt', sql.Decimal(18, 13), toNumber(a.discount, 0))
    .input('cAdressZusatz', sql.NVarChar, a.addressAddition || '')
    .input('cGeburtstag', sql.NVarChar, a.birthday || '')
    .input('kKundenGruppe', sql.Int, kKundengruppe)
    .input('kSprache', sql.Int, config.kSprache)
    .input('cISO', sql.NVarChar, iso)
    .input('cBundesland', sql.NVarChar, a.state || '')
    .input('cHerkunft', sql.NVarChar, 'Kasse')
    .input('cKassenKunde', sql.Char(1), 'Y')
    .input('nDebitorennr', sql.Int, Number(a.debtorNumber) || 0)
    .query(`
      DECLARE @returnValue INT;
      DECLARE @p1 dbo.TYPE_spkundeInsert;
      INSERT INTO @p1
        (kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName,
         cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter,
         cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe,
         nZahlungsziel, kSprache, cISO, cBundesland, cHerkunft, cKassenKunde, cHRNr, kZahlungsart,
         nDebitorennr, cSteuerNr, nKreditlimit, kKundenDrucktext, nMahnstopp, nMahnrhythmus, kFirma,
         fProvision, nVertreter, fSkonto, nSkontoInTagen)
      VALUES
        (0, 0, @cKundenNr, @cFirma, @cAnrede, @cTitel, @cVorname, @cName,
         @cStrasse, @cPLZ, @cOrt, @cLand, @cTel, @cFax, @cEMail, GETDATE(), @cMobil, @fRabatt, NULL, 'N',
         N'', N'', 0, @cAdressZusatz, @cGeburtstag, N'', 'N', NULL, @kKundenGruppe,
         0, @kSprache, @cISO, @cBundesland, @cHerkunft, @cKassenKunde, N'', 0,
         @nDebitorennr, N'', 0, 0, 0, 0, 0,
         NULL, 0, 0, 0);
      EXEC @returnValue = Kunde.spKundeInsert @daten = @p1;
      SELECT @returnValue AS kKunde;
    `);

  const kKunde = result.recordset[0]?.kKunde;
  if (!kKunde || kKunde <= 0) {
    throw new Error(`Kunde.spKundeInsert failed for '${customerNumber}'`);
  }
  return { kKunde, kKundengruppe };
}

function isWalkInOrder(order) {
  const customerNumber = String(order.customerNumber || '').trim();
  if (!customerNumber || customerNumber === '0') return true;

  const billing = order.billingAddress || {};
  return (billing.lastName || '') === 'Laufkunde' && !(billing.firstName || '').trim();
}

async function lookupKassenkunde(transaction, defaults) {
  const result = await new sql.Request(transaction).query(
    "SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde"
  );
  if (result.recordset[0]) {
    return {
      kKunde: result.recordset[0].kKunde,
      kKundengruppe: result.recordset[0].kKundenGruppe || defaults.kKundengruppe,
    };
  }
  return null;
}

/**
 * Resolves the customer by cKundenNr; creates the Kunde on the fly when it
 * does not exist. Walk-in orders use (or create) the Kassenkunde.
 */
async function resolveCustomer(transaction, order, defaults) {
  if (isWalkInOrder(order)) {
    const kassenKunde = await lookupKassenkunde(transaction, defaults);
    if (kassenKunde) return kassenKunde;
    return createCustomer(transaction, {
      customerNumber: await nextCustomerNumber(transaction),
      address: order.billingAddress,
      defaults,
    });
  }

  const customerNumber = String(order.customerNumber || '').trim();
  const result = await new sql.Request(transaction)
    .input('cKundenNr', sql.NVarChar, customerNumber)
    .query('SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKundenNr = @cKundenNr');
  if (result.recordset[0]) {
    return {
      kKunde: result.recordset[0].kKunde,
      kKundengruppe: result.recordset[0].kKundenGruppe || defaults.kKundengruppe,
    };
  }
  return createCustomer(transaction, {
    customerNumber,
    address: order.billingAddress,
    defaults,
  });
}

async function nextOrderNumber(transaction, orderDate) {
  return nextNumberFromSequence(transaction, config.orderNumberSequence, orderDate);
}

async function insertOrderAddress(transaction, kAuftrag, kKunde, address, nTyp) {
  const a = address || {};
  const iso = (a.countryIso || 'DE').toUpperCase();
  await new sql.Request(transaction)
    .input('kAuftrag', sql.Int, kAuftrag)
    .input('kKunde', sql.Int, kKunde)
    .input('cFirma', sql.NVarChar, a.company || '')
    .input('cAnrede', sql.NVarChar, a.salutation || '')
    .input('cTitel', sql.NVarChar, a.title || '')
    .input('cVorname', sql.NVarChar, a.firstName || '')
    .input('cName', sql.NVarChar, a.lastName || '-')
    .input('cStrasse', sql.NVarChar, a.street || '-')
    .input('cPLZ', sql.NVarChar, a.zipCode || '')
    .input('cOrt', sql.NVarChar, a.city || '-')
    .input('cLand', sql.NVarChar, COUNTRY_NAMES[iso] || iso)
    .input('cTel', sql.NVarChar, a.phone || '')
    .input('cZusatz', sql.NVarChar, a.extraAddressLine || '')
    .input('cAdressZusatz', sql.NVarChar, a.addressAddition || '')
    .input('cMobil', sql.NVarChar, a.mobile || '')
    .input('cMail', sql.NVarChar, a.email || '')
    .input('cFax', sql.NVarChar, a.fax || '')
    .input('cBundesland', sql.NVarChar, a.state || '')
    .input('cISO', sql.NVarChar, iso)
    .input('nTyp', sql.Int, nTyp)
    .query(`
      INSERT INTO Verkauf.tAuftragAdresse
        (kAuftrag, kKunde, cFirma, cAnrede, cTitel, cVorname, cName, cStrasse, cPLZ, cOrt, cLand,
         cTel, cZusatz, cAdressZusatz, cMobil, cMail, cFax, cBundesland, cISO, nTyp, nZolldokumenteErforderlich)
      VALUES (@kAuftrag, @kKunde, @cFirma, @cAnrede, @cTitel, @cVorname, @cName, @cStrasse, @cPLZ, @cOrt, @cLand,
         @cTel, @cZusatz, @cAdressZusatz, @cMobil, @cMail, @cFax, @cBundesland, @cISO, @nTyp, 0)
    `);
}

async function insertOrderItem(transaction, kAuftrag, item) {
  const vat = toNumber(item.vat, 19);
  const quantity = toNumber(item.quantity, 1);
  const priceNet = toNumber(item.priceNet, toNumber(item.priceGross) / (1 + vat / 100));
  const discount = toNumber(item.discountPercent, 0);
  const kSteuerklasse = steuerklasseForVat(vat);
  const sku = String(item.sku || '').trim();

  let kArtikel = null;
  if (sku) {
    const result = await new sql.Request(transaction)
      .input('cArtNr', sql.NVarChar, sku)
      .query('SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = @cArtNr');
    kArtikel = result.recordset[0]?.kArtikel ?? null;
  }

  const result = await new sql.Request(transaction)
    .input('kArtikel', sql.Int, kArtikel)
    .input('kAuftrag', sql.Int, kAuftrag)
    .input('cArtNr', sql.NVarChar, kArtikel ? sku : null)
    .input('cName', sql.NVarChar, item.name || sku || 'Position')
    .input('cHinweis', sql.NVarChar, item.note || '')
    .input('fAnzahl', sql.Float, quantity)
    .input('fVkNetto', sql.Float, priceNet)
    .input('fMwSt', sql.Float, vat)
    .input('kSteuerklasse', sql.Int, kSteuerklasse)
    .input('nType', sql.Int, kArtikel ? 1 : 0)
    .input('cEinheit', sql.NVarChar, item.unit || '')
    .input('fRabatt', sql.Float, discount)
    .query(`
      DECLARE @t TABLE ([kAuftragPosition] INT);
      INSERT INTO Verkauf.tAuftragPosition
        (kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt,
         cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt)
      OUTPUT inserted.kAuftragPosition INTO @t
      VALUES (@kArtikel, @kAuftrag, @cArtNr, 1, @cName, @cHinweis, @fAnzahl, @fVkNetto, @fMwSt,
         @cName, @kSteuerklasse, @nType, @cEinheit, 1.0, 3, @fRabatt);
      SELECT kAuftragPosition FROM @t;
    `);
  return result.recordset[0]?.kAuftragPosition ?? null;
}

/**
 * JTL PosOrderCreationService.CheckIfOrderExists — skip re-import when the POS
 * order id is already mapped to a Wawi order for this subshop.
 */
async function findExistingPosOrderMapping(kPosAuftrag) {
  const kShopSubShop = getActiveShopSubshopId();
  if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) {
    return null;
  }

  const result = await getPool()
    .request()
    .input('kPosAuftrag', sql.Int, kPosAuftrag)
    .input('kShopSubShop', sql.Int, kShopSubShop)
    .query(`
      SELECT TOP 1 m.kAuftrag, a.cAuftragsNr
      FROM Pos.tAuftragMapping m
      LEFT JOIN Verkauf.tAuftrag a ON a.kAuftrag = m.kAuftrag
      WHERE m.kPosAuftrag = @kPosAuftrag
        AND m.kShopSubShop = @kShopSubShop
        AND m.kAuftrag IS NOT NULL
      ORDER BY m.kAuftrag DESC
    `);

  const row = result.recordset[0];
  if (!row?.kAuftrag) {
    return null;
  }
  return row;
}

/**
 * Links a JTL-Wawi order back to the originating POS order id.
 * MERGE matches JTL CreateOrUpdate outcome for the final committed row.
 */
async function upsertPosOrderMapping(transaction, kAuftrag, kPosAuftrag) {
  const kShopSubShop = getActiveShopSubshopId();
  if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) {
    return;
  }
  await new sql.Request(transaction)
    .input('kAuftrag', sql.Int, kAuftrag)
    .input('kPosAuftrag', sql.Int, kPosAuftrag)
    .input('kShopSubShop', sql.Int, kShopSubShop)
    .query(`
      MERGE INTO Pos.tAuftragMapping WITH (HOLDLOCK) AS Target
      USING (SELECT @kAuftrag AS kAuftrag, @kPosAuftrag AS kPosAuftrag, @kShopSubShop AS kShopSubShop) AS Source
      ON Target.kPosAuftrag = Source.kPosAuftrag
         AND Target.kShopSubShop = Source.kShopSubShop
      WHEN MATCHED THEN
        UPDATE SET Target.kAuftrag = Source.kAuftrag
      WHEN NOT MATCHED BY TARGET THEN
        INSERT (kAuftrag, kPosAuftrag, kShopSubShop)
        VALUES (Source.kAuftrag, Source.kPosAuftrag, Source.kShopSubShop);
    `);
}

async function insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition) {
  const kShopSubShop = getActiveShopSubshopId();
  if (!Number.isInteger(kAuftragPosition) || !Number.isInteger(kPosAuftragPosition) || !kShopSubShop) {
    return;
  }
  await new sql.Request(transaction)
    .input('kAuftragPosition', sql.Int, kAuftragPosition)
    .input('kPosAuftragPosition', sql.Int, kPosAuftragPosition)
    .input('kShopSubShop', sql.Int, kShopSubShop)
    .query(`
      INSERT INTO Pos.tAuftragPositionMapping (kAuftragPosition, kPosAuftragPosition, kShopSubShop)
      VALUES (@kAuftragPosition, @kPosAuftragPosition, @kShopSubShop)
    `);
}

/**
 * Mirrors JTL's own PosOrderCreationService -> DeliveryStep.Run(): after a POS
 * order is created, `Settings.Deliver` (default true, see decompiled
 * jtlCore.Classes.Sync.PosOrderSteps.DeliveryStep:199,
 * `bool flag = orderEntity.Settings?.Deliver ?? true;`) decides whether the
 * order is delivered immediately. Digital-voucher partial delivery
 * (IstGutscheinDigital) is intentionally not reproduced.
 *
 * The actual delivery mechanics (Pickliste creation, real stock decrement,
 * numbered Lieferschein) live in ./delivery/ - see delivery.md for the full
 * investigation behind it.
 */
function isOrderDelivered(order) {
  const deliver = order.settings?.deliver;
  if (deliver === undefined || deliver === null) {
    return true;
  }
  return deliver === true || String(deliver) === '1' || String(deliver).toLowerCase() === 'true';
}

async function insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache) {
  const zahlungsart = await resolveZahlungsart(transaction, payment.paymentMethodName || order.paymentMethodName, zahlungsartCache);
  const kZahlung = await allocatePk(transaction, 'tZahlung');

  await new sql.Request(transaction)
    .input('kZahlung', sql.Int, kZahlung)
    .input('cName', sql.NVarChar, zahlungsart.cName)
    .input('dDatum', sql.DateTime, orderDate)
    .input('fBetrag', sql.Float, toNumber(payment.amount, 0))
    .input('kBestellung', sql.Int, kAuftrag)
    .input('kBenutzer', sql.Int, config.kBenutzer)
    .input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart)
    .input('cExternalTransactionId', sql.NVarChar, order.externalOrderNumber || '')
    .query(`
      INSERT INTO dbo.tZahlung
        (kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart,
         nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)
      VALUES (@kZahlung, @cName, @dDatum, @fBetrag, @kBestellung, @kBenutzer, 0, '', @kZahlungsart,
         0, @cExternalTransactionId, 0, 0, '', 0)
    `);
}

export async function createOrder(order) {
  const kPosAuftrag = Number.parseInt(order.externalId, 10);
  if (Number.isInteger(kPosAuftrag) && kPosAuftrag > 0) {
    const existing = await findExistingPosOrderMapping(kPosAuftrag);
    if (existing) {
      return {
        orderId: String(existing.kAuftrag),
        orderNumber: existing.cAuftragsNr || '',
      };
    }
  }

  const defaults = await getDefaults();
  const transaction = new sql.Transaction(getPool());
  await transaction.begin(sql.ISOLATION_LEVEL.READ_COMMITTED);

  try {
    const zahlungsartCache = new Map();

    // Use the client-supplied order.creationDate for stored timestamps (dErstellt,
    // number-sequence date placeholders, payment date), falling back to the
    // server's current time if it's missing/unparseable.
    const orderDate = parseOrderDate(order.creationDate);
    const { kKunde, kKundengruppe } = await resolveCustomer(transaction, order, defaults);
    const zahlungsart = await resolveZahlungsart(transaction, order.paymentMethodName, zahlungsartCache);
    const cAuftragsNr = await nextOrderNumber(transaction, orderDate);

    const resultAuftrag = await new sql.Request(transaction)
      .input('cAuftragsNr', sql.NVarChar, cAuftragsNr)
      .input('dErstellt', sql.DateTime, orderDate)
      .input('kBenutzer', sql.Int, config.kBenutzer)
      .input('kKunde', sql.Int, kKunde)
      .input('kFirmaHistory', sql.Int, defaults.kFirmaHistory)
      .input('kSprache', sql.Int, config.kSprache)
      .input('cWaehrung', sql.NVarChar, order.currencyIso || 'EUR')
      .input('kPlattform', sql.Int, defaults.kPlattform)
      .input('kShop', sql.Int, getActiveShopId() || null)
      .input('cKundenNr', sql.NVarChar, String(order.customerNumber || ''))
      .input('cVersandlandISO', sql.NVarChar, (order.shippingAddress?.countryIso || 'DE').toUpperCase())
      .input('kVersandArt', sql.Int, defaults.kVersandArt)
      .input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart)
      .input('kKundengruppe', sql.Int, kKundengruppe)
      .input('cExterneAuftragsnummer', sql.NVarChar, order.externalOrderNumber || '')
      .query(`
        DECLARE @t TABLE ([kAuftrag] INT);
        INSERT INTO Verkauf.tAuftrag
          (cAuftragsNr, dErstellt, nKomplettAusgeliefert, kBenutzer, kKunde, kBenutzerErstellt, nType, fFaktor,
           kFirmaHistory, kSprache, cVersandlandWaehrung, fVersandlandWaehrungFaktor, fFinanzierungskosten,
           cWaehrung, kPlattform, kShop, cKundenNr, cVersandlandISO, kVersandArt, kZahlungsart, kKundengruppe,
           cExterneAuftragsnummer)
        OUTPUT inserted.kAuftrag INTO @t
        VALUES (@cAuftragsNr, @dErstellt, 0, @kBenutzer, @kKunde, @kBenutzer, 1, 1.0,
           @kFirmaHistory, @kSprache, @cWaehrung, 1.0, 0.0,
           @cWaehrung, @kPlattform, @kShop, @cKundenNr, @cVersandlandISO, @kVersandArt, @kZahlungsart, @kKundengruppe,
           @cExterneAuftragsnummer);
        SELECT kAuftrag FROM @t;
      `);

    const kAuftrag = resultAuftrag.recordset[0].kAuftrag;

    const kPosAuftrag = Number.parseInt(order.externalId, 10);
    await upsertPosOrderMapping(transaction, kAuftrag, kPosAuftrag);

    await insertOrderAddress(transaction, kAuftrag, kKunde, order.shippingAddress, 0);
    await insertOrderAddress(transaction, kAuftrag, kKunde, order.billingAddress, 1);

    const deliveredItems = [];
    for (const item of order.orderItems || []) {
      const kAuftragPosition = await insertOrderItem(transaction, kAuftrag, item);
      const kPosAuftragPosition = Number.parseInt(item.externalId, 10);
      await insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition);
      if (kAuftragPosition != null) {
        deliveredItems.push({ kAuftragPosition, quantity: toNumber(item.quantity, 1) });
      }
    }

    for (const payment of order.payments || []) {
      await insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache);
    }

    if (isOrderDelivered(order)) {
      await deliverOrder(transaction, config.kBenutzer, kAuftrag, defaults.kVersandArt, deliveredItems);
    }

    await new sql.Request(transaction).input('kAuftrag', sql.Int, kAuftrag).query(`
      DECLARE @p1 Verkauf.TYPE_spAuftragEckdatenBerechnen;
      INSERT INTO @p1 VALUES (@kAuftrag);
      EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @p1;
    `);

    await transaction.commit();

    return { orderId: String(kAuftrag), orderNumber: cAuftragsNr };
  } catch (err) {
    try {
      await transaction.rollback();
    } catch {
      // transaction may already be aborted by the server
    }
    throw err;
  }
}
