<?php
/**
 * Fichier  : api/products.php
 * Auteur   : KREMER Régis
 * Version  : 2.6.0
 * Date     : 2026-07-08
 * Description : API REST — CRUD des produits, images, déclinaisons génériques et validation MIME
 *
 * Changelog :
 *   v2.1.0 (2026-02-21) — Version initiale auditée et sécurisée
 *   v2.1.1 (2026-02-21) — Corrections sécurité (CSRF, MIME, session, migrateSuppliers)
 *   v2.2.0 (2026-02-21) — Permissions magasins, bons de commande, rapport d'écart,
 *   v2.3.0 (2026-02-26) — Lot 1: permissions fines + packagings dynamiques (migration).
 *                      import CSV, historique prix, bon de livraison multi-lignes
 *   v2.3.25 (2026-06-09) — Fiabilisation EAN, magasin par défaut et stock/rangement produit.
 *   v2.5.0 (2026-07-08) — Perf : plafond N versions/produit dans recordProductVersion (borne product_versions.json).
 *   v2.6.0 (2026-07-08) — recordProductVersion écrit data/versions/{id}.json (perf, plus de global).
 */
require_once dirname(__DIR__).'/config.php';
startSession(); requireAuth();
$method = $_SERVER['REQUEST_METHOD'];
if($method === 'GET') {
    requirePerm('products.read');
}
if($method !== 'GET') {
    requireAdmin();
    csrfCheck();
}

// ── Lot 4: EAN multiples + versioning ──
function sanitizeEans($raw): array {
    if(is_string($raw)){
        $trimmed = trim($raw);
        if($trimmed === '' || in_array(strtolower($trimmed), ['[]', '{}', 'null', 'undefined'], true)) {
            return [];
        }
        $decoded = json_decode($trimmed, true);
        if(json_last_error() === JSON_ERROR_NONE) {
            $raw = is_array($decoded) ? $decoded : [];
        } else {
            $raw = array_filter(array_map('trim', preg_split('/[,;\s]+/', $trimmed)));
        }
    }
    if(!is_array($raw)) return [];
    $out = [];
    foreach($raw as $e){
        if(is_array($e) || is_object($e) || $e === null) continue;
        $e = trim((string)$e);
        if($e === '' || in_array(strtolower($e), ['[]', '{}', 'null', 'undefined'], true)) continue;
        if(!in_array($e, $out, true)) $out[] = $e;
    }
    return $out;
}

function sanitizeWarehouseIds($raw): array {
    if(is_string($raw)){
        $decoded = json_decode($raw, true);
        if(json_last_error() === JSON_ERROR_NONE) $raw = $decoded;
        else $raw = preg_split('/[,;\s]+/', trim($raw));
    }
    if(!is_array($raw)) return [];
    $out = [];
    foreach($raw as $id){
        $id = (int)$id;
        if($id > 0 && !in_array($id, $out, true)) $out[] = $id;
    }
    return $out;
}

function normalizeDefaultWarehouse($raw, array $allowedWarehouses): ?int {
    $id = (int)$raw;
    if($id > 0) {
        if(empty($allowedWarehouses) || in_array($id, $allowedWarehouses, true)) return $id;
        array_unshift($allowedWarehouses, $id);
    }
    return !empty($allowedWarehouses) ? (int)$allowedWarehouses[0] : null;
}

function ensureProductStockAssignment(int $productId, ?int $warehouseId, array $extra = []): void {
    if($productId <= 0 || !$warehouseId) return;
    $stock = loadData('stock');
    foreach($stock as &$row){
        if((string)($row['productId'] ?? '') === (string)$productId && (string)($row['warehouseId'] ?? '') === (string)$warehouseId){
            foreach(['picking','location','rack','shelf','locationId'] as $key){
                if(array_key_exists($key, $extra)) $row[$key] = $extra[$key];
            }
            $row['updatedAt'] = date('c');
            saveData('stock', $stock);
            return;
        }
    }
    unset($row);
    $stock[] = array_merge([
        'id' => genId(),
        'productId' => $productId,
        'warehouseId' => $warehouseId,
        'quantity' => 0,
        'minStock' => null,
        'maxStock' => null,
        'safetyStock' => null,
        'picking' => '',
        'location' => '',
        'rack' => '',
        'shelf' => '',
        'locationId' => null,
        'lots' => [],
        'lastMovementAt' => null,
        'createdAt' => date('c'),
    ], $extra);
    saveData('stock', $stock);
}



function sanitizeVariantAttributes($raw): array {
    if(is_string($raw)){
        $decoded = json_decode($raw, true);
        $raw = is_array($decoded) ? $decoded : [];
    }
    if(!is_array($raw)) return [];
    $out = [];
    foreach($raw as $key => $value){
        $k = preg_replace('/[^a-zA-Z0-9_\-]/', '', (string)$key);
        if($k === '') continue;
        if(is_array($value)) continue;
        if($value === null) continue;
        $v = is_string($value) ? trim($value) : $value;
        if($v === '') continue;
        $out[$k] = $v;
    }
    return $out;
}

function normalizeBool($value): bool {
    if(is_bool($value)) return $value;
    if(is_numeric($value)) return ((int)$value) === 1;
    return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}

function sanitizeSupplierLinks($raw): array {
    if(is_string($raw)){
        $decoded = json_decode($raw, true);
        $raw = is_array($decoded) ? $decoded : [];
    }
    if(!is_array($raw)) return [];
    $out = [];
    foreach($raw as $row){
        if(!is_array($row)) continue;
        $supplierId = trim((string)($row['supplierId'] ?? ''));
        $supplierSku = trim((string)($row['supplierSku'] ?? ''));
        $packagingLabel = trim((string)($row['packagingLabel'] ?? ''));
        $allowedWarehouses = $row['allowedWarehouses'] ?? [];
        if(is_string($allowedWarehouses)){
            $allowedWarehouses = json_decode($allowedWarehouses, true) ?: [];
        }
        if(!is_array($allowedWarehouses)) $allowedWarehouses = [];
        $item = [
            'supplierId' => $supplierId,
            'supplierSku' => $supplierSku,
            'purchasePrice' => round((float)($row['purchasePrice'] ?? 0), 4),
            'packagingLabel' => $packagingLabel,
            'packagingQty' => (float)($row['packagingQty'] ?? 0),
            'minOrderQty' => (float)($row['minOrderQty'] ?? 0),
            'leadTimeDays' => (int)($row['leadTimeDays'] ?? 0),
            'discountRate' => (float)($row['discountRate'] ?? 0),
            'preferred' => !empty($row['preferred']),
            'allowedWarehouses' => array_values(array_filter(array_map('strval', $allowedWarehouses), fn($v)=>$v!=='')),
        ];
        if($item['supplierId']==='' && $item['supplierSku']==='' && $item['purchasePrice']<=0 && $item['packagingLabel']===''){
            continue;
        }
        $out[] = $item;
    }
    return $out;
}

function recordProductVersion(int $productId, int $version, string $action, array $before, array $after): void {
    $u = currentUser();
    $diff = [];
    $keys = array_unique(array_merge(array_keys($before), array_keys($after)));
    foreach($keys as $k){
        if(in_array($k,['updatedAt','createdAt'],true)) continue;
        $b = $before[$k] ?? null;
        $a = $after[$k] ?? null;
        if($b === $a) continue;
        $diff[$k] = ['from'=>$b,'to'=>$a];
    }
    // ── Historique PAR PRODUIT (perf) ───────────────────────────────
    // Écrit un petit fichier data/versions/{id}.json au lieu de recharger/réécrire
    // le gros product_versions.json global. Le plafond N est appliqué par pvSaveOne().
    $arr = pvLoadOne($productId);
    $arr[] = [
        'id' => genId(),
        'productId' => $productId,
        'version' => $version,
        'action' => $action,
        'userId' => (int)($u['id'] ?? 0),
        'userName' => (string)($u['name'] ?? $u['username'] ?? ''),
        'diff' => $diff,
        'createdAt' => date('c'),
    ];
    pvSaveOne($productId, $arr);
}

// ── IMAGE UPLOAD (multipart/form-data) ──
// Supports create (default) and edit image (mode=editImage) without changing endpoints.
if($method === 'POST' && isset($_FILES['image']) && (($_GET['mode'] ?? '') !== 'editImage')){
    $name = trim($_POST['name'] ?? '');
    if($name === '') jsonError('Nom obligatoire.');
    $sku  = trim($_POST['sku'] ?? '') ?: nextSKU();
    foreach(loadData('products') as $p) if(($p['sku']??'') === $sku) jsonError('SKU déjà utilisé.');
    
    $imgUrl = '';
    if(($_FILES['image']['error'] ?? 1) === UPLOAD_ERR_OK){
        // Validate real MIME type (not just extension) to prevent PHP file upload
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $realMime = finfo_file($finfo, $_FILES['image']['tmp_name']);
        finfo_close($finfo);
        $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
        $allowedExts  = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
        $ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
        if(in_array($realMime, $allowedMimes) && in_array($ext, $allowedExts)){
            $fn = uniqid('img_').'.'.$ext;
            if(move_uploaded_file($_FILES['image']['tmp_name'], UPLOADS_DIR.'/'.$fn)) $imgUrl = 'uploads/'.$fn;
        } else {
            jsonError('Type de fichier non autorisé. Utilisez JPG, PNG, GIF ou WEBP.', 400);
        }
    }
    
    $item = addItem('products', [
        'name'             => $name,
        'sku'              => $sku,
        'barcode'          => trim($_POST['barcode'] ?? ''),
        'brand'            => trim($_POST['brand'] ?? ''),
        'category'         => trim($_POST['category'] ?? ''),
        'description'      => trim($_POST['description'] ?? ''),
        'cost'             => floatval($_POST['cost'] ?? 0),
        'supplier'         => trim($_POST['supplier'] ?? ''),
        'supplierLinks'    => sanitizeSupplierLinks($_POST['supplierLinks'] ?? []),
        'allowedWarehouses' => sanitizeWarehouseIds($_POST['allowedWarehouses'] ?? []),
        'picking'          => trim($_POST['picking'] ?? ''),
        'location'         => trim($_POST['location'] ?? ''),
        'rack'             => trim($_POST['rack'] ?? ''),
        'shelf'            => trim($_POST['shelf'] ?? ''),
        'datasheetUrl'     => trim($_POST['datasheetUrl'] ?? ''),
        'msdsUrl'          => trim($_POST['msdsUrl'] ?? ''),
        // Packaging (legacy kept + new fields)
        'unitType'         => trim($_POST['unitType'] ?? 'piece'),
        'unitsPerBox'      => intval($_POST['unitsPerBox'] ?? 0),
        'boxesPerPallet'   => intval($_POST['boxesPerPallet'] ?? 0),
        'volume'           => floatval($_POST['volume'] ?? 0),
        'packBaseUnit'     => trim($_POST['packBaseUnit'] ?? ''),
        'packCartonQty'    => intval($_POST['packCartonQty'] ?? 0),
        'packPaletteQty'   => intval($_POST['packPaletteQty'] ?? 0),
        'packBidonQty'     => floatval($_POST['packBidonQty'] ?? 0),
        'packFutQty'       => floatval($_POST['packFutQty'] ?? 0),
        'packAerosolQty'   => floatval($_POST['packAerosolQty'] ?? 0),
        'packSacQty'       => floatval($_POST['packSacQty'] ?? 0),
        'packSeauQty'      => floatval($_POST['packSeauQty'] ?? 0),
        'packRouleauQty'   => floatval($_POST['packRouleauQty'] ?? 0),
        'packBobineQty'    => floatval($_POST['packBobineQty'] ?? 0),
        'packTubeQty'      => floatval($_POST['packTubeQty'] ?? 0),
        'packBoiteQty'     => floatval($_POST['packBoiteQty'] ?? 0),
        'packPaquetQty'    => floatval($_POST['packPaquetQty'] ?? 0),
        'costByUnit'       => (isset($_POST['costByUnit']) && is_string($_POST['costByUnit'])) ? (json_decode($_POST['costByUnit'], true) ?: []) : [],
        'trackLot'         => filter_var($_POST['trackLot'] ?? false, FILTER_VALIDATE_BOOLEAN),
        'trackExpiry'      => filter_var($_POST['trackExpiry'] ?? false, FILTER_VALIDATE_BOOLEAN),
        'shelfLifeDays'    => ($_POST['shelfLifeDays'] ?? '') !== '' ? intval($_POST['shelfLifeDays']) : null,
        'safetyStock'      => ($_POST['safetyStock'] ?? '') !== '' ? intval($_POST['safetyStock']) : null,
        'cump'             => floatval($_POST['cump'] ?? floatval($_POST['cost'] ?? 0)),
        'image'            => $imgUrl,
        'defaultWarehouse' => normalizeDefaultWarehouse($_POST['defaultWarehouse'] ?? null, sanitizeWarehouseIds($_POST['allowedWarehouses'] ?? [])),
        'eans'             => sanitizeEans($_POST['eans'] ?? []),
        'decompSub'        => filter_var($_POST['decompSub'] ?? false, FILTER_VALIDATE_BOOLEAN),
        'sizeType'         => trim($_POST['sizeType'] ?? 'none'),
        'sizeNorm'         => trim($_POST['sizeNorm'] ?? 'eu'),
        'size'             => ($_POST['size'] ?? '') !== '' ? trim($_POST['size']) : null,
        'parentId'         => ($_POST['parentId'] ?? '') !== '' ? (int)$_POST['parentId'] : null,
        'isVariantParent'  => normalizeBool($_POST['isVariantParent'] ?? false),
        'variantFamily'    => trim((string)($_POST['variantFamily'] ?? '')),
        'variantAttributes'=> sanitizeVariantAttributes($_POST['variantAttributes'] ?? []),
        'variantLabel'     => trim((string)($_POST['variantLabel'] ?? '')),
        'variantSort'      => intval($_POST['variantSort'] ?? 0),
        'version'          => 1,
    ]);
    
    ensureProductStockAssignment((int)($item['id'] ?? 0), isset($item['defaultWarehouse']) ? (int)$item['defaultWarehouse'] : null);
    recordProductVersion((int)($item['id'] ?? 0), 1, 'create', [], $item);
    jsonResponse($item, 201);
}

// ── IMAGE EDIT (multipart/form-data) ──
// Called by Api.prodEditImg() with ?mode=editImage
if($method === 'POST' && isset($_FILES['image']) && (($_GET['mode'] ?? '') === 'editImage')){
    $id = $_POST['id'] ?? null;
    if(!$id) jsonError('ID manquant.');

    $products = loadData('products');
    $p = findById($products, $id);
    if(!$p) jsonError('Non trouvé.', 404);

    $imgUrl = $p['image'] ?? '';
    if(($_FILES['image']['error'] ?? 1) === UPLOAD_ERR_OK){
        // Validate real MIME type
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $realMime = finfo_file($finfo, $_FILES['image']['tmp_name']);
        finfo_close($finfo);
        $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
        $allowedExts  = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
        $ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
        if(in_array($realMime, $allowedMimes) && in_array($ext, $allowedExts)){
            $fn = uniqid('img_').'.'.$ext;
            if(move_uploaded_file($_FILES['image']['tmp_name'], UPLOADS_DIR.'/'.$fn)){
                // Remove previous file if it was a local upload.
                if(!empty($imgUrl)){
                    $oldPath = APP_ROOT.'/'.$imgUrl;
                    if(file_exists($oldPath)) @unlink($oldPath);
                }
                $imgUrl = 'uploads/'.$fn;
            }
        } else {
            jsonError('Type de fichier non autorisé. Utilisez JPG, PNG, GIF ou WEBP.', 400);
        }
    }

    $fields = ['image' => $imgUrl];
    $before = $p; // capture state before update
    $nextVersion = (int)($p['version'] ?? 1) + 1;
    $fields['version'] = $nextVersion;
    $up = updateItem('products', $id, $fields);
    if($up){
        ensureProductStockAssignment((int)$id, isset($up['defaultWarehouse']) ? (int)$up['defaultWarehouse'] : null);
        recordProductVersion((int)$id, $nextVersion, 'update', $before, $up);
        jsonResponse($up);
    }
    jsonError('Non trouvé.', 404);
}

$body = json_decode(file_get_contents('php://input'), true) ?? [];

switch($method){
case 'GET':
    $prods = loadData('products');
    $q     = strtolower($_GET['q'] ?? '');
    $cat   = $_GET['category'] ?? '';
    
    if($cat) $prods = array_filter($prods, fn($p) => ($p['category']??'') === $cat);
    if($q)   $prods = array_filter($prods, fn($p) =>
        str_contains(strtolower($p['name']??''), $q) ||
        str_contains(strtolower($p['sku']??''), $q) ||
        str_contains(strtolower($p['barcode']??''), $q ) ||
        (isset($p['eans']) && is_array($p['eans']) && in_array($q, array_map(fn($x)=>strtolower((string)$x), $p['eans']), true)) ||
        str_contains(strtolower($p['brand']??''), $q)
    );
    jsonResponse(array_values($prods));

case 'POST':
    $name = trim($body['name'] ?? '');
    if($name === '') jsonError('Nom obligatoire.');
    $sku  = trim($body['sku'] ?? '') ?: nextSKU();
    foreach(loadData('products') as $p) if(($p['sku']??'') === $sku) jsonError('SKU déjà utilisé.');
    
    $item = addItem('products', [
        'name'             => $name,
        'sku'              => $sku,
        'barcode'          => trim($body['barcode'] ?? ''),
        'brand'            => trim($body['brand'] ?? ''),
        'category'         => trim($body['category'] ?? ''),
        'description'      => trim($body['description'] ?? ''),
        'cost'             => floatval($body['cost'] ?? 0),
        'supplier'         => trim($body['supplier'] ?? ''),
        'supplierLinks'    => sanitizeSupplierLinks($body['supplierLinks'] ?? []),
        'allowedWarehouses' => sanitizeWarehouseIds($body['allowedWarehouses'] ?? []),
        'picking'          => trim($body['picking'] ?? ''),
        'location'         => trim($body['location'] ?? ''),
        'rack'             => trim($body['rack'] ?? ''),
        'shelf'            => trim($body['shelf'] ?? ''),
        'datasheetUrl'     => trim($body['datasheetUrl'] ?? ''),
        'msdsUrl'          => trim($body['msdsUrl'] ?? ''),
        // Packaging (legacy kept + new fields)
        'unitType'         => trim($body['unitType'] ?? 'piece'),
        'unitsPerBox'      => intval($body['unitsPerBox'] ?? 0),
        'boxesPerPallet'   => intval($body['boxesPerPallet'] ?? 0),
        'volume'           => floatval($body['volume'] ?? 0),
        'packBaseUnit'     => trim($body['packBaseUnit'] ?? ''),
        'packCartonQty'    => intval($body['packCartonQty'] ?? 0),
        'packPaletteQty'   => intval($body['packPaletteQty'] ?? 0),
        'packBidonQty'     => floatval($body['packBidonQty'] ?? 0),
        'packFutQty'       => floatval($body['packFutQty'] ?? 0),
        'packAerosolQty'   => floatval($body['packAerosolQty'] ?? 0),
        'packSacQty'       => floatval($body['packSacQty'] ?? 0),
        'packSeauQty'      => floatval($body['packSeauQty'] ?? 0),
        'packRouleauQty'   => floatval($body['packRouleauQty'] ?? 0),
        'packBobineQty'    => floatval($body['packBobineQty'] ?? 0),
        'packTubeQty'      => floatval($body['packTubeQty'] ?? 0),
        'packBoiteQty'     => floatval($body['packBoiteQty'] ?? 0),
        'packPaquetQty'    => floatval($body['packPaquetQty'] ?? 0),
        'costByUnit'       => is_array($body['costByUnit'] ?? null) ? ($body['costByUnit'] ?? []) : [],
        'trackLot'         => (bool)($body['trackLot'] ?? false),
        'trackExpiry'      => (bool)($body['trackExpiry'] ?? false),
        'shelfLifeDays'    => ($body['shelfLifeDays'] ?? null) !== null && ($body['shelfLifeDays'] ?? '') !== '' ? intval($body['shelfLifeDays']) : null,
        'safetyStock'      => ($body['safetyStock'] ?? null) !== null && ($body['safetyStock'] ?? '') !== '' ? intval($body['safetyStock']) : null,
        'cump'             => floatval($body['cump'] ?? floatval($body['cost'] ?? 0)),
        'image'            => $body['image'] ?? '',
        'defaultWarehouse' => normalizeDefaultWarehouse($body['defaultWarehouse'] ?? null, sanitizeWarehouseIds($body['allowedWarehouses'] ?? [])),
        'eans'             => sanitizeEans($body['eans'] ?? []),
        'decompSub'        => (bool)($body['decompSub'] ?? false),
        'sizeType'         => trim((string)($body['sizeType'] ?? 'none')),
        'sizeNorm'         => trim((string)($body['sizeNorm'] ?? 'eu')),
        'size'             => isset($body['size']) && $body['size']!=='' ? trim((string)$body['size']) : null,
        'parentId'         => isset($body['parentId']) && $body['parentId'] ? (int)$body['parentId'] : null,
        'isVariantParent'  => normalizeBool($body['isVariantParent'] ?? false),
        'variantFamily'    => trim((string)($body['variantFamily'] ?? '')),
        'variantAttributes'=> sanitizeVariantAttributes($body['variantAttributes'] ?? []),
        'variantLabel'     => trim((string)($body['variantLabel'] ?? '')),
        'variantSort'      => intval($body['variantSort'] ?? 0),
        'version'          => 1,
    ]);

    // Versioning trace (Lot 4)
    recordProductVersion((int)($item['id'] ?? 0), 1, 'create', [], $item);
    
    ensureProductStockAssignment((int)($item['id'] ?? 0), isset($item['defaultWarehouse']) ? (int)$item['defaultWarehouse'] : null);
    jsonResponse($item, 201);

case 'PUT':
    $id = $body['id'] ?? null;
    if(!$id) jsonError('ID manquant.');
    $fields = [];
    foreach([
        'name','sku','barcode','brand','category','description','cost','supplier','supplierLinks','picking','location','rack','shelf','datasheetUrl','msdsUrl',
        'unitType','unitsPerBox','boxesPerPallet','volume',
        'packBaseUnit','packCartonQty','packPaletteQty','packBidonQty','packFutQty',
        'packAerosolQty','packSacQty','packSeauQty','packRouleauQty','packBobineQty','packTubeQty','packBoiteQty','packPaquetQty',
        'costByUnit','trackLot','trackExpiry','shelfLifeDays','safetyStock','cump',
        'image','defaultWarehouse','eans','decompSub','sizeType','sizeNorm','size','parentId',
        'isVariantParent','variantFamily','variantAttributes','variantLabel','variantSort'
    ] as $k){
        if(array_key_exists($k, $body)) $fields[$k] = $body[$k];
    }
    
    $before = findById(loadData('products'), $id);
    if(!$before) jsonError('Non trouvé.', 404);

    // Normaliser magasins autorisés et magasin principal après lecture de l'ancien produit.
    if(array_key_exists('allowedWarehouses', $body)) {
        $fields['allowedWarehouses'] = sanitizeWarehouseIds($body['allowedWarehouses']);
    }
    if(array_key_exists('defaultWarehouse', $body)) {
        $allowedForDefault = array_key_exists('allowedWarehouses', $fields) ? $fields['allowedWarehouses'] : sanitizeWarehouseIds($before['allowedWarehouses'] ?? []);
        $fields['defaultWarehouse'] = normalizeDefaultWarehouse($body['defaultWarehouse'], $allowedForDefault);
        if($fields['defaultWarehouse'] && !in_array((int)$fields['defaultWarehouse'], $allowedForDefault, true)) {
            $allowedForDefault[] = (int)$fields['defaultWarehouse'];
            $fields['allowedWarehouses'] = $allowedForDefault;
        }
    } elseif(array_key_exists('allowedWarehouses', $fields)) {
        $fields['defaultWarehouse'] = normalizeDefaultWarehouse($before['defaultWarehouse'] ?? null, $fields['allowedWarehouses']);
    }

    // Sanitize EANs if present
    if(array_key_exists('eans',$fields)) $fields['eans'] = sanitizeEans($fields['eans']);
    if(array_key_exists('supplierLinks',$fields)) $fields['supplierLinks'] = sanitizeSupplierLinks($fields['supplierLinks']);
    if(array_key_exists('variantAttributes',$fields)) $fields['variantAttributes'] = sanitizeVariantAttributes($fields['variantAttributes']);
    if(array_key_exists('isVariantParent',$fields)) $fields['isVariantParent'] = normalizeBool($fields['isVariantParent']);

    // Increment product version (Lot 4)
    $nextVersion = (int)($before['version'] ?? 1) + 1;
    $fields['version'] = $nextVersion;

    if(isset($fields['sku'])){
        foreach(loadData('products') as $p){
            if((string)($p['id']??'') !== (string)$id && ($p['sku']??'') === $fields['sku']){
                jsonError('SKU déjà utilisé.');
            }
        }
    }
    $up = updateItem('products', $id, $fields);
    if($up){
        ensureProductStockAssignment((int)$id, isset($up['defaultWarehouse']) ? (int)$up['defaultWarehouse'] : null);
        recordProductVersion((int)$id, $nextVersion, 'update', $before, $up);
        jsonResponse($up);
    }
    jsonError('Non trouvé.', 404);

case 'DELETE':
    $id = $body['id'] ?? $_GET['id'] ?? null;
    if(!$id) jsonError('ID manquant.');
    $p = findById(loadData('products'), $id);
    if($p && !empty($p['image'])){
        $path = APP_ROOT.'/'.$p['image'];
        if(file_exists($path)) unlink($path);
    }
    $s = array_filter(loadData('stock'), fn($x) => (string)($x['productId']??'') !== (string)$id);
    saveData('stock', array_values($s));
    $a = array_filter(loadData('alerts'), fn($x) => (string)($x['productId']??'') !== (string)$id);
    saveData('alerts', array_values($a));
    deleteItem('products', $id);
    jsonResponse(['ok' => true]);

default:
    jsonError('Méthode non supportée.', 405);
}
?>
