<?php
declare(strict_types=1);

/* doc-project | caisse-aqp/public/api/saveOrder.php | Reçoit, valide et enregistre une commande caisse en base, puis file les tâches d’impression et de SMS, avec persistance de l’adresse de livraison provisoire portée par la commande, contrôle 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 validation bloquante de la fidélité juste avant enregistrement final pour empêcher toute double consommation sur la journée. | Expose: respond, normalizeBool, shouldPreserveDeliveryCityPayload, computeDayBoundsMs, computeNextIdDate | Dépend de: config.php, _lib/clientUpsert.php, _lib/payLink.php, _lib/printJobs.php, _lib/smsQueue.php, _lib/firstNamesUpsert.php, _lib/tempAddress.php, _lib/deliveryCityChoices.php, lib/loyalty.php | Impacte: BDD, file d’impression, file SMS, état de transaction, validation métier des adresses caisse, blocage serveur des conflits fidélité | Tables: pos_commandes(_pel), pos_pizzas_commandees(_pel), pos_modifs_pizzas(_pel), pos_pizzas, std_clients, pos_liste_prenoms, 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);
}

function computeDayBoundsMs(string $dateOnly, DateTimeZone $tz): array {
  $day = DateTimeImmutable::createFromFormat('Y-m-d', $dateOnly, $tz);
  if (!$day) return [0, 0];
  $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;
  return [$startMs, $endMs];
}

function computeNextIdDate(PDO $pdo, string $tableOrders, int $startMs, int $endMs): int {
  $sql = "SELECT MAX(id_date) AS max_id_date
          FROM {$tableOrders}
          WHERE (statut IS NULL OR statut <> 'deleted')
            AND heure_prepa BETWEEN :startMs AND :endMs";
  $stmt = $pdo->prepare($sql);
  $stmt->bindValue(':startMs', $startMs, PDO::PARAM_INT);
  $stmt->bindValue(':endMs', $endMs, PDO::PARAM_INT);
  $stmt->execute();
  $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
  $max = isset($row['max_id_date']) ? (int)$row['max_id_date'] : 0;
  $next = $max + 1;
  return ($next > 0) ? $next : 1;
}

// Load PDO from project root config.php
@require_once __DIR__ . '/../../config.php';
@require_once __DIR__ . '/_lib/clientUpsert.php';
@require_once __DIR__ . '/_lib/payLink.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']);
}

// UI flag (future SMS decision) — currently no-op (no DB write).
$modifCommande = normalizeBool($json['modif_commande'] ?? false);
if ($modifCommande) error_log("[saveOrder] modif_commande=true store={$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';

// Input fields
$dateISO = isset($json['dateISO']) ? (string)$json['dateISO'] : '';
$idDateRaw = $json['id_date'] ?? null;
$heureHHMM = isset($json['heure_prepa_hhmm']) ? (string)$json['heure_prepa_hhmm'] : '';
$heureMsRaw = $json['heure_prepa_ms'] ?? null;

$coupe = isset($json['coupe']) ? (int)$json['coupe'] : 0;
$livraison = isset($json['livraison']) ? (int)$json['livraison'] : 0;
$idClient = isset($json['id_client']) ? (int)$json['id_client'] : 0;
$phoneRaw = isset($json['phoneNumber']) ? (string)$json['phoneNumber'] : '';
$toPrint = isset($json['to_print']) ? (int)$json['to_print'] : 0;
$clientPayload = $json['client'] ?? null;

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

$items = $json['items'] ?? null;
if (!is_array($items) || count($items) === 0) {
  respond(400, ['ok' => false, 'error' => 'Panier vide']);
}

// Validate date_only
$tz = new DateTimeZone('Europe/Paris');
$dateOnly = null;
if (is_string($dateISO) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateISO)) {
  $d = DateTimeImmutable::createFromFormat('Y-m-d', $dateISO, $tz);
  if ($d instanceof DateTimeImmutable) $dateOnly = $d->format('Y-m-d');
}
if ($dateOnly === null) {
  $dateOnly = (new DateTimeImmutable('now', $tz))->format('Y-m-d');
}

// id_date is assigned server-side (next available for the computed day)
$idDate = is_numeric($idDateRaw) ? (int)$idDateRaw : 0;

// Validate heure_prepa (epoch ms or HH:MM)
$heurePrepaMs = null;
if (is_numeric($heureMsRaw)) {
  $ms = (int)$heureMsRaw;
  if ($ms > 0) $heurePrepaMs = $ms;
}
if ($heurePrepaMs === null) {
  if (!preg_match('/^\d{2}:\d{2}$/', $heureHHMM)) {
    respond(400, ['ok' => false, 'error' => 'Choisir un créneau']);
  }
  // Build timestamp from selected date + HH:MM in Europe/Paris
  $dt = DateTimeImmutable::createFromFormat('Y-m-d H:i', $dateOnly . ' ' . $heureHHMM, $tz);
  if (!$dt) {
    respond(400, ['ok' => false, 'error' => 'Invalid heure_prepa']);
  }
  $heurePrepaMs = ((int)$dt->format('U')) * 1000;
}

// Compute next id_date for the day derived from heure_prepa/dateOnly (store-specific table)
try {
  [$dayStartMs, $dayEndMs] = computeDayBoundsMs($dateOnly, $tz);
  if ($dayStartMs <= 0 || $dayEndMs <= 0) {
    respond(400, ['ok' => false, 'error' => 'Invalid date_only']);
  }
  $idDate = computeNextIdDate($pdo, $tableOrders, $dayStartMs, $dayEndMs);
} catch (Throwable $e) {
  error_log("[saveOrder] computeNextIdDate failed store={$store} dateOnly={$dateOnly} err=" . $e->getMessage());
  respond(500, ['ok' => false, 'error' => 'Server error']);
}

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

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'];
}

// Prepare items: validate ids and compute totals using DB prices
$posIds = [];
$normItems = [];
$nbrPizzas = 0;
foreach ($items as $it) {
  if (!is_array($it)) continue;
  $pid = isset($it['id_pos_pizzas']) ? (int)$it['id_pos_pizzas'] : 0;
  if ($pid <= 0) respond(400, ['ok' => false, 'error' => 'Invalid item id_pos_pizzas']);
  $dataId = isset($it['data_id']) ? trim((string)$it['data_id']) : '';
  if ($dataId === '') respond(400, ['ok' => false, 'error' => 'Invalid item data_id']);

  $cls = isset($it['class']) ? strtolower(trim((string)$it['class'])) : '';
  if ($cls === 'pizza') $nbrPizzas += 1;

  $modifs = $it['modifs'] ?? [];
  if (!is_array($modifs)) $modifs = [];

  $normItems[] = [
    'id_pos_pizzas' => $pid,
    'data_id' => $dataId,
    'class' => $cls,
    'modifs' => $modifs,
  ];
  $posIds[$pid] = true;
}
if (count($normItems) === 0) {
  respond(400, ['ok' => false, 'error' => 'Panier vide']);
}

// Load base prices from pos_pizzas
$ids = array_keys($posIds);
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$priceById = [];
try {
  $stmtP = $pdo->prepare("SELECT id, price_large FROM pos_pizzas WHERE id IN ($placeholders)");
  foreach ($ids as $i => $pid) {
    $stmtP->bindValue($i + 1, (int)$pid, PDO::PARAM_INT);
  }
  $stmtP->execute();
  while ($row = $stmtP->fetch(PDO::FETCH_ASSOC)) {
    $rid = isset($row['id']) ? (int)$row['id'] : 0;
    $pr = isset($row['price_large']) ? (float)$row['price_large'] : 0.0;
    if ($rid > 0) $priceById[$rid] = $pr;
  }
} catch (Throwable $e) {
  error_log("[saveOrder] load prices failed store={$store} err=" . $e->getMessage());
  respond(500, ['ok' => false, 'error' => 'Server error']);
}

// Compute total in cents (base + modifs for pizzas)
$totalCents = 0;
foreach ($normItems as $it) {
  $pid = (int)$it['id_pos_pizzas'];
  if (!array_key_exists($pid, $priceById)) {
    respond(400, ['ok' => false, 'error' => 'Unknown pos_pizzas id: ' . $pid]);
  }
  $base = (float)$priceById[$pid];
  $totalCents += (int)round($base * 100);

  if ($it['class'] === 'pizza' && is_array($it['modifs'])) {
    foreach ($it['modifs'] as $m) {
      if (!is_array($m)) continue;
      $dp = $m['data_price'] ?? 0;
      $n = is_numeric($dp) ? (float)$dp : 0.0;
      $totalCents += (int)round($n * 100);
    }
  }
}
$prixCom = $totalCents / 100.0;
$prixComStr = number_format($prixCom, 2, '.', '');

try {
  $pdo->beginTransaction();

  // Upsert client inside transaction (client -> order -> lines)
  if (trim($phoneRaw) === '') {
    $pdo->rollBack();
    respond(400, ['ok' => false, 'error' => 'Missing phoneNumber (client required)']);
  }
  $upsertedId = upsertClientFromPayload($pdo, $phoneRaw, $clientPayload, $store);
  if ($upsertedId <= 0) {
    $pdo->rollBack();
    respond(500, ['ok' => false, 'error' => 'Unable to resolve client']);
  }
  $idClient = $upsertedId;
  $tempAddressId = tempAddressPersist($pdo, $temporaryDeliveryAddress, null, $store);

  // Auto-learn first name (fail-safe): add to pos_liste_prenoms if missing.
  if (function_exists('rememberFirstNameFromClientId')) {
    try {
      rememberFirstNameFromClientId($pdo, $idClient);
    } catch (Throwable $eFn) {
      // Fail-safe: never block order save on first-name learning issues.
      error_log("[saveOrder] firstName upsert skipped (fail-safe) store={$store} client_id={$idClient} err=" . $eFn->getMessage());
    }
  }

  // Generate payLink + verif_code inside the same transaction (before INSERT order)
  // NOTE: payLink must be unique within the store-specific orders table.
  $payLink = generateUniquePayLinkIdForTable($pdo, $tableOrders);
  $verifCode = generateVerificationCode();

  // Insert commande
  $sqlO = "INSERT INTO {$tableOrders} (
    date_com,
    date_only,
    id_date,
    id_client,
    statut,
    heure_prepa,
    coupe,
    livraison,
    nbr_pizzas,
    prix_com,
    statutSms,
    smsRetard,
    smsPrepa,
    payLink,
    verif_code,
    adresse_temporaire,
    is_online,
    need_account,
    to_print,
    account_to_print
  ) VALUES (
    :date_com,
    :date_only,
    :id_date,
    :id_client,
    'pending',
    :heure_prepa,
    :coupe,
    :livraison,
    :nbr_pizzas,
    :prix_com,
    1,
    0,
    0,
    :payLink,
    :verif_code,
    :adresse_temporaire,
    0,
    0,
    :to_print,
    0
  )";

  $now = new DateTimeImmutable('now', $tz);
  $stmtO = $pdo->prepare($sqlO);
  $stmtO->bindValue(':date_com', $now->format('Y-m-d H:i:s'), PDO::PARAM_STR);
  $stmtO->bindValue(':date_only', $dateOnly, PDO::PARAM_STR);
  $stmtO->bindValue(':id_date', $idDate, PDO::PARAM_INT);
  $stmtO->bindValue(':id_client', $idClient, PDO::PARAM_INT);
  $stmtO->bindValue(':heure_prepa', $heurePrepaMs, PDO::PARAM_INT);
  $stmtO->bindValue(':coupe', $coupe, PDO::PARAM_INT);
  $stmtO->bindValue(':livraison', $livraison, PDO::PARAM_INT);
  $stmtO->bindValue(':nbr_pizzas', $nbrPizzas, PDO::PARAM_INT);
  $stmtO->bindValue(':prix_com', $prixComStr, PDO::PARAM_STR);
  $stmtO->bindValue(':payLink', $payLink, PDO::PARAM_STR);
  $stmtO->bindValue(':verif_code', $verifCode, PDO::PARAM_STR);
  if ($tempAddressId === null) $stmtO->bindValue(':adresse_temporaire', null, PDO::PARAM_NULL);
  else $stmtO->bindValue(':adresse_temporaire', $tempAddressId, PDO::PARAM_INT);
  $stmtO->bindValue(':to_print', $toPrint, PDO::PARAM_INT);
  $stmtO->execute();

  $orderId = (int)$pdo->lastInsertId();
  if ($orderId <= 0) {
    $pdo->rollBack();
    respond(500, ['ok' => false, 'error' => 'Insert failed']);
  }

  // Insert lines
  if ($store === 'pel') {
    $sqlL = "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 {
    $sqlL = "INSERT INTO {$tableLines} (
      {$colOrderFk},
      id_pos_pizzas,
      data_id,
      is_web_addition,
      is_free
    ) VALUES (
      :order_id,
      :id_pos_pizzas,
      :data_id,
      0,
      0
    )";
  }

  $stmtL = $pdo->prepare($sqlL);

  // Insert modifs (only for pizzas)
  $sqlM = "INSERT INTO {$tableModifs} (
    {$colLineFk},
    nom_option,
    class_option,
    data_price
  ) VALUES (
    :line_id,
    :nom_option,
    :class_option,
    :data_price
  )";
  $stmtM = $pdo->prepare($sqlM);

  $lineCount = 0;
  $modifCount = 0;

  foreach ($normItems as $it) {
    $stmtL->bindValue(':order_id', $orderId, PDO::PARAM_INT);
    $stmtL->bindValue(':id_pos_pizzas', (int)$it['id_pos_pizzas'], PDO::PARAM_INT);
    $stmtL->bindValue(':data_id', (string)$it['data_id'], PDO::PARAM_STR);
    $stmtL->execute();
    $lineId = (int)$pdo->lastInsertId();
    $lineCount += 1;

    if ($it['class'] === 'pizza' && is_array($it['modifs']) && $lineId > 0) {
      foreach ($it['modifs'] as $m) {
        if (!is_array($m)) continue;
        $nom = isset($m['nom_option']) ? trim((string)$m['nom_option']) : '';
        if ($nom === '') continue;
        if (mb_strlen($nom, 'UTF-8') > 120) $nom = mb_substr($nom, 0, 120, 'UTF-8');

        $cls = isset($m['class_option']) ? trim((string)$m['class_option']) : '';
        $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'];
        if (mb_strlen($cls, 'UTF-8') > 60) $cls = mb_substr($cls, 0, 60, 'UTF-8');

        $dp = $m['data_price'] ?? 0;
        $n = is_numeric($dp) ? (float)$dp : 0.0;
        $dpStr = number_format($n, 2, '.', '');

        // Heuristic: if price is 0 and it's NOT an explicit "+ option",
        // treat it as a removed composition ingredient for printing parity.
        // (Keeps paid extras/options unchanged.)
        if (!in_array($clsNorm, $knownIntentClasses, true) && abs($n) < 0.00001 && preg_match('/^\s*\+/u', $nom) !== 1) {
          $clsNorm = 'composition-item';
        }

        $stmtM->bindValue(':line_id', $lineId, PDO::PARAM_INT);
        $stmtM->bindValue(':nom_option', $nom, PDO::PARAM_STR);
        $stmtM->bindValue(':class_option', $clsNorm, PDO::PARAM_STR);
        $stmtM->bindValue(':data_price', $dpStr, PDO::PARAM_STR);
        $stmtM->execute();
        $modifCount += 1;
      }
    }
  }

  $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'] ?? $dateOnly),
      ]);
    }
  }

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

  // Queue SMS (fail-safe): uses pos_text_sms templates + std_clients.stop_sms opt-out.
  // Keep inside the same transaction as order save.
  if (function_exists('queueOrderSms')) {
    try {
      $smsType = $modifCommande ? 'order_modif' : 'order_confirm';
      $deliveryMode = ($livraison === 1) ? 'delivery' : 'pickup';
      queueOrderSms($pdo, $store, $orderId, $idClient, $smsType, $deliveryMode);
    } catch (Throwable $eSms) {
      // Fail-safe: never block order validation on SMS issues.
      error_log("[saveOrder] SMS skipped (fail-safe): store={$store} order_id={$orderId} err=" . $eSms->getMessage());
    }
  } else {
    error_log("[saveOrder] smsQueue helper missing; skip SMS store={$store} order_id={$orderId}");
  }

  $pdo->commit();

  respond(200, [
    'ok' => true,
    'store' => $store,
    'order_id' => $orderId,
    'id_client' => $idClient, // reliable (new or existing after upsert)
    'id_date' => $idDate,
    'date_only' => $dateOnly,
    'heure_prepa' => $heurePrepaMs,
    'temp_address_id' => $tempAddressId,
    'nbr_pizzas' => $nbrPizzas,
    'prix_com' => (float)$prixComStr,
    'payLink' => $payLink,
    'verif_code' => $verifCode,
    'lines_inserted' => $lineCount,
    'modifs_inserted' => $modifCount,
    'print_job' => [
      'queued' => $printQueued,
      'job_id' => $printJobId,
    ],
  ]);
} catch (Throwable $e) {
  try {
    if ($pdo->inTransaction()) $pdo->rollBack();
  } catch (Throwable $e2) {}
  $storeLog = isset($store) ? (string)$store : '';
  error_log("[saveOrder] crash store={$storeLog} err=" . $e->getMessage() . " @ " . $e->getFile() . ":" . $e->getLine());
  respond(500, ['ok' => false, 'error' => 'Server error', 'kind' => 'server_error']);
}
