<?php
/**
 * Fichier  : api/movements.php
 * Auteur   : KREMER Régis
 * Version  : 2.3.0
 * Date     : 2026-07-22
 * Description : API REST — CRUD des mouvements de stock (entrée, sortie, transfert, inventaire)
 *
 * 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 + emplacements internes (locationId).
 *                      import CSV, historique prix, bon de livraison multi-lignes
 */
require_once dirname(__DIR__).'/config.php';
require_once dirname(__DIR__).'/includes/push-helper.php';
startSession(); requireAuth();


$method=$_SERVER['REQUEST_METHOD'];
$body=json_decode(file_get_contents('php://input'),true)??[];
$user=currentUser();
switch($method){
case 'GET':
    $mvs=loadData('movements');
    $allowedWh = getUserAllowedWarehouses();
    if($allowedWh !== null) $mvs = array_values(array_filter($mvs, fn($m)=>in_array((int)($m['warehouseId']??0),$allowedWh)));
    $q=strtolower($_GET['q']??'');$type=$_GET['type']??'';$wid=$_GET['warehouseId']??'';
    $from=$_GET['dateFrom']??'';$to=$_GET['dateTo']??'';
    if($type) $mvs=array_filter($mvs,fn($m)=>($m['type']??'')===$type);
    if($wid)  $mvs=array_filter($mvs,fn($m)=>(string)($m['warehouseId']??'')===$wid);
    if($from) $mvs=array_filter($mvs,fn($m)=>($m['date']??'')>=$from);
    if($to)   $mvs=array_filter($mvs,fn($m)=>($m['date']??'')<=$to.'T23:59:59');
    if($q){
        $prods=loadData('products');
        $mvs=array_filter($mvs,function($m) use($q,$prods){
            $p=findById($prods,$m['productId']??0);
            return str_contains(strtolower($p['name']??''),$q)||str_contains(strtolower($p['sku']??''),$q)
                ||str_contains(strtolower($m['reason']??''),$q)||str_contains(strtolower($m['site']??''),$q)
                ||str_contains(strtolower($m['operator']??''),$q)||str_contains(strtolower($m['note']??''),$q);
        });
    }
    if(!hasAdminAccess($user)) $mvs=array_filter($mvs,fn($m)=>(string)($m['userId']??'')===(string)($user['id']??''));
    $mvs=array_values($mvs);
    usort($mvs,fn($a,$b)=>strcmp($b['date']??'',$a['date']??''));
    jsonResponse($mvs);
case 'POST':
    csrfCheck();
    requirePerm('movements.create');
    
    $pid=(int)($body['productId']??0);
    $wid=(int)($body['warehouseId']??0);
    $type=$body['type']??'';
    $qty=(float)($body['quantity']??0);
    $unit=normalizeUnit((string)($body['unit']??'piece'));
    $lotNumber=trim((string)($body['lotNumber']??''));
    $expiryDate=trim((string)($body['expiryDate']??''));
    $unitCost=(float)($body['unitCost']??0);
    $locationId = $body['locationId'] ?? '';
    $locationToId = $body['locationToId'] ?? '';

    $reason=trim((string)($body['reason']??''));
    $site=trim((string)($body['site']??''));
    $operator=trim((string)($body['operator']??''));
    $note=trim((string)($body['note']??''));
    $transferTo=(int)($body['transferTo']??0);
    
    // NEW V2.1: Entry-specific fields
    $deliveryNote=trim((string)($body['deliveryNote']??''));
    $supplier=trim((string)($body['supplier']??''));

    if(!$pid||!$wid||$qty<=0) jsonError('productId, warehouseId, quantity>0 requis.');
    if(!userCanAccessWarehouse($wid)) jsonError('Accès refusé à ce magasin.', 403);
    
    // NEW V2.1: Support "entry" type
    if(!in_array($type,['add','remove','transfer','entry'])) jsonError('Type invalide.');
    
    if($type==='transfer'&&!$transferTo) jsonError('transferTo requis.');
    
    // NEW V2.1: Entry requires admin
    if($type==='entry' && !hasAdminAccess($user)) {
        jsonError('Seul un administrateur peut faire une entrée de stock (réception commande).');
    }
    
    // MODIFIED V2.1: Non-admin can do "add" but not other types
    if(!hasAdminAccess($user) && !in_array($type, ['remove', 'add'])) {
        jsonError('Vous ne pouvez effectuer que des sorties ou remises en stock.');
    }
    
    if($operator==='') $operator=$user['name']??'Inconnu';

    $products = loadData('products');
    $prod = findById($products, $pid);
    if(!$prod) jsonError('Produit introuvable.',404);

    $baseQty = toBaseQuantity($prod, $unit, $qty);
    if($baseQty<=0) jsonError('Quantité invalide.');
    $baseUnitCost = toBaseUnitCost($prod, $unit, $unitCost);
    
    // NEW V2.1: Check stock limit for non-admin "add"
    if($type === 'add' && !hasAdminAccess($user)) {
        $check = checkStockLimit($pid, $wid, $baseQty);
        if(!$check['ok']) {
            jsonError($check['message'], 400);
        }
    }

    // ── BLOCAGE RÉSERVATION : sortie/transfert vs stock disponible ──
    // Le stock disponible = stock physique − réservations actives.
    // Toute sortie (remove/transfer) qui empiète sur les quantités réservées est refusée.
    if(in_array($type, ['remove', 'transfer'])){
        $avail = getAvailableStock($pid, $wid);
        if($baseQty > $avail['available']){
            $msg = 'Stock disponible insuffisant.'
                 . ' Stock physique : ' . (int)$avail['physical'] . ' u.'
                 . ', Réservé : ' . (int)$avail['reserved'] . ' u.'
                 . ', Disponible : ' . (int)$avail['available'] . ' u.'
                 . ', Demandé : ' . (int)round($baseQty) . ' u.';
            jsonError($msg, 400);
        }
    }

    // Enforce lot/expiry rules per product configuration (Lot 2)
    $needsLot = (bool)($prod['trackLot'] ?? false);
    $needsExpiry = (bool)($prod['trackExpiry'] ?? false);

    // Inbound must declare lot/expiry when required.
    if(in_array($type, ['add','entry'])){
        if($needsLot && $lotNumber==='') jsonError('Numéro de lot obligatoire pour ce produit.');
        if($needsExpiry && $expiryDate==='') jsonError('Date de péremption / DLUO obligatoire pour ce produit.');
    }
    // Outbound can auto-allocate lots (FEFO/FIFO) if no lot is specified.

    // Lot lines (multi-lot compatible; UI may still send single lot)
    $lotLines = [];
    if(isset($body['lots']) && is_array($body['lots']) && count($body['lots'])>0){
        foreach($body['lots'] as $ln){
            if(!is_array($ln)) continue;
            $lnLot = trim((string)($ln['lotNumber'] ?? ''));
            $lnExp = trim((string)($ln['expiryDate'] ?? ''));
            $lnQty = (float)($ln['quantity'] ?? 0);
            $lnUnit = trim((string)($ln['unit'] ?? $unit));
            $lnUnitCost = (float)($ln['unitCost'] ?? $unitCost);

            if($lnQty<=0) continue;
            if($needsLot && $lnLot==='') jsonError('Numéro de lot obligatoire pour ce produit.');
            if($needsExpiry && $lnExp==='') jsonError('Date de péremption / DLUO obligatoire pour ce produit.');

            $lnBaseQty = toBaseQuantity($prod, $lnUnit, $lnQty);
            if($lnBaseQty<=0) jsonError('Quantité invalide (ligne lot).');

            $lnBaseUnitCost = $lnUnitCost>0 ? toBaseUnitCost($prod, $lnUnit, $lnUnitCost) : 0;

            $lotLines[] = [
                'lotNumber'=>$lnLot,
                'expiryDate'=>$lnExp,
                'quantityBase'=>(int)round($lnBaseQty),
                'baseUnitCost'=>$lnBaseUnitCost,
                'unit'=>$lnUnit,
                'quantity'=>$lnQty,
                'unitCost'=>$lnUnitCost
            ];
        }
        if(empty($lotLines)) jsonError('Aucune ligne lot valide.');
        // Overwrite baseQty with sum
        $baseQty = 0;
        foreach($lotLines as $ln){ $baseQty += (float)($ln['quantityBase'] ?? 0); }
        if($baseQty<=0) jsonError('Quantité invalide.');
    } else if($lotNumber !== '' || $expiryDate !== ''){
        // Single line lot
        $lotLines[] = [
            'lotNumber'=>$lotNumber,
            'expiryDate'=>$expiryDate,
            'quantityBase'=>(int)round($baseQty),
            'baseUnitCost'=>$baseUnitCost,
            'unit'=>$unit,
            'quantity'=>$qty,
            'unitCost'=>$unitCost
        ];
    }

    // CUMP: capture on-hand quantity before applying inbound movement (Lot 2)
    $qtyBefore = getTotalStockQty($pid);

    // Stock update (lot-aware)
    $stock = loadData('stock');
    $findOrCreate=function($p,$w) use(&$stock){
        foreach($stock as $i=>$s){
            if((string)($s['productId']??'')===(string)$p && (string)($s['warehouseId']??'')===(string)$w) return $i;
        }
        $stock[]=[
            'id'=>genId(),
            'productId'=>$p,
            'warehouseId'=>$w,
            'quantity'=>0,
            'maxAuthorized'=>0,
            'minStock'=>null,
            'maxStock'=>null,
            'safetyStock'=>null,
            'lots'=>[],
            'locations'=>['default'=>['quantity'=>0,'lots'=>[]]],
            'lastMovementAt'=>null,
            'createdAt'=>date('c')
        ];
        return count($stock)-1;
    };

    $srcI = $findOrCreate($pid,$wid);
    $dstI = null;
    if($type==='transfer') $dstI = $findOrCreate($pid,$transferTo);

    $srcLocKey = getLocationKey($locationId);
    $dstLocKey = getLocationKey($locationToId);

    // ── Rétrocompatibilité : garantir que le bucket 'default' reflète la vraie quantité ──
    // Les stocks créés avant les emplacements ont locations={} ou []. On corrige inline, sans closure.
    foreach(array_filter([$srcI, $dstI], fn($i) => $i !== null) as $_idx) {
        if(!isset($stock[$_idx]['locations']) || !is_array($stock[$_idx]['locations'])) {
            $stock[$_idx]['locations'] = [];
        }
        $entryQty = (int)($stock[$_idx]['quantity'] ?? 0);
        $totalBuckets = 0;
        foreach($stock[$_idx]['locations'] as $_loc) {
            $totalBuckets += (int)(is_array($_loc) ? ($_loc['quantity'] ?? 0) : 0);
        }
        if($totalBuckets < $entryQty && $entryQty > 0) {
            $missing = $entryQty - $totalBuckets;
            if(!isset($stock[$_idx]['locations']['default']) || !is_array($stock[$_idx]['locations']['default'])) {
                $stock[$_idx]['locations']['default'] = ['quantity' => 0, 'lots' => []];
            }
            // Fusionner les lots top-level dans le bucket default
            $topLots = is_array($stock[$_idx]['lots'] ?? null) ? $stock[$_idx]['lots'] : [];
            $existKeys = [];
            foreach(($stock[$_idx]['locations']['default']['lots'] ?? []) as $_l) {
                $existKeys[] = ($_l['lotNumber']??'').'|'.($_l['expiryDate']??'');
            }
            foreach($topLots as $_tl) {
                $k = ($_tl['lotNumber']??'').'|'.($_tl['expiryDate']??'');
                if(!in_array($k, $existKeys)) {
                    $stock[$_idx]['locations']['default']['lots'][] = $_tl;
                    $existKeys[] = $k;
                }
            }
            $stock[$_idx]['locations']['default']['quantity'] = (int)($stock[$_idx]['locations']['default']['quantity'] ?? 0) + $missing;
        }
    }

    $now = date('c');
    $stock[$srcI]['lastMovementAt'] = $now;
    if($dstI!==null) $stock[$dstI]['lastMovementAt'] = $now;

    // Helpers to manipulate lots
    $addToLot=function(&$entry, string $locKey, string $lot, string $exp, float $qtyBase, float $baseUnitCost) use($now){
        if(!isset($entry['lots']) || !is_array($entry['lots'])) $entry['lots']=[];
        $found=false;
        foreach($entry['lots'] as &$l){
            if(($l['lotNumber']??'')===$lot && ($l['expiryDate']??'')===$exp){
                $l['quantity']=(int)($l['quantity']??0) + (int)round($qtyBase);
                $l['lastMovementAt']=$now;
                if($baseUnitCost>0) $l['unitCost']=$baseUnitCost;
                $found=true;break;
            }
        }
        unset($l);
        if(!$found){
            $entry['lots'][]=[
                'lotNumber'=>$lot,
                'expiryDate'=>$exp,
                'quantity'=>(int)round($qtyBase),
                'unitCost'=>$baseUnitCost>0?$baseUnitCost:null,
                'receivedAt'=>$now,
                'lastMovementAt'=>$now
            ];
        }
    
        // Lot 1: update internal location bucket
        $bucket =& getStockLocationBucket($entry, $locKey);
        if(!isset($bucket['lots']) || !is_array($bucket['lots'])) $bucket['lots'] = [];
        $found2=false;
        foreach($bucket['lots'] as &$bl){
            if(($bl['lotNumber']??'')===$lot && ($bl['expiryDate']??'')===$exp){
                $bl['quantity']=(int)($bl['quantity']??0) + (int)round($qtyBase);
                $bl['lastMovementAt']=$now;
                if($baseUnitCost>0) $bl['unitCost']=$baseUnitCost;
                $found2=true;break;
            }
        }
        unset($bl);
        if(!$found2){
            $bucket['lots'][]=[
                'lotNumber'=>$lot,
                'expiryDate'=>$exp,
                'quantity'=>(int)round($qtyBase),
                'unitCost'=>$baseUnitCost>0?$baseUnitCost:null,
                'receivedAt'=>$now,
                'lastMovementAt'=>$now
            ];
        }
        $bucket['quantity'] = (int)($bucket['quantity'] ?? 0) + (int)round($qtyBase);

};

    $removeFromLot=function(&$entry, string $locKey, string $lot, string $exp, float $qtyBase) use($needsLot){
        $need = (int)round($qtyBase);
        if($need<=0) return;

        $removeFromBucket = function(array &$bucket) use($needsLot, $lot, $exp, $need){
            if(!isset($bucket['lots']) || !is_array($bucket['lots'])) $bucket['lots']=[];
            $qty = (int)($bucket['quantity'] ?? 0);

            // If there are no lots, fallback to quantity only
            if(count($bucket['lots'])===0){
                if($qty < $need) jsonError("Stock insuffisant ({$qty} u.).");
                $bucket['quantity'] = $qty - $need;
                return;
            }

            if($needsLot){
                foreach($bucket['lots'] as $i=>$l){
                    if(($l['lotNumber']??'')===$lot && ($exp==='' || ($l['expiryDate']??'')===$exp)){
                        if((bool)($l['blocked'] ?? false)) jsonError('Lot bloqué: '.$lot.' '.$exp);

                        $have=(int)($l['quantity']??0);
                        if($have < $need) jsonError("Stock insuffisant sur le lot ({$have} u.).");
                        $bucket['lots'][$i]['quantity']=$have-$need;
                        $bucket['quantity']=$qty-$need;
                        return;
                    }
                }
                jsonError("Lot introuvable ou stock insuffisant.");
            } else {
                // FEFO/FIFO fallback: skip blocked lots; remove from earliest expiry first
                $bucket['lots'] = array_values(array_filter($bucket['lots'], fn($x)=>!((bool)($x['blocked'] ?? false))));
                usort($bucket['lots'], function($a,$b){
                    $ea=(string)($a['expiryDate']??'');
                    $eb=(string)($b['expiryDate']??'');
                    if($ea===$eb) return 0;
                    if($ea==='') return 1;
                    if($eb==='') return -1;
                    return $ea < $eb ? -1 : 1;
                });
                $remain = $need;
                foreach($bucket['lots'] as $i=>$l){
                    $have=(int)($l['quantity']??0);
                    if($have<=0) continue;
                    $take = min($have, $remain);
                    $bucket['lots'][$i]['quantity']=$have-$take;
                    $remain-=$take;
                    if($remain<=0) break;
                }
                if($remain>0) jsonError("Stock insuffisant.");
                $bucket['quantity']=$qty-$need;
                return;
            }
        };

        // 1) Remove from internal location bucket
        $bucket =& getStockLocationBucket($entry, $locKey);
        $removeFromBucket($bucket);

        // 2) Keep backward compatibility: remove also from aggregated entry (top-level)
        // Rebuild aggregated view from locations
        $total = 0;
        $locLots = [];
        foreach(($entry['locations'] ?? []) as $loc){
            $total += (int)($loc['quantity'] ?? 0);
            $locLots[] = $loc['lots'] ?? [];
        }
        $entry['quantity'] = $total;
        $entry['lots'] = mergeLots($locLots);
    };


    // Apply stock move
    $allocationStrategy = 'MANUAL';
    $outCostTotal = 0.0;
    $outCostLines = [];
    $inCostTotal = 0.0;

    // Helper: get lot unitCost from bucket (or 0)
    $getLotUnitCost = function(array $bucket, string $lot, string $exp): float {
        if(!isset($bucket['lots']) || !is_array($bucket['lots'])) return 0.0;
        foreach($bucket['lots'] as $l){
            if((string)($l['lotNumber']??'')===(string)$lot && ((string)$exp==='' || (string)($l['expiryDate']??'')===(string)$exp)){
                return (float)($l['unitCost'] ?? 0);
            }
        }
        return 0.0;
    };

    $prodFallbackCost = (float)($prod['cump'] ?? ($prod['cost'] ?? 0));

    if($type==='add' || $type==='entry'){
        // Inbound: apply each line (or no-lot for non-tracked products)
        if(!empty($lotLines)){
            foreach($lotLines as $ln){
                $q = (float)($ln['quantityBase'] ?? 0);
                if($q<=0) continue;
                $c = (float)($ln['baseUnitCost'] ?? $baseUnitCost);
                $inCostTotal += ($c>0 ? $c*$q : 0);
                $addToLot($stock[$srcI], $srcLocKey, (string)$ln['lotNumber'], (string)$ln['expiryDate'], $q, $c);
            }
        } else {
            $inCostTotal = $baseUnitCost>0 ? ($baseUnitCost*$baseQty) : 0;
            $addToLot($stock[$srcI], $srcLocKey, $lotNumber, $expiryDate, $baseQty, $baseUnitCost);
        }
    } else if($type==='remove' || $type==='transfer'){
        // Outbound: auto-allocate if tracked and no lot provided
        if(empty($lotLines) && ($needsLot || $needsExpiry)){
            $allocationStrategy = $needsExpiry ? 'FEFO' : 'FIFO';
            $need = (int)round($baseQty);
            $alloc = allocateLots($stock[$srcI], $srcLocKey, $need, $allocationStrategy);
            $sum = 0;
            foreach($alloc as $a){ $sum += (int)($a['quantity'] ?? 0); }
            if($sum < $need) jsonError("Stock insuffisant (lots non bloqués) pour allocation {$allocationStrategy}.");
            $lotLines = [];
            foreach($alloc as $a){
                $lotLines[] = [
                    'lotNumber'=>(string)($a['lotNumber'] ?? ''),
                    'expiryDate'=>(string)($a['expiryDate'] ?? ''),
                    'quantityBase'=>(int)($a['quantity'] ?? 0),
                    'baseUnitCost'=>(float)($a['unitCost'] ?? 0),
                    'locationKey'=>(string)($a['locationKey'] ?? $srcLocKey),
                ];
            }
        }

        // Remove per lot line when we have them; else fallback (non-tracked products)
        if(!empty($lotLines)){
            foreach($lotLines as $ln){
                $q = (float)($ln['quantityBase'] ?? 0);
                if($q<=0) continue;
                $lot = (string)($ln['lotNumber'] ?? '');
                $exp = (string)($ln['expiryDate'] ?? '');
                $loc = (string)($ln['locationKey'] ?? $srcLocKey);

                // Block check + cost lookup from bucket
                $bucket =& getStockLocationBucket($stock[$srcI], getLocationKey($loc));
                $uCost = $getLotUnitCost($bucket, $lot, $exp);
                // if lot exists and is blocked => refuse
                if(isset($bucket['lots']) && is_array($bucket['lots'])){
                    foreach($bucket['lots'] as $l){
                        if((string)($l['lotNumber']??'')===$lot && ((string)$exp==='' || (string)($l['expiryDate']??'')===$exp)){
                            if((bool)($l['blocked'] ?? false)) jsonError("Lot bloqué: {$lot} {$exp}");
                        }
                    }
                }

                $removeFromLot($stock[$srcI], getLocationKey($loc), $lot, $exp, $q);

                $use = ($uCost>0) ? $uCost : $prodFallbackCost;
                $outCostTotal += $use * $q;
                $outCostLines[] = ['lotNumber'=>$lot,'expiryDate'=>$exp,'quantity'=>$q,'baseUnitCost'=>$use];
            }
        } else {
            // No lots (non tracked): remove from quantity only
            $removeFromLot($stock[$srcI], $srcLocKey, $lotNumber, $expiryDate, $baseQty);
            $use = $prodFallbackCost;
            $outCostTotal = $use * (float)$baseQty;
        }

        if($type==='transfer'){
            // Add to destination using same lotLines (if present) with unitCost propagated
            if(!empty($lotLines)){
                foreach($lotLines as $ln){
                    $q = (float)($ln['quantityBase'] ?? 0);
                    if($q<=0) continue;
                    $c = (float)($ln['baseUnitCost'] ?? 0);
                    $addToLot($stock[$dstI], $dstLocKey, (string)$ln['lotNumber'], (string)$ln['expiryDate'], $q, $c);
                }
            } else {
                $addToLot($stock[$dstI], $dstLocKey, $lotNumber, $expiryDate, $baseQty, $baseUnitCost);
            }
        }
    }

// Recompute legacy quantity fields as sum(lots)
    foreach([$srcI,$dstI] as $idx){
        if($idx===null) continue;
        $sum = 0;
        if(isset($stock[$idx]['lots']) && is_array($stock[$idx]['lots'])){
            foreach($stock[$idx]['lots'] as $l){ $sum += (int)($l['quantity']??0); }
        }
        $stock[$idx]['quantity']=$sum;
    }

    saveData('stock',$stock);

    // Update purchase price history + CUMP on inbound (Lot 2)
    if(in_array($type, ['add','entry'])){
        $purchase = loadData('purchase_prices');

        // Record each lot line when available (keeps traceability multi-lot)
        if(!empty($lotLines)){
            foreach($lotLines as $ln){
                $qBase = (float)($ln['quantityBase'] ?? 0);
                $cBase = (float)($ln['baseUnitCost'] ?? 0);
                if($qBase<=0 || $cBase<=0) continue;
                $purchase[]=[
                    'id'=>genId(),
                    'productId'=>$pid,
                    'unit'=>$ln['unit'] ?? $unit,
                    'quantity'=>$ln['quantity'] ?? $qty,
                    'baseQuantity'=>$qBase,
                    'unitCost'=>$ln['unitCost'] ?? $unitCost,
                    'baseUnitCost'=>$cBase,
                    'supplier'=>$type==='entry' ? ($supplier ?: ($prod['supplier']??'')) : ($prod['supplier']??''),
                    'type'=>$type,
                    'date'=>$now,
                    'userId'=>$user['id'],
                    'lotNumber'=>(string)($ln['lotNumber'] ?? ''),
                    'expiryDate'=>(string)($ln['expiryDate'] ?? '')
                ];
            }
        } else if($baseUnitCost>0){
            $purchase[]=[
                'id'=>genId(),
                'productId'=>$pid,
                'unit'=>$unit,
                'quantity'=>$qty,
                'baseQuantity'=>$baseQty,
                'unitCost'=>$unitCost,
                'baseUnitCost'=>$baseUnitCost,
                'costTotal'=>round($inCostTotal, 2),
                'supplier'=>$type==='entry' ? ($supplier ?: ($prod['supplier']??'')) : ($prod['supplier']??''),
                'type'=>$type,
                'date'=>$now,
                'userId'=>$user['id']
            ];
        }
        saveData('purchase_prices',$purchase);

        // Update CUMP using inbound total cost (if available)
        if($inCostTotal>0 && $baseQty>0){
            updateProductCUMP($pid, (float)$baseQty, (float)$inCostTotal, (float)$qtyBefore);

            // Align product.cost and costByUnit[base] with CUMP for valuation screens
            $products = loadData('products');
            foreach($products as &$pp){
                if((int)($pp['id']??0)===(int)$pid){
                    $base = normalizeUnit($pp['packBaseUnit'] ?? ($pp['unitType'] ?? 'piece'));
                    $pp['cost'] = (float)($pp['cump'] ?? ($pp['cost'] ?? 0));
                    if(!isset($pp['costByUnit']) || !is_array($pp['costByUnit'])) $pp['costByUnit']=[];
                    $pp['costByUnit'][$base] = (float)($pp['cump'] ?? ($pp['cost'] ?? 0));
                    $pp['updatedAt'] = date('c');
                    break;
                }
            }
            unset($pp);
            saveData('products',$products);
        }
    }


    // Persist movement with traceability fields
    $mvLots = [];
    if(!empty($lotLines)){
        foreach($lotLines as $ln){
            $mvLots[] = [
                'lotNumber'=>(string)($ln['lotNumber'] ?? ''),
                'expiryDate'=>(string)($ln['expiryDate'] ?? ''),
                'quantity'=>(int)($ln['quantityBase'] ?? ($ln['quantity'] ?? 0)),
                'baseUnitCost'=>(float)($ln['baseUnitCost'] ?? 0),
                'locationId'=>(string)($ln['locationKey'] ?? $srcLocKey),
            ];
        }
    }

    $legacyLot = '';
    $legacyExp = '';
    if(count($mvLots)===1){
        $legacyLot = (string)($mvLots[0]['lotNumber'] ?? '');
        $legacyExp = (string)($mvLots[0]['expiryDate'] ?? '');
    } else if(count($mvLots)>1){
        $legacyLot = 'MULTI';
        $legacyExp = '';
    } else {
        $legacyLot = $lotNumber;
        $legacyExp = $expiryDate;
    }
    $mv=[
        'productId'=>$pid,
        'warehouseId'=>$wid,
        'type'=>$type,
        'quantity'=>$qty,
        'unit'=>$unit,
        'baseQuantity'=>$baseQty,
        'locationId'=>$srcLocKey,
        'locationToId'=>($type==='transfer' ? $dstLocKey : null),
        'lotNumber'=>$legacyLot,
        'expiryDate'=>$legacyExp,
        'lots'=>$mvLots,
        'allocationStrategy'=>$allocationStrategy,
        'unitCost'=>$unitCost,
        'baseUnitCost'=>$baseUnitCost,
        'costTotal'=>round((in_array($type,['remove','transfer']) ? $outCostTotal : $inCostTotal), 2),
        'costLines'=> (in_array($type,['remove','transfer']) ? $outCostLines : null),
        'reason'=>$reason,
        'site'=>$site,
        'operator'=>$operator,
        'note'=>$note,
        'userId'=>$user['id'],
        'date'=>$now
    ];
    
    // NEW V2.1: Add entry-specific fields
    if($type === 'entry') {
        $mv['deliveryNote'] = $deliveryNote;
        $mv['supplier'] = $supplier;
    }
    
    if($type==='transfer') $mv['transferTo']=$transferTo;
    $savedMv = addItem('movements',$mv);
    
    // ══════════════════════════════════════════════════════════════
    // NOTIFICATIONS PUSH — Envoyer aux admins (sauf initiateur)
    // ══════════════════════════════════════════════════════════════
    $warehouses = loadData('warehouses');
    $wh = findById($warehouses, $wid);
    $whName = $wh ? $wh['name'] : 'Magasin inconnu';
    $prodName = $prod['name'] ?? 'Produit';
    $currentUserId = (int)($user['id'] ?? 0);
    
    if ($type === 'remove') {
        notifyStockOut($prodName, $whName, (int)round($baseQty), $currentUserId);
    } else if ($type === 'add' || $type === 'entry') {
        notifyStockIn($prodName, $whName, (int)round($baseQty), $currentUserId);
    } else if ($type === 'transfer') {
        $whTo = findById($warehouses, $transferTo);
        $whToName = $whTo ? $whTo['name'] : 'Autre magasin';
        sendPushToAdmins(
            'transfer',
            '🔄 Transfert de stock',
            (int)round($baseQty) . " × $prodName transféré de $whName vers $whToName",
            ['url' => appUrl('index.php?page=movements')],
            $currentUserId
        );
    }
    
    // Vérifier les alertes après le mouvement
    checkAndNotifyAlerts($pid, $wid);
    
    jsonResponse($savedMv, 201);
case 'DELETE':
    requireAdmin();
    requirePerm('movements.delete');
    clearData('movements');
    jsonResponse(['ok'=>true]);
default: jsonError('Méthode non supportée.',405);
}
?>
