<?php
/**
 * Fichier  : api/plans.php
 * Auteur   : KREMER Régis
 * Version  : 2.6.65
 * Date     : 2026-07-09
 * Description : API REST — Plans d’emplacements / schéma composants murs par points et ouvertures accrochables
 *
 * Changelog :
 *   v2.6.25 (2026-07-09) — Création du socle API plans/objets, permissions admin en écriture et lecture filtrée par magasin.
 *   v2.6.26 (2026-07-09) — PLAN 2 : sauvegarde groupée des objets depuis l’éditeur SVG de base.
 *   v2.6.27 (2026-07-09) — PLAN 3 : schéma composants enrichi pour bibliothèque visuelle et catégories métier.
 *   v2.6.28 (2026-07-09) — PLAN 4 : lecture emplacements existants, informations métier et rattachements contrôlés.
 *   v2.6.29 (2026-07-09) — PLAN 5 : localisation d’un produit vers le plan et l’objet liés à son emplacement.
 *   v2.6.40 (2026-07-09) — PLANS DESIGN1 : enrichissement des réponses avec le nom du magasin pour la consultation finie.
 *   v2.6.44 (2026-07-09) — PLANS ARCHITECTE1 : exposition des familles Construire, Ouvertures et Repères pour l’éditeur plan 2D.
 *   v2.6.45 (2026-07-09) — PLANS WALLS2 : schéma compatible murs/cloisons en segments et ouvertures accrochées via metadata.
 */
require_once dirname(__DIR__) . '/config.php';
startSession();
requireAuth();
ensurePlansStorage();

$method = $_SERVER['REQUEST_METHOD'];
$bodyRaw = file_get_contents('php://input');
$body = json_decode($bodyRaw ?: '[]', true);
if(!is_array($body)) $body = [];

if($method === 'GET') {
    requirePerm('plans.read');
} else {
    requireAdmin();
    requirePerm('plans.manage');
    csrfCheck();
}

function planCleanString($value, int $max = 160): string {
    $value = trim((string)$value);
    $value = str_replace(["\r", "\n", "\t"], ' ', $value);
    $value = preg_replace('/\s+/u', ' ', $value) ?: '';
    if(function_exists('mb_substr')) return mb_substr($value, 0, $max, 'UTF-8');
    return substr($value, 0, $max);
}

function planNumber($value, float $default = 0.0, float $min = -100000.0, float $max = 100000.0): float {
    if($value === null || $value === '') return $default;
    $n = is_numeric($value) ? (float)$value : $default;
    if($n < $min) $n = $min;
    if($n > $max) $n = $max;
    return $n;
}

function planColor($value, string $fallback): string {
    $value = trim((string)$value);
    if(preg_match('/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/', $value)) return $value;
    return $fallback;
}

function planWarehouseExists(int $warehouseId): bool {
    if($warehouseId <= 0) return false;
    foreach(loadData('warehouses') as $w){
        if((int)($w['id'] ?? 0) === $warehouseId) return true;
    }
    return false;
}

function planGetById($id): ?array {
    foreach(loadData('plans') as $p){
        if((string)($p['id'] ?? '') === (string)$id) return $p;
    }
    return null;
}

function planEnsureReadable(array $plan): void {
    $wid = (int)($plan['warehouseId'] ?? 0);
    if($wid > 0 && !userCanAccessWarehouse($wid)) jsonError('Accès refusé à ce magasin.', 403);
}

function planLocationById($id): ?array {
    if($id === null || $id === '' || (string)$id === '0') return null;
    foreach(loadData('locations') as $loc){
        if((string)($loc['id'] ?? '') === (string)$id) return $loc;
    }
    return null;
}


function planWarehouseMap(): array {
    $out = [];
    foreach(loadData('warehouses') as $w){
        $id = (string)($w['id'] ?? '');
        if($id !== '') $out[$id] = $w;
    }
    return $out;
}

function planWarehouseName($warehouseId): string {
    static $warehouses = null;
    if($warehouses === null) $warehouses = planWarehouseMap();
    $id = (string)($warehouseId ?? '');
    if($id === '' || !isset($warehouses[$id])) return '';
    return (string)($warehouses[$id]['name'] ?? '');
}

function planProductMap(): array {
    $out = [];
    foreach(loadData('products') as $p){
        $id = (string)($p['id'] ?? '');
        if($id !== '') $out[$id] = [
            'id' => $p['id'],
            'name' => $p['name'] ?? '',
            'sku' => $p['sku'] ?? '',
            'barcode' => $p['barcode'] ?? '',
            'category' => $p['category'] ?? '',
        ];
    }
    return $out;
}

function planLocationStockSummary($locationId, ?int $warehouseId = null, int $limit = 8): array {
    $products = planProductMap();
    $totalQty = 0.0;
    $entryCount = 0;
    $productIds = [];
    $items = [];

    foreach(loadData('stock') as $s){
        $sid = $s['locationId'] ?? null;
        if((string)$sid !== (string)$locationId) continue;
        if($warehouseId !== null && (int)($s['warehouseId'] ?? 0) !== $warehouseId) continue;
        if(!userCanAccessWarehouse((int)($s['warehouseId'] ?? 0))) continue;

        $qty = (float)($s['quantity'] ?? 0);
        $pid = (string)($s['productId'] ?? '');
        $entryCount++;
        $totalQty += $qty;
        if($pid !== '') $productIds[$pid] = true;

        if(count($items) < $limit){
            $prod = $products[$pid] ?? ['id'=>$pid, 'name'=>'Produit #'.$pid, 'sku'=>'', 'barcode'=>'', 'category'=>''];
            $items[] = [
                'productId' => $pid,
                'name' => $prod['name'] ?? ('Produit #'.$pid),
                'sku' => $prod['sku'] ?? '',
                'barcode' => $prod['barcode'] ?? '',
                'category' => $prod['category'] ?? '',
                'quantity' => $qty,
                'picking' => $s['picking'] ?? '',
                'rack' => $s['rack'] ?? '',
                'shelf' => $s['shelf'] ?? '',
            ];
        }
    }

    return [
        'stockEntries' => $entryCount,
        'productCount' => count($productIds),
        'totalQuantity' => $totalQty,
        'products' => $items,
    ];
}

function planLocationSummary(array $loc, bool $withStock = false): array {
    $warehouseId = (int)($loc['warehouseId'] ?? 0);
    if($warehouseId > 0 && !userCanAccessWarehouse($warehouseId)) jsonError('Accès refusé à cet emplacement.', 403);

    $out = [
        'id' => $loc['id'] ?? null,
        'warehouseId' => $warehouseId,
        'warehouseName' => planWarehouseName($warehouseId),
        'code' => $loc['code'] ?? '',
        'label' => $loc['label'] ?? '',
        'zone' => $loc['zone'] ?? '',
        'type' => $loc['type'] ?? '',
        'active' => array_key_exists('active', $loc) ? (bool)$loc['active'] : true,
        'createdAt' => $loc['createdAt'] ?? null,
        'updatedAt' => $loc['updatedAt'] ?? null,
    ];
    if($withStock){
        $out['stock'] = planLocationStockSummary($loc['id'] ?? '', $warehouseId);
    }
    return $out;
}

function planLocationList(int $warehouseId = 0, string $q = '', bool $withStock = false): array {
    if($warehouseId > 0 && !userCanAccessWarehouse($warehouseId)) jsonError('Accès refusé à ce magasin.', 403);
    $q = function_exists('mb_strtolower') ? mb_strtolower(trim($q), 'UTF-8') : strtolower(trim($q));
    $rows = [];
    foreach(loadData('locations') as $loc){
        if(!is_array($loc)) continue;
        $wid = (int)($loc['warehouseId'] ?? 0);
        if($wid > 0 && !userCanAccessWarehouse($wid)) continue;
        if($warehouseId > 0 && $wid !== $warehouseId) continue;
        if($q !== ''){
            $hay = trim(($loc['code'] ?? '').' '.($loc['label'] ?? '').' '.($loc['zone'] ?? '').' '.($loc['type'] ?? ''));
            $hay = function_exists('mb_strtolower') ? mb_strtolower($hay, 'UTF-8') : strtolower($hay);
            if(strpos($hay, $q) === false) continue;
        }
        $rows[] = planLocationSummary($loc, $withStock);
    }
    usort($rows, function($a,$b){
        return strcmp((string)($a['zone'] ?? ''), (string)($b['zone'] ?? '')) ?: strcmp((string)($a['code'] ?? ''), (string)($b['code'] ?? ''));
    });
    return $rows;
}

function planPublicPlan(array $p): array {
    return [
        'id' => $p['id'] ?? null,
        'name' => $p['name'] ?? '',
        'warehouseId' => (int)($p['warehouseId'] ?? 0),
        'warehouseName' => planWarehouseName((int)($p['warehouseId'] ?? 0)),
        'type' => $p['type'] ?? '',
        'width' => (int)($p['width'] ?? 1600),
        'height' => (int)($p['height'] ?? 1000),
        'scale' => (float)($p['scale'] ?? 1),
        'active' => array_key_exists('active', $p) ? (bool)$p['active'] : true,
    ];
}

function planPublicObject(array $o): array {
    return [
        'id' => $o['id'] ?? null,
        'planId' => $o['planId'] ?? null,
        'type' => $o['type'] ?? '',
        'label' => $o['label'] ?? '',
        'x' => (float)($o['x'] ?? 0),
        'y' => (float)($o['y'] ?? 0),
        'width' => (float)($o['width'] ?? 0),
        'height' => (float)($o['height'] ?? 0),
        'rotation' => (float)($o['rotation'] ?? 0),
        'color' => $o['color'] ?? '',
        'icon' => $o['icon'] ?? '',
        'linkedWarehouseId' => $o['linkedWarehouseId'] ?? null,
        'linkedLocationId' => $o['linkedLocationId'] ?? null,
        'linkedZone' => $o['linkedZone'] ?? '',
        'linkedProductId' => $o['linkedProductId'] ?? null,
        'metadata' => (isset($o['metadata']) && is_array($o['metadata'])) ? $o['metadata'] : [],
    ];
}

function planProductById($id): ?array {
    if($id === null || $id === '' || (string)$id === '0') return null;
    foreach(loadData('products') as $p){
        if((string)($p['id'] ?? '') === (string)$id) return $p;
    }
    return null;
}

function planProductPublic(array $p): array {
    return [
        'id' => $p['id'] ?? null,
        'name' => $p['name'] ?? '',
        'sku' => $p['sku'] ?? '',
        'barcode' => $p['barcode'] ?? '',
        'brand' => $p['brand'] ?? '',
        'category' => $p['category'] ?? '',
        'defaultWarehouse' => $p['defaultWarehouse'] ?? null,
    ];
}

function planStockRowsForProduct($productId, int $warehouseId = 0): array {
    $rows = [];
    foreach(loadData('stock') as $s){
        if((string)($s['productId'] ?? '') !== (string)$productId) continue;
        $wid = (int)($s['warehouseId'] ?? 0);
        if($warehouseId > 0 && $wid !== $warehouseId) continue;
        if($wid > 0 && !userCanAccessWarehouse($wid)) continue;
        $qty = (float)($s['quantity'] ?? 0);
        $locationIds = [];
        if(isset($s['locationId']) && $s['locationId'] !== '' && (string)$s['locationId'] !== '0') {
            $locationIds[] = (string)$s['locationId'];
        }
        if(isset($s['locations']) && is_array($s['locations'])){
            foreach($s['locations'] as $key => $locRow){
                if(is_array($locRow) && isset($locRow['locationId']) && $locRow['locationId'] !== '') $locationIds[] = (string)$locRow['locationId'];
                elseif(is_string($key) && $key !== '' && $key !== 'default') $locationIds[] = $key;
            }
        }
        $locationIds = array_values(array_unique(array_filter($locationIds, fn($v)=>$v !== '' && $v !== '0')));
        $rows[] = [
            'id' => $s['id'] ?? null,
            'productId' => $s['productId'] ?? null,
            'warehouseId' => $wid,
            'quantity' => $qty,
            'locationId' => $s['locationId'] ?? null,
            'locationIds' => $locationIds,
            'picking' => $s['picking'] ?? '',
            'location' => $s['location'] ?? '',
            'rack' => $s['rack'] ?? '',
            'shelf' => $s['shelf'] ?? '',
            'minStock' => $s['minStock'] ?? null,
            'maxStock' => $s['maxStock'] ?? null,
            'lastMovementAt' => $s['lastMovementAt'] ?? null,
        ];
    }
    return $rows;
}

function planProductLocationPayload($productId, int $warehouseId = 0): array {
    requirePerm('products.read');
    $product = planProductById($productId);
    if(!$product) jsonError('Produit introuvable.', 404);
    if($warehouseId > 0 && !userCanAccessWarehouse($warehouseId)) jsonError('Accès refusé à ce magasin.', 403);

    $stockRows = planStockRowsForProduct($productId, $warehouseId);
    $locationIds = [];
    foreach($stockRows as $s){
        foreach(($s['locationIds'] ?? []) as $lid){
            if($lid !== '') $locationIds[$lid] = true;
        }
    }

    $plans = [];
    foreach(loadData('plans') as $p){
        if(!is_array($p)) continue;
        if(array_key_exists('active', $p) && $p['active'] === false) continue;
        $wid = (int)($p['warehouseId'] ?? 0);
        if($warehouseId > 0 && $wid !== $warehouseId) continue;
        if($wid > 0 && !userCanAccessWarehouse($wid)) continue;
        $plans[(string)($p['id'] ?? '')] = $p;
    }

    $matches = [];
    foreach(loadData('plan_objects') as $obj){
        if(!is_array($obj)) continue;
        $planId = (string)($obj['planId'] ?? '');
        if($planId === '' || !isset($plans[$planId])) continue;
        $linkedProduct = isset($obj['linkedProductId']) && (string)$obj['linkedProductId'] === (string)$productId;
        $linkedLocationId = (string)($obj['linkedLocationId'] ?? '');
        $linkedLocation = ($linkedLocationId !== '' && isset($locationIds[$linkedLocationId]));
        if(!$linkedProduct && !$linkedLocation) continue;

        $plan = $plans[$planId];
        $loc = $linkedLocationId !== '' ? planLocationById($linkedLocationId) : null;
        $stockForObject = array_values(array_filter($stockRows, function($s) use ($linkedLocationId, $plan){
            $wid = (int)($s['warehouseId'] ?? 0);
            if($wid > 0 && $wid !== (int)($plan['warehouseId'] ?? 0)) return false;
            if($linkedLocationId === '') return true;
            return in_array($linkedLocationId, array_map('strval', $s['locationIds'] ?? []), true);
        }));
        $qty = array_reduce($stockForObject, fn($sum,$s)=>$sum + (float)($s['quantity'] ?? 0), 0.0);

        $matches[] = [
            'matchType' => $linkedProduct ? 'product' : 'location',
            'score' => ($linkedProduct ? 1000 : 500) + ($qty > 0 ? 100 : 0),
            'plan' => planPublicPlan($plan),
            'object' => planPublicObject($obj),
            'location' => $loc ? planLocationSummary($loc, true) : null,
            'stock' => [
                'quantity' => $qty,
                'rows' => $stockForObject,
            ],
        ];
    }

    usort($matches, fn($a,$b)=>($b['score'] <=> $a['score']) ?: strcmp((string)($a['plan']['name'] ?? ''), (string)($b['plan']['name'] ?? '')));

    $unplaced = [];
    foreach(array_keys($locationIds) as $lid){
        $already = false;
        foreach($matches as $m){
            if((string)($m['location']['id'] ?? '') === (string)$lid){ $already = true; break; }
        }
        if($already) continue;
        $loc = planLocationById($lid);
        if($loc) $unplaced[] = planLocationSummary($loc, true);
    }

    return [
        'product' => planProductPublic($product),
        'warehouseId' => $warehouseId ?: null,
        'matches' => array_values(array_map(function($m){ unset($m['score']); return $m; }, $matches)),
        'best' => isset($matches[0]) ? (function($m){ unset($m['score']); return $m; })($matches[0]) : null,
        'stockRows' => $stockRows,
        'unplacedLocations' => $unplaced,
    ];
}

function planNormalizePlanInput(array $body, ?array $existing = null): array {
    $name = array_key_exists('name', $body) ? planCleanString($body['name'], 120) : planCleanString($existing['name'] ?? '', 120);
    if($name === '') jsonError('Nom du plan requis.');

    $warehouseId = array_key_exists('warehouseId', $body) ? (int)$body['warehouseId'] : (int)($existing['warehouseId'] ?? 0);
    if($warehouseId <= 0) jsonError('Magasin requis pour le plan.');
    if(!planWarehouseExists($warehouseId)) jsonError('Magasin introuvable.', 404);
    if(!userCanAccessWarehouse($warehouseId)) jsonError('Accès refusé à ce magasin.', 403);

    $allowedTypes = ['depot','entrepot','atelier','local','magasin','piece','zone'];
    $type = array_key_exists('type', $body) ? planCleanString($body['type'], 40) : planCleanString($existing['type'] ?? 'depot', 40);
    if(!in_array($type, $allowedTypes, true)) $type = 'depot';

    $out = [
        'name' => $name,
        'warehouseId' => $warehouseId,
        'type' => $type,
        'width' => (int)planNumber($body['width'] ?? ($existing['width'] ?? 1600), 1600, 100, 20000),
        'height' => (int)planNumber($body['height'] ?? ($existing['height'] ?? 1000), 1000, 100, 20000),
        'scale' => planNumber($body['scale'] ?? ($existing['scale'] ?? 1), 1, 0.01, 100),
        'active' => array_key_exists('active', $body) ? (bool)$body['active'] : (bool)($existing['active'] ?? true),
    ];

    $metadata = array_key_exists('metadata', $body) ? $body['metadata'] : ($existing['metadata'] ?? []);
    if(is_array($metadata)){
        $out['metadata'] = $metadata;
    } elseif(isset($existing['metadata']) && is_array($existing['metadata'])){
        $out['metadata'] = $existing['metadata'];
    }

    return $out;
}

function planNormalizeObjectInput(array $body, ?array $existing = null): array {
    $types = planObjectComponentTypes();

    $planId = array_key_exists('planId', $body) ? $body['planId'] : ($existing['planId'] ?? null);
    $plan = planGetById($planId);
    if(!$plan) jsonError('Plan introuvable.', 404);
    planEnsureReadable($plan);

    $type = array_key_exists('type', $body) ? planCleanString($body['type'], 50) : planCleanString($existing['type'] ?? 'emplacement', 50);
    if(!isset($types[$type])) $type = 'emplacement';
    $defaults = $types[$type];

    $linkedWarehouseIdRaw = array_key_exists('linkedWarehouseId', $body) ? $body['linkedWarehouseId'] : ($existing['linkedWarehouseId'] ?? null);
    $linkedWarehouseId = ($linkedWarehouseIdRaw === null || $linkedWarehouseIdRaw === '' || (string)$linkedWarehouseIdRaw === '0') ? null : (int)$linkedWarehouseIdRaw;
    if($linkedWarehouseId !== null){
        if(!planWarehouseExists($linkedWarehouseId)) jsonError('Magasin lié introuvable.', 404);
        if(!userCanAccessWarehouse($linkedWarehouseId)) jsonError('Accès refusé au magasin lié.', 403);
        $planWarehouse = (int)($plan['warehouseId'] ?? 0);
        if($planWarehouse > 0 && $linkedWarehouseId !== $planWarehouse) jsonError('Le magasin lié doit correspondre au magasin du plan.');
    }

    $linkedLocationIdRaw = array_key_exists('linkedLocationId', $body) ? $body['linkedLocationId'] : ($existing['linkedLocationId'] ?? null);
    $linkedLocationId = ($linkedLocationIdRaw === null || $linkedLocationIdRaw === '' || (string)$linkedLocationIdRaw === '0') ? null : $linkedLocationIdRaw;
    if($linkedLocationId !== null){
        $loc = planLocationById($linkedLocationId);
        if(!$loc) jsonError('Emplacement lié introuvable.', 404);
        $locWarehouse = (int)($loc['warehouseId'] ?? 0);
        $planWarehouse = (int)($plan['warehouseId'] ?? 0);
        if($linkedWarehouseId === null) $linkedWarehouseId = $locWarehouse;
        if($locWarehouse > 0 && $linkedWarehouseId !== $locWarehouse) jsonError('L’emplacement lié ne correspond pas au magasin sélectionné.');
        if($planWarehouse > 0 && $locWarehouse > 0 && $locWarehouse !== $planWarehouse) jsonError('L’emplacement lié doit appartenir au magasin du plan.');
        if(!userCanAccessWarehouse($locWarehouse)) jsonError('Accès refusé à l’emplacement lié.', 403);
    }

    $linkedProductIdRaw = array_key_exists('linkedProductId', $body) ? $body['linkedProductId'] : ($existing['linkedProductId'] ?? null);
    $linkedProductId = ($linkedProductIdRaw === null || $linkedProductIdRaw === '' || (string)$linkedProductIdRaw === '0') ? null : (int)$linkedProductIdRaw;

    $metadata = array_key_exists('metadata', $body) ? $body['metadata'] : ($existing['metadata'] ?? []);
    if(!is_array($metadata)) $metadata = [];

    return [
        'planId' => $plan['id'],
        'type' => $type,
        'label' => planCleanString($body['label'] ?? ($existing['label'] ?? ''), 120),
        'x' => planNumber($body['x'] ?? ($existing['x'] ?? 0), 0, -20000, 20000),
        'y' => planNumber($body['y'] ?? ($existing['y'] ?? 0), 0, -20000, 20000),
        'width' => planNumber($body['width'] ?? ($existing['width'] ?? 160), 160, 10, 20000),
        'height' => planNumber($body['height'] ?? ($existing['height'] ?? 80), 80, 10, 20000),
        'rotation' => planNumber($body['rotation'] ?? ($existing['rotation'] ?? 0), 0, -360, 360),
        'color' => planColor($body['color'] ?? ($existing['color'] ?? ($defaults['color'] ?? '#dbeafe')), $defaults['color'] ?? '#dbeafe'),
        'icon' => planCleanString($body['icon'] ?? ($existing['icon'] ?? ($defaults['icon'] ?? '')), 60),
        'linkedWarehouseId' => $linkedWarehouseId,
        'linkedLocationId' => $linkedLocationId,
        'linkedZone' => planCleanString($body['linkedZone'] ?? ($existing['linkedZone'] ?? ''), 120),
        'linkedProductId' => $linkedProductId,
        'metadata' => $metadata,
    ];
}

function planListReadable(): array {
    $rows = loadData('plans');
    $allowedWh = getUserAllowedWarehouses();
    if($allowedWh !== null){
        $allowedWh = array_map('intval', $allowedWh);
        $rows = array_values(array_filter($rows, fn($p)=>in_array((int)($p['warehouseId'] ?? 0), $allowedWh, true)));
    }
    if(isset($_GET['warehouseId']) && $_GET['warehouseId'] !== ''){
        $wid = (int)$_GET['warehouseId'];
        if(!userCanAccessWarehouse($wid)) jsonError('Accès refusé à ce magasin.', 403);
        $rows = array_values(array_filter($rows, fn($p)=>(int)($p['warehouseId'] ?? 0) === $wid));
    }
    if(isset($_GET['active'])){
        $active = (bool)((int)$_GET['active']);
        $rows = array_values(array_filter($rows, fn($p)=>(bool)($p['active'] ?? true) === $active));
    }
    usort($rows, fn($a,$b)=>strcmp((string)($a['name'] ?? ''), (string)($b['name'] ?? '')));
    return $rows;
}

function planObjectsForPlan($planId): array {
    $rows = array_values(array_filter(loadData('plan_objects'), fn($o)=>(string)($o['planId'] ?? '') === (string)$planId));
    usort($rows, fn($a,$b)=>((float)($a['y'] ?? 0) <=> (float)($b['y'] ?? 0)) ?: ((float)($a['x'] ?? 0) <=> (float)($b['x'] ?? 0)));
    return $rows;
}

$action = $_GET['action'] ?? ($body['action'] ?? 'plans');

switch($method){
case 'GET':
    if($action === 'schema'){
        jsonResponse([
            'planTypes' => ['depot','entrepot','atelier','local','magasin','piece','zone'],
            'objectTypes' => planObjectComponentTypes(),
            'componentCategories' => [
                'construire' => 'Construire',
                'structure' => 'Pièces et zones',
                'ouvertures' => 'Portes et ouvertures',
                'circulation' => 'Circulation',
                'stockage' => 'Stockage',
                'organisation' => 'Organisation',
                'repere' => 'Repères',
            ],
            'storage' => ['plans'=>'plans.json', 'objects'=>'plan_objects.json'],
            'permissions' => ['read'=>'plans.read', 'manage'=>'plans.manage'],
        ]);
    }

    if($action === 'locations' || $action === 'locationCandidates'){
        $warehouseId = isset($_GET['warehouseId']) && $_GET['warehouseId'] !== '' ? (int)$_GET['warehouseId'] : 0;
        $q = planCleanString($_GET['q'] ?? '', 120);
        $withStock = isset($_GET['withStock']) && (int)$_GET['withStock'] === 1;
        jsonResponse([
            'warehouseId' => $warehouseId ?: null,
            'locations' => planLocationList($warehouseId, $q, $withStock),
        ]);
    }

    if($action === 'locationInfo'){
        $id = $_GET['id'] ?? '';
        $loc = planLocationById($id);
        if(!$loc) jsonError('Emplacement introuvable.', 404);
        jsonResponse(planLocationSummary($loc, true));
    }

    if($action === 'productLocation'){
        $productId = $_GET['productId'] ?? ($_GET['id'] ?? '');
        $warehouseId = isset($_GET['warehouseId']) && $_GET['warehouseId'] !== '' ? (int)$_GET['warehouseId'] : 0;
        jsonResponse(planProductLocationPayload($productId, $warehouseId));
    }

    if($action === 'objects'){
        $planId = $_GET['planId'] ?? '';
        $plan = planGetById($planId);
        if(!$plan) jsonError('Plan introuvable.', 404);
        planEnsureReadable($plan);
        jsonResponse(planObjectsForPlan($planId));
    }

    if(isset($_GET['id']) && $_GET['id'] !== ''){
        $plan = planGetById($_GET['id']);
        if(!$plan) jsonError('Plan introuvable.', 404);
        planEnsureReadable($plan);
        if((string)($_GET['includeObjects'] ?? '') === '1'){
            $plan['objects'] = planObjectsForPlan($plan['id']);
        }
        jsonResponse($plan);
    }

    jsonResponse(planListReadable());

case 'POST':
    if($action === 'object' || $action === 'createObject'){
        $fields = planNormalizeObjectInput($body);
        $fields['createdAt'] = date('c');
        $fields['updatedAt'] = $fields['createdAt'];
        $fields['createdBy'] = currentUser()['id'] ?? null;
        $created = addItem('plan_objects', $fields);
        jsonResponse($created, 201);
    }

    $fields = planNormalizePlanInput($body);
    $fields['createdAt'] = date('c');
    $fields['updatedAt'] = $fields['createdAt'];
    $fields['createdBy'] = currentUser()['id'] ?? null;
    $fields['updatedBy'] = $fields['createdBy'];
    $created = addItem('plans', $fields);
    jsonResponse($created, 201);

case 'PUT':
    if($action === 'saveObjects' || $action === 'bulkObjects'){
        $planId = $body['planId'] ?? null;
        if(!$planId) jsonError('ID plan requis.');
        $plan = planGetById($planId);
        if(!$plan) jsonError('Plan introuvable.', 404);
        planEnsureReadable($plan);

        $inputObjects = $body['objects'] ?? [];
        if(!is_array($inputObjects)) jsonError('Liste d’objets invalide.');
        if(count($inputObjects) > 600) jsonError('Trop d’objets dans ce plan. Limite : 600.');

        $allObjects = loadData('plan_objects');
        $existingForPlan = [];
        $others = [];
        foreach($allObjects as $obj){
            if((string)($obj['planId'] ?? '') === (string)$planId){
                $existingForPlan[(string)($obj['id'] ?? '')] = $obj;
            } else {
                $others[] = $obj;
            }
        }

        $now = date('c');
        $userId = currentUser()['id'] ?? null;
        $saved = [];
        $seen = [];

        foreach($inputObjects as $raw){
            if(!is_array($raw)) continue;
            $raw['planId'] = $planId;
            $rawId = isset($raw['id']) ? (string)$raw['id'] : '';
            $existing = ($rawId !== '' && strpos($rawId, 'tmp_') !== 0 && isset($existingForPlan[$rawId])) ? $existingForPlan[$rawId] : null;
            $fields = planNormalizeObjectInput($raw, $existing);
            $id = $existing['id'] ?? genId();
            $fields['id'] = $id;
            $fields['createdAt'] = $existing['createdAt'] ?? $now;
            $fields['updatedAt'] = $now;
            $fields['createdBy'] = $existing['createdBy'] ?? $userId;
            $fields['updatedBy'] = $userId;
            $saved[] = $fields;
            $seen[(string)$id] = true;
        }

        saveData('plan_objects', array_values(array_merge($others, $saved)));

        $plans = loadData('plans');
        foreach($plans as &$p){
            if((string)($p['id'] ?? '') === (string)$planId){
                $p['updatedAt'] = $now;
                $p['updatedBy'] = $userId;
                break;
            }
        }
        unset($p);
        saveData('plans', $plans);

        usort($saved, fn($a,$b)=>((float)($a['y'] ?? 0) <=> (float)($b['y'] ?? 0)) ?: ((float)($a['x'] ?? 0) <=> (float)($b['x'] ?? 0)));
        jsonResponse(['objects'=>$saved, 'count'=>count($saved)]);
    }

    if($action === 'object' || $action === 'updateObject'){
        $id = $body['id'] ?? null;
        if(!$id) jsonError('ID objet requis.');
        $objects = loadData('plan_objects');
        $found = null;
        foreach($objects as $o){
            if((string)($o['id'] ?? '') === (string)$id){ $found = $o; break; }
        }
        if(!$found) jsonError('Objet introuvable.', 404);
        $fields = planNormalizeObjectInput($body, $found);
        $fields['updatedAt'] = date('c');
        $fields['updatedBy'] = currentUser()['id'] ?? null;
        $updated = updateItem('plan_objects', $id, $fields);
        $updated ? jsonResponse($updated) : jsonError('Objet introuvable.', 404);
    }

    $id = $body['id'] ?? null;
    if(!$id) jsonError('ID plan requis.');
    $existing = planGetById($id);
    if(!$existing) jsonError('Plan introuvable.', 404);
    planEnsureReadable($existing);
    $fields = planNormalizePlanInput($body, $existing);
    $fields['updatedAt'] = date('c');
    $fields['updatedBy'] = currentUser()['id'] ?? null;
    $updated = updateItem('plans', $id, $fields);
    $updated ? jsonResponse($updated) : jsonError('Plan introuvable.', 404);

case 'DELETE':
    if($action === 'object' || $action === 'deleteObject'){
        $id = $body['id'] ?? ($_GET['id'] ?? null);
        if(!$id) jsonError('ID objet requis.');
        $found = null;
        foreach(loadData('plan_objects') as $o){
            if((string)($o['id'] ?? '') === (string)$id){ $found = $o; break; }
        }
        if(!$found) jsonError('Objet introuvable.', 404);
        $plan = planGetById($found['planId'] ?? '');
        if($plan) planEnsureReadable($plan);
        deleteItem('plan_objects', $id);
        jsonResponse(['ok'=>true]);
    }

    $id = $body['id'] ?? ($_GET['id'] ?? null);
    if(!$id) jsonError('ID plan requis.');
    $plan = planGetById($id);
    if(!$plan) jsonError('Plan introuvable.', 404);
    planEnsureReadable($plan);
    deleteItem('plans', $id);
    $objects = array_values(array_filter(loadData('plan_objects'), fn($o)=>(string)($o['planId'] ?? '') !== (string)$id));
    saveData('plan_objects', $objects);
    jsonResponse(['ok'=>true]);

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