<?php
declare(strict_types=1);

/* doc-project | caisse-aqp/public/api/updateOrder.php | Met à jour une commande existante, ses lignes, ses modifs, le client, l’adresse de livraison provisoire portée par la commande, puis déclenche les effets de bord de préparation/notification, avec validation serveur des couples code postal / ville autorisés selon le PDV lorsqu’une option valide est choisie, conservation explicite des valeurs existantes quand le sélecteur composite ne fournit aucune correspondance exploitable, et revalidation bloquante de la fidélité juste avant validation finale de la commande modifiée. | Expose: respond, normalizeBool, shouldPreserveDeliveryCityPayload | Dépend de: config.php, _lib/storeSlotConfig.php, _lib/chipSlots.php, _lib/clientUpsert.php, _lib/printJobs.php, _lib/smsQueue.php, _lib/firstNamesUpsert.php, _lib/tempAddress.php, _lib/deliveryCityChoices.php, lib/loyalty.php | Impacte: BDD, file d’attente d’impression, file SMS, logs serveur, validation métier des adresses caisse, blocage serveur des conflits fidélité | Tables: pos_commandes(id, id_date, heure_prepa, id_client, livraison, coupe, nbr_pizzas, prix_com, adresse_temporaire, statut), pos_pizzas_commandees(id, id_pos_pizzas, data_id, is_web_addition, is_free), pos_modifs_pizzas(id, nom_option, class_option, data_price), pos_temp_adresses */
header('Content-Type: application/json; charset=utf-8');
// Never cache writes
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');

function respond(int $code, array $payload): void {
  http_response_code($code);
  echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  exit;
}

/**
 * Normalize mixed JSON inputs to boolean.
 * Accepts bool, 0/1, "true"/"false", "on"/"off".
 */
function normalizeBool($v): bool {
  if (is_bool($v)) return $v;
  if (is_int($v) || is_float($v)) return ((int)$v) === 1;
  $s = strtolower(trim((string)($v ?? '')));
  if ($s === '1' || $s === 'true' || $s === 'on' || $s === 'yes') return true;
  return false;
}

function shouldPreserveDeliveryCityPayload($payload): bool {
  $src = is_array($payload) ? $payload : [];
  return normalizeBool($src['deliveryCityChoicePreserveExisting'] ?? false);
}

@require_once __DIR__ . '/../../config.php';
@require_once __DIR__ . '/_lib/storeSlotConfig.php';
@require_once __DIR__ . '/_lib/chipSlots.php';
@require_once __DIR__ . '/_lib/clientUpsert.php';
@require_once __DIR__ . '/_lib/printJobs.php';
@require_once __DIR__ . '/_lib/smsQueue.php';
@require_once __DIR__ . '/_lib/firstNamesUpsert.php';
@require_once __DIR__ . '/_lib/tempAddress.php';
@require_once __DIR__ . '/_lib/deliveryCityChoices.php';
@require_once __DIR__ . '/lib/loyalty.php';

if (!isset($pdo) || !($pdo instanceof PDO)) {
  respond(500, ['ok' => false, 'error' => 'DB not configured']);
}

$raw = file_get_contents('php://input');
$json = is_string($raw) ? json_decode($raw, true) : null;
if (!is_array($json)) {
  respond(400, ['ok' => false, 'error' => 'Invalid JSON']);
}

$store = isset($json['store']) ? (string)$json['store'] : '';
if (!preg_match('/^(lan|pel)$/', $store)) {
  respond(400, ['ok' => false, 'error' => 'Invalid store']);
}

// Store-specific tables (strict whitelist)
$tableOrders = ($store === 'pel') ? 'pos_commandes_pel' : 'pos_commandes';
$tableLines  = ($store === 'pel') ? 'pos_pizzas_commandees_pel' : 'pos_pizzas_commandees';
$colOrderFk  = ($store === 'pel') ? 'id_pos_commandes_pel' : 'id_pos_commandes';
$tableModifs = ($store === 'pel') ? 'pos_modifs_pizzas_pel' : 'pos_modifs_pizzas';
$colLineFk   = ($store === 'pel') ? 'id_pos_pizzas_commandees_pel' : 'id_pos_pizzas_commandees';

// Required identifiers (loaded order context)
$idRaw = $json['id'] ?? null;
$idDateRaw = $json['id_date'] ?? null;
$dateISO = isset($json['dateISO']) ? (string)$json['dateISO'] : '';

if (!is_numeric($idRaw) || (int)$idRaw <= 0) respond(400, ['ok' => false, 'error' => 'Invalid id']);
if (!is_numeric($idDateRaw) || (int)$idDateRaw <= 0) respond(400, ['ok' => false, 'error' => 'Invalid id_date']);
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateISO)) respond(400, ['ok' => false, 'error' => 'Invalid dateISO']);

$orderId = (int)$idRaw;
$idDate = (int)$idDateRaw;

// UI flag (future SMS decision) — currently no-op (no DB write).
$modifCommande = normalizeBool($json['modif_commande'] ?? false);
if ($modifCommande) error_log("[updateOrder] modif_commande=true store={$store} order_id={$orderId} id_date={$idDate}");

$coupe = isset($json['coupe']) ? (int)$json['coupe'] : 0;
$livraison = isset($json['livraison']) ? (int)$json['livraison'] : 0;
$nbrPizzas = isset($json['nbr_pizzas']) ? (int)$json['nbr_pizzas'] : 0;
$prixComRaw = $json['prix_com'] ?? null;
$prixCom = is_numeric($prixComRaw) ? (float)$prixComRaw : 0.0;
$prixComStr = number_format($prixCom, 2, '.', '');

$temporaryDeliveryAddress = (isset($json['temporaryDeliveryAddress']) && is_array($json['temporaryDeliveryAddress']))
  ? $json['temporaryDeliveryAddress']
  : null;

$heureHHMM = isset($json['heure_prepa_hhmm']) ? (string)$json['heure_prepa_hhmm'] : '';
if (!preg_match('/^\d{2}:\d{2}$/', $heureHHMM)) {
  respond(400, ['ok' => false, 'error' => 'Choisir un créneau']);
}

// Client payload (must be persisted on UPDATE too if operator edited fields)
$phoneRaw = isset($json['phoneNumber']) ? (string)$json['phoneNumber'] : '';
$clientPayload = $json['client'] ?? null;

// Diff payload
$diff = isset($json['diff']) && is_array($json['diff']) ? $json['diff'] : [];
$addList = (isset($diff['add']) && is_array($diff['add'])) ? $diff['add'] : [];
$updList = (isset($diff['update']) && is_array($diff['update'])) ? $diff['update'] : [];
$delList = (isset($diff['delete']) && is_array($diff['delete'])) ? $diff['delete'] : [];

// NOTE: clientUpsert.php defines cleanStr(). Avoid redeclare fatal by using local names here.
function cleanStrLocal($v, int $maxLen = 0): string {
  $s = trim((string)($v ?? ''));
  if ($s === '') return '';
  if ($maxLen > 0 && mb_strlen($s, 'UTF-8') > $maxLen) $s = mb_substr($s, 0, $maxLen, 'UTF-8');
  return $s;
}

function round2Local($v): float {
  $n = is_numeric($v) ? (float)$v : 0.0;
  return round($n, 2);
}

// Robust money parsing => integer cents (avoid float drift in comparisons)
function moneyToCentsLocal($v): int {
  $s = trim((string)($v ?? ''));
  if ($s === '') return 0;
  $s = str_replace(["€", " "], "", $s);
  $s = str_replace(",", ".", $s);

  if (!preg_match('/^([+-])?(\d+)(?:\.(\d{1,2}))?$/', $s, $m)) return 0;
  $sign = (($m[1] ?? '') === '-') ? -1 : 1;
  $euros = (int)$m[2];
  $cents = isset($m[3]) ? (int)str_pad((string)$m[3], 2, '0') : 0;
  return $sign * ($euros * 100 + $cents);
}

// Compute order total (lines base price_large + modifs data_price) in cents.
// Uses DB as source-of-truth (works even if prix_com is stale or float-rounded).
function fetchOrderTotalCentsLocal(PDO $pdo, string $tableLines, string $tableModifs, string $colOrderFk, string $colLineFk, int $orderId): int {
  $total = 0;
  $lineIds = [];

  // Lines base price (pos_pizzas.price_large)
  $stmt = $pdo->prepare("
    SELECT ppc.id AS line_id, pp.price_large AS price_large
    FROM {$tableLines} ppc
    LEFT JOIN pos_pizzas pp ON ppc.id_pos_pizzas = pp.id
    WHERE ppc.{$colOrderFk} = :order_id
  ");
  $stmt->bindValue(':order_id', $orderId, PDO::PARAM_INT);
  $stmt->execute();
  while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $lid = (int)($r['line_id'] ?? 0);
    if ($lid > 0) $lineIds[] = $lid;
    $total += moneyToCentsLocal($r['price_large'] ?? 0);
  }

  // Options/modifs sum (data_price)
  if ($lineIds) {
    $ph = implode(',', array_fill(0, count($lineIds), '?'));
    $stmtM = $pdo->prepare("SELECT data_price FROM {$tableModifs} WHERE {$colLineFk} IN ({$ph})");
    foreach ($lineIds as $i => $lid) $stmtM->bindValue($i + 1, $lid, PDO::PARAM_INT);
    $stmtM->execute();
    while ($m = $stmtM->fetch(PDO::FETCH_ASSOC)) {
      $total += moneyToCentsLocal($m['data_price'] ?? 0);
    }
  }

  return (int)$total;
}

function normalizeLineForInsert($it): array {
  $a = is_array($it) ? $it : [];
  $pid = isset($a['id_pos_pizzas']) ? (int)$a['id_pos_pizzas'] : 0;
  $dataId = cleanStrLocal($a['data_id'] ?? '', 80);
  $cls = strtolower(cleanStrLocal($a['class'] ?? '', 40));
  $modifs = (isset($a['modifs']) && is_array($a['modifs'])) ? $a['modifs'] : [];
  return [
    'id_pos_pizzas' => $pid,
    'data_id' => $dataId,
    'class' => $cls,
    'modifs' => $modifs,
  ];
}

function normalizeUpdateLine($it): array {
  $a = is_array($it) ? $it : [];
  $oid = isset($a['ordered_id']) ? (int)$a['ordered_id'] : 0;
  $pid = isset($a['id_pos_pizzas']) ? (int)$a['id_pos_pizzas'] : 0;
  $dataId = cleanStrLocal($a['data_id'] ?? '', 80);
  $cls = strtolower(cleanStrLocal($a['class'] ?? '', 40));
  $modifs = (isset($a['modifs']) && is_array($a['modifs'])) ? $a['modifs'] : [];
  return [
    'ordered_id' => $oid,
    'id_pos_pizzas' => $pid,
    'data_id' => $dataId,
    'class' => $cls,
    'modifs' => $modifs,
  ];
}

function normalizeDeleteLine($it): int {
  $a = is_array($it) ? $it : [];
  return isset($a['ordered_id']) ? (int)$a['ordered_id'] : 0;
}

function validateModifsArray($modifs): array {
  $out = [];
  if (!is_array($modifs)) return $out;
  foreach ($modifs as $m) {
    if (!is_array($m)) continue;
    $nom = cleanStrLocal($m['nom_option'] ?? '', 120);
    if ($nom === '') continue;
    $dp = round2Local($m['data_price'] ?? 0);
    $cls = cleanStrLocal($m['class_option'] ?? '', 60);
    $clsNorm = strtolower($cls);
    if ($clsNorm === '') $clsNorm = 'modif-item';
    // Normalize common variants coming from different clients/systems.
    if ($clsNorm === 'composition' || $clsNorm === 'removed' || $clsNorm === 'remove' || $clsNorm === 'retrait' || $clsNorm === 'ingredient-removed') {
      $clsNorm = 'composition-item';
    }
    $knownIntentClasses = ['composition-item', 'extra-item', 'option-item', 'ingredient-item'];
    // Heuristic: if price is 0 and it's NOT an explicit "+ option",
    // treat it as a removed composition ingredient for printing parity.
    if (!in_array($clsNorm, $knownIntentClasses, true) && abs($dp) < 0.00001 && preg_match('/^\s*\+/u', $nom) !== 1) {
      $clsNorm = 'composition-item';
    }
    $out[] = [
      'nom_option' => $nom,
      'class_option' => $clsNorm,
      'data_price' => $dp,
    ];
  }
  return $out;
}

try {
  $tz = new DateTimeZone('Europe/Paris');
  $day = DateTimeImmutable::createFromFormat('Y-m-d', $dateISO, $tz);
  if (!$day) respond(400, ['ok' => false, 'error' => 'Invalid dateISO']);

  $start = $day->setTime(0, 0, 0);
  $end   = $day->setTime(23, 59, 59);
  $startMs = ((int)$start->format('U')) * 1000;
  $endMs   = ((int)$end->format('U')) * 1000 + 999;

  // Strict UI grid check for HH:MM (no adjustment)
  $cfg = fetchStoreSlotConfig($pdo, $store);
  $firstDisplaySlot = normalizeHHMM((string)($cfg['first_display_slot'] ?? '18:00'), '18:00');
  $lastDisplaySlot = normalizeHHMM((string)($cfg['last_display_slot'] ?? '23:50'), '23:50');
  $slotDurationMin = (int)($cfg['slot_duration_min'] ?? 10);
  if ($slotDurationMin <= 0) $slotDurationMin = 10;
  $chipSlots = buildChipSlotsFromHHMM($firstDisplaySlot, $lastDisplaySlot, $slotDurationMin);
  $chipSlotSet = array_fill_keys($chipSlots, true);
  if (!isset($chipSlotSet[$heureHHMM])) {
    error_log("[updateOrder] off-grid heure_prepa_hhmm; store={$store} date={$dateISO} hhmm={$heureHHMM}");
    respond(400, ['ok' => false, 'error' => 'Heure hors grille (strict time)', 'kind' => 'heure_prepa_offgrid', 'hhmm' => $heureHHMM]);
  }

  // Build heure_prepa ms from selected date + HH:MM in Europe/Paris
  $dt = DateTimeImmutable::createFromFormat('Y-m-d H:i', $dateISO . ' ' . $heureHHMM, $tz);
  if (!$dt) respond(400, ['ok' => false, 'error' => 'Invalid heure_prepa']);
  $heurePrepaMs = ((int)$dt->format('U')) * 1000;

  if ($heurePrepaMs < $startMs || $heurePrepaMs > $endMs) {
    respond(400, ['ok' => false, 'error' => 'Heure hors journée (strict)', 'kind' => 'heure_prepa_out_of_day']);
  }

  // Locate order strictly by (id, id_date) and ensure it belongs to the selected day
  $sqlFind = "SELECT id, id_date, heure_prepa, id_client, livraison, adresse_temporaire
              FROM {$tableOrders}
              WHERE (statut IS NULL OR statut <> 'deleted')
                AND id = :id
                AND id_date = :id_date
                AND heure_prepa BETWEEN :startMs AND :endMs
              LIMIT 1";
  $stmtF = $pdo->prepare($sqlFind);
  $stmtF->bindValue(':id', $orderId, PDO::PARAM_INT);
  $stmtF->bindValue(':id_date', $idDate, PDO::PARAM_INT);
  $stmtF->bindValue(':startMs', $startMs, PDO::PARAM_INT);
  $stmtF->bindValue(':endMs', $endMs, PDO::PARAM_INT);
  $stmtF->execute();
  $found = $stmtF->fetch(PDO::FETCH_ASSOC);
  if (!$found) {
    error_log("[updateOrder] Order not found for day; store={$store} dateISO={$dateISO} order_id={$orderId} id_date={$idDate} startMs={$startMs} endMs={$endMs}");
    respond(404, ['ok' => false, 'error' => 'Order not found for day']);
  }
  $idClient = isset($found['id_client']) ? (int)$found['id_client'] : 0;

  // ── Snapshot BEFORE update (DB source of truth) ──
  $beforeHeurePrepaMs = (int)($found['heure_prepa'] ?? 0);
  $beforeLivraison = ((int)($found['livraison'] ?? 0) === 1) ? 1 : 0;
  $beforeTotalCents = fetchOrderTotalCentsLocal($pdo, $tableLines, $tableModifs, $colOrderFk, $colLineFk, $orderId);

  // Normalize flags
  $coupe = ($coupe === 1) ? 1 : 0;
  $livraison = ($livraison === 1) ? 1 : 0;
  if ($livraison !== 1) $temporaryDeliveryAddress = null;
  if ($nbrPizzas < 0) $nbrPizzas = 0;

  if ($livraison === 1 && is_array($clientPayload) && !shouldPreserveDeliveryCityPayload($clientPayload)) {
    $normalizedClientPayload = caisseNormalizeDeliveryCityPayload($clientPayload, $store, 'ville', 'code_postal');
    if (empty($normalizedClientPayload['ok']) || !is_array($normalizedClientPayload['payload'])) {
      respond(400, [
        'ok' => false,
        'error' => 'delivery_city_not_allowed',
        'target' => 'client',
        'message' => 'Adresse client : ville / code postal non autorisés pour ce point de vente.',
      ]);
    }
    $clientPayload = $normalizedClientPayload['payload'];
  }

  if ($livraison === 1 && is_array($temporaryDeliveryAddress) && !shouldPreserveDeliveryCityPayload($temporaryDeliveryAddress)) {
    $normalizedTemporaryAddress = caisseNormalizeDeliveryCityPayload($temporaryDeliveryAddress, $store, 'ville', 'code_postal');
    if (empty($normalizedTemporaryAddress['ok']) || !is_array($normalizedTemporaryAddress['payload'])) {
      respond(400, [
        'ok' => false,
        'error' => 'delivery_city_not_allowed',
        'target' => 'temporary_delivery_address',
        'message' => 'Adresse provisoire : ville / code postal non autorisés pour ce point de vente.',
      ]);
    }
    $temporaryDeliveryAddress = $normalizedTemporaryAddress['payload'];
  }

  // Normalize diff lines
  $adds = [];
  foreach ($addList as $it) {
    $l = normalizeLineForInsert($it);
    if ($l['id_pos_pizzas'] <= 0) respond(400, ['ok' => false, 'error' => 'Invalid add line id_pos_pizzas']);
    if ($l['data_id'] === '') respond(400, ['ok' => false, 'error' => 'Invalid add line data_id']);
    $l['modifs'] = validateModifsArray($l['modifs']);
    $adds[] = $l;
  }
  $upds = [];
  foreach ($updList as $it) {
    $l = normalizeUpdateLine($it);
    if ($l['ordered_id'] <= 0) respond(400, ['ok' => false, 'error' => 'Invalid update ordered_id']);
    if ($l['id_pos_pizzas'] <= 0) respond(400, ['ok' => false, 'error' => 'Invalid update line id_pos_pizzas']);
    if ($l['data_id'] === '') respond(400, ['ok' => false, 'error' => 'Invalid update line data_id']);
    $l['modifs'] = validateModifsArray($l['modifs']);
    $upds[] = $l;
  }
  $dels = [];
  foreach ($delList as $it) {
    $oid = normalizeDeleteLine($it);
    if ($oid > 0) $dels[] = $oid;
  }

  $pdo->beginTransaction();

  // ── Upsert client on UPDATE (same rule as CREATE) ──
  // Source of truth = UI at validate time. If client info was edited, persist it.
  // Fail-safe: if no phone provided, we keep existing id_client (backward compatible),
  // but if phone is provided it must resolve to a client.
  if (trim($phoneRaw) !== '') {
    $newClientId = upsertClientFromPayload($pdo, $phoneRaw, $clientPayload, $store);
    if ($newClientId <= 0) {
      $pdo->rollBack();
      respond(500, ['ok' => false, 'error' => 'Unable to resolve client']);
    }
    $idClient = $newClientId;
  }

  // Auto-learn first name (fail-safe): add to pos_liste_prenoms if missing.
  // We do this after potential upsert, and always from DB (std_clients) so it also works
  // when the UPDATE payload is a merge-patch without nom_prenom.
  if (function_exists('rememberFirstNameFromClientId') && $idClient > 0) {
    try {
      rememberFirstNameFromClientId($pdo, $idClient);
    } catch (Throwable $eFn) {
      // Fail-safe: never block order update on first-name learning issues.
      error_log("[updateOrder] firstName upsert skipped (fail-safe) store={$store} client_id={$idClient} order_id={$orderId} err=" . $eFn->getMessage());
    }
  }
  $existingTempAddressId = isset($found['adresse_temporaire']) ? (int)$found['adresse_temporaire'] : 0;
  $tempAddressId = tempAddressPersist($pdo, $temporaryDeliveryAddress, $existingTempAddressId > 0 ? $existingTempAddressId : null, $store);

  // DELETE lines
  if (count($dels) > 0) {
    $placeholders = implode(',', array_fill(0, count($dels), '?'));
    // delete modifs first
    $stmtDM = $pdo->prepare("DELETE FROM {$tableModifs} WHERE {$colLineFk} IN ({$placeholders})");
    foreach ($dels as $i => $oid) $stmtDM->bindValue($i + 1, (int)$oid, PDO::PARAM_INT);
    $stmtDM->execute();
    // delete line rows (ensure they belong to this order)
    // IMPORTANT: do not mix positional (?) and named (:x) placeholders in the same statement (PDO error -> 500).
    $stmtDL = $pdo->prepare("DELETE FROM {$tableLines} WHERE id IN ({$placeholders}) AND {$colOrderFk} = ?");
    foreach ($dels as $i => $oid) {
      $stmtDL->bindValue($i + 1, (int)$oid, PDO::PARAM_INT);
    }
    // last positional param = order_id
    $stmtDL->bindValue(count($dels) + 1, $orderId, PDO::PARAM_INT);
    $stmtDL->execute();
  }

  // UPDATE existing lines (row + modifs refresh)
  $stmtCheckLine = $pdo->prepare("SELECT id FROM {$tableLines} WHERE id = :id AND {$colOrderFk} = :order_id LIMIT 1");
  $stmtUL = $pdo->prepare("UPDATE {$tableLines} SET id_pos_pizzas = :id_pos_pizzas, data_id = :data_id WHERE id = :id AND {$colOrderFk} = :order_id");
  $stmtDelM = $pdo->prepare("DELETE FROM {$tableModifs} WHERE {$colLineFk} = :line_id");
  $stmtInsM = $pdo->prepare("INSERT INTO {$tableModifs} ({$colLineFk}, nom_option, class_option, data_price) VALUES (:line_id, :nom_option, :class_option, :data_price)");

  $updatedLines = 0;
  $updatedModifs = 0;
  foreach ($upds as $u) {
    $oid = (int)$u['ordered_id'];
    $stmtCheckLine->bindValue(':id', $oid, PDO::PARAM_INT);
    $stmtCheckLine->bindValue(':order_id', $orderId, PDO::PARAM_INT);
    $stmtCheckLine->execute();
    $exists = $stmtCheckLine->fetchColumn();
    if ($exists === false) {
      $pdo->rollBack();
      respond(400, ['ok' => false, 'error' => 'Update line not in order', 'ordered_id' => $oid]);
    }

    $stmtUL->bindValue(':id', $oid, PDO::PARAM_INT);
    $stmtUL->bindValue(':order_id', $orderId, PDO::PARAM_INT);
    $stmtUL->bindValue(':id_pos_pizzas', (int)$u['id_pos_pizzas'], PDO::PARAM_INT);
    $stmtUL->bindValue(':data_id', (string)$u['data_id'], PDO::PARAM_STR);
    $stmtUL->execute();
    $updatedLines += 1;

    // refresh modifs
    $stmtDelM->bindValue(':line_id', $oid, PDO::PARAM_INT);
    $stmtDelM->execute();
    foreach ($u['modifs'] as $m) {
      $stmtInsM->bindValue(':line_id', $oid, PDO::PARAM_INT);
      $stmtInsM->bindValue(':nom_option', (string)$m['nom_option'], PDO::PARAM_STR);
      $stmtInsM->bindValue(':class_option', (string)$m['class_option'], PDO::PARAM_STR);
      $stmtInsM->bindValue(':data_price', number_format((float)$m['data_price'], 2, '.', ''), PDO::PARAM_STR);
      $stmtInsM->execute();
      $updatedModifs += 1;
    }
  }

  // INSERT new lines
  $insertedLines = 0;
  $insertedModifs = 0;
  if ($store === 'pel') {
    $sqlIL = "INSERT INTO {$tableLines} ({$colOrderFk}, id_pos_pizzas, data_id, data_type, is_web_addition, is_free)
              VALUES (:order_id, :id_pos_pizzas, :data_id, NULL, 0, 0)";
  } else {
    $sqlIL = "INSERT INTO {$tableLines} ({$colOrderFk}, id_pos_pizzas, data_id, is_web_addition, is_free)
              VALUES (:order_id, :id_pos_pizzas, :data_id, 0, 0)";
  }
  $stmtIL = $pdo->prepare($sqlIL);

  foreach ($adds as $a) {
    $stmtIL->bindValue(':order_id', $orderId, PDO::PARAM_INT);
    $stmtIL->bindValue(':id_pos_pizzas', (int)$a['id_pos_pizzas'], PDO::PARAM_INT);
    $stmtIL->bindValue(':data_id', (string)$a['data_id'], PDO::PARAM_STR);
    $stmtIL->execute();
    $lineId = (int)$pdo->lastInsertId();
    $insertedLines += 1;

    foreach ($a['modifs'] as $m) {
      $stmtInsM->bindValue(':line_id', $lineId, PDO::PARAM_INT);
      $stmtInsM->bindValue(':nom_option', (string)$m['nom_option'], PDO::PARAM_STR);
      $stmtInsM->bindValue(':class_option', (string)$m['class_option'], PDO::PARAM_STR);
      $stmtInsM->bindValue(':data_price', number_format((float)$m['data_price'], 2, '.', ''), PDO::PARAM_STR);
      $stmtInsM->execute();
      $insertedModifs += 1;
    }
  }

  // Update order header (keep id_date immutable; heure_prepa can move within day)
  $sqlUO = "UPDATE {$tableOrders}
            SET heure_prepa = :heure_prepa,
                id_client = :id_client,
                coupe = :coupe,
                livraison = :livraison,
                nbr_pizzas = :nbr_pizzas,
                prix_com = :prix_com,
                adresse_temporaire = :adresse_temporaire
            WHERE id = :id AND id_date = :id_date";
  $stmtUO = $pdo->prepare($sqlUO);
  $stmtUO->bindValue(':heure_prepa', $heurePrepaMs, PDO::PARAM_INT);
  $stmtUO->bindValue(':id_client', $idClient, PDO::PARAM_INT);
  $stmtUO->bindValue(':coupe', $coupe, PDO::PARAM_INT);
  $stmtUO->bindValue(':livraison', $livraison, PDO::PARAM_INT);
  $stmtUO->bindValue(':nbr_pizzas', $nbrPizzas, PDO::PARAM_INT);
  $stmtUO->bindValue(':prix_com', $prixComStr, PDO::PARAM_STR);
  if ($tempAddressId === null) $stmtUO->bindValue(':adresse_temporaire', null, PDO::PARAM_NULL);
  else $stmtUO->bindValue(':adresse_temporaire', $tempAddressId, PDO::PARAM_INT);
  $stmtUO->bindValue(':id', $orderId, PDO::PARAM_INT);
  $stmtUO->bindValue(':id_date', $idDate, PDO::PARAM_INT);
  $stmtUO->execute();

  $customerRow = loadCustomerById($pdo, $idClient);
  if (is_array($customerRow)) {
    $loyaltyGuard = enforceOrderFinalLoyaltyUsage($pdo, $customerRow, $store, $orderId, $heurePrepaMs, 'reject');
    if (empty($loyaltyGuard['ok'])) {
      $pdo->rollBack();
      respond(409, [
        'ok' => false,
        'error' => 'loyalty_already_used_on_day',
        'kind' => 'business_rule_recalc_required',
        'message' => 'L’avantage fidélité a déjà été consommé sur cette journée pour ce client/groupe. Merci de recalculer la commande.',
        'date_only' => (string)($loyaltyGuard['date_only'] ?? $dateISO),
      ]);
    }
  }

  // Queue ESC/POS print job (receipt ticket from updated order) atomically with the order update.
  $printQueued = false;
  $printJobId = 0;
  if (function_exists('queueOrderReceiptTicketJob')) {
    try {
      $printJobId = (int)queueOrderReceiptTicketJob($pdo, $store, $orderId, 'caisse');
      if ($printJobId > 0) $printQueued = true;
      else error_log("[updateOrder] print job insert failed (fail-safe) store={$store} order_id={$orderId}");
    } catch (Throwable $ePrint) {
      error_log("[updateOrder] print job failed (fail-safe) store={$store} order_id={$orderId} err=" . $ePrint->getMessage());
    }
  } else {
    error_log("[updateOrder] printJobs helper missing; skip print job store={$store} order_id={$orderId}");
  }

  // ── Auto decision: queue SMS on UPDATE only if (total OR heure_prepa OR livraison) changed ──
  // Keep inside the same transaction as order update (fail-safe).
  $afterTotalCents = fetchOrderTotalCentsLocal($pdo, $tableLines, $tableModifs, $colOrderFk, $colLineFk, $orderId);
  $shouldSendSms = !(
    $beforeTotalCents === $afterTotalCents &&
    $beforeHeurePrepaMs === $heurePrepaMs &&
    $beforeLivraison === $livraison
  );

  if ($shouldSendSms && function_exists('queueOrderSms')) {
    try {
      if ($idClient > 0) {
        $smsType = 'order_modif';
        $deliveryMode = ($livraison === 1) ? 'delivery' : 'pickup';
        queueOrderSms($pdo, $store, $orderId, $idClient, $smsType, $deliveryMode);
      } else {
        error_log("[updateOrder] SMS skip: missing id_client store={$store} order_id={$orderId}");
      }
    } catch (Throwable $eSms) {
      // Fail-safe: never block update on SMS issues.
      error_log("[updateOrder] SMS skipped (fail-safe): store={$store} order_id={$orderId} err=" . $eSms->getMessage());
    }
  }

  $pdo->commit();

  respond(200, [
    'ok' => true,
    'store' => $store,
    'id' => $orderId,
    'id_date' => $idDate,
    'dateISO' => $dateISO,
    'heure_prepa' => $heurePrepaMs,
    'temp_address_id' => $tempAddressId,
    'id_client' => $idClient,
    'diff' => [
      'lines_inserted' => $insertedLines,
      'lines_deleted' => count($dels),
      'lines_updated' => $updatedLines,
      'modifs_inserted' => $insertedModifs,
      'modifs_updated' => $updatedModifs,
    ],
    'print_job' => [
      'queued' => $printQueued,
      'job_id' => $printJobId,
    ],
  ]);
} catch (Throwable $e) {
  try { if ($pdo->inTransaction()) $pdo->rollBack(); } catch (Throwable $e2) {}
  $storeLog = isset($store) ? (string)$store : '';
  $orderLog = isset($orderId) ? (int)$orderId : 0;
  error_log("[updateOrder] crash store={$storeLog} order_id={$orderLog} err=" . $e->getMessage() . " @ " . $e->getFile() . ":" . $e->getLine());
  respond(500, ['ok' => false, 'error' => 'Server error', 'kind' => 'server_error']);
}