<?php
/**
 * Fichier  : api/inventories.php
 * Auteur   : KREMER Régis
 * Version  : 2.6.76
 * Date     : 2026-07-17
 * Description : API REST — Inventaires persistants, reprise, revue des écarts et application au stock.
 *
 * Changelog :
 *   v2.1.0 (2026-02-21) — Version initiale auditée et sécurisée.
 *   v2.3.17 (2026-06-04) — Quantités décimales, EAN multiples et notification push non bloquante.
 *   v2.6.75 (2026-07-17) — INVENTORY RESUME REVIEW2 : brouillons serveur, modes complet/ciblé,
 *                           reprise multi-appareil, révision optimiste et revue obligatoire avant clôture.
 *   v2.6.76 (2026-07-17) — INVENTORY FIELD RECETTE3 : empreinte du stock revue/clôture et contrôle renforcé des conflits.
 */
require_once dirname(__DIR__).'/config.php';
require_once dirname(__DIR__).'/includes/push-helper.php';
startSession();
requireAuth();

$method = $_SERVER['REQUEST_METHOD'];
if ($method !== 'GET') csrfCheck();
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$user = currentUser() ?: [];

function inventoryCategoriesFromPayload(array $payload): array {
    $categories = [];
    if (isset($payload['categories']) && is_array($payload['categories'])) {
        $categories = $payload['categories'];
    } elseif (isset($payload['category']) && is_string($payload['category']) && trim($payload['category']) !== '') {
        $categories = [trim($payload['category'])];
    }
    $categories = array_values(array_unique(array_filter(array_map(
        static fn($value) => trim((string)$value),
        $categories
    ), static fn($value) => $value !== '')));
    return array_slice($categories, 0, 250);
}

function inventoryModeFromPayload(array $payload, array $categories): string {
    $raw = strtolower(trim((string)($payload['mode'] ?? '')));
    if (in_array($raw, ['full', 'partial'], true)) return $raw;
    return count($categories) > 0 ? 'partial' : 'full';
}

function inventoryAssertWarehouseAccess($warehouseId): void {
    if (!$warehouseId) jsonError('warehouseId requis.', 422);
    if (!userCanAccessWarehouse($warehouseId)) jsonError('Accès interdit à ce magasin.', 403);
}

function inventoryFindIndex(array $inventories, $id): int {
    foreach ($inventories as $index => $inventory) {
        if ((string)($inventory['id'] ?? '') === (string)$id) return (int)$index;
    }
    return -1;
}

function inventoryAssertDraft(array $inventory): void {
    if (($inventory['status'] ?? '') !== 'draft') jsonError('Ce brouillon n’est plus disponible.', 409);
    inventoryAssertWarehouseAccess($inventory['warehouseId'] ?? null);
}

function inventoryAssertRevision(array $inventory, array $payload): void {
    if (!array_key_exists('expectedRevision', $payload) || $payload['expectedRevision'] === null || $payload['expectedRevision'] === '') return;
    $expected = (int)$payload['expectedRevision'];
    $actual = (int)($inventory['revision'] ?? 1);
    if ($expected !== $actual) {
        jsonResponse([
            'error' => 'Ce brouillon a été modifié depuis un autre appareil. Rechargez-le avant de poursuivre.',
            'currentRevision' => $actual,
        ], 409);
    }
}

function inventoryStoreTransaction(callable $mutator) {
    $lockPath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR
        . 'magestock-inventories-' . sha1((string)DATA_DIR) . '.lock';
    $lock = fopen($lockPath, 'c+');
    if (!$lock) jsonError('Impossible de verrouiller les inventaires.', 500);
    if (!flock($lock, LOCK_EX)) {
        fclose($lock);
        jsonError('Impossible de verrouiller les inventaires.', 500);
    }
    try {
        $inventories = loadData('inventories');
        $result = $mutator($inventories);
        if (!saveData('inventories', array_values($inventories))) {
            jsonError('Impossible d’enregistrer les inventaires.', 500);
        }
        return $result;
    } finally {
        flock($lock, LOCK_UN);
        fclose($lock);
    }
}

function inventoryBuildLines($warehouseId, array $categories): array {
    $products = loadData('products');
    $stock = loadData('stock');
    $productIds = [];
    $stockQty = [];

    foreach ($stock as $row) {
        if ((string)($row['warehouseId'] ?? '') !== (string)$warehouseId) continue;
        $productId = (string)($row['productId'] ?? '');
        if ($productId === '') continue;
        $productIds[$productId] = true;
        $stockQty[$productId] = (float)($row['quantity'] ?? 0);
    }

    $lines = [];
    foreach ($products as $product) {
        $productId = (string)($product['id'] ?? '');
        if ($productId === '' || !isset($productIds[$productId])) continue;
        $category = trim((string)($product['category'] ?? ''));
        if ($categories && !in_array($category, $categories, true)) continue;
        $lines[] = [
            'productId' => $product['id'],
            'name' => trim((string)($product['name'] ?? 'Produit')),
            'sku' => trim((string)($product['sku'] ?? '')),
            'barcode' => trim((string)($product['barcode'] ?? '')),
            'eans' => isset($product['eans']) && is_array($product['eans']) ? array_values($product['eans']) : [],
            'category' => $category,
            'image' => trim((string)($product['image'] ?? '')),
            'expected' => $stockQty[$productId] ?? 0,
            'counted' => null,
        ];
    }
    return $lines;
}

function inventoryNormaliseCount($value) {
    if ($value === null || $value === '') return null;
    if (!is_numeric($value)) return null;
    $number = (float)$value;
    return $number < 0 ? null : $number;
}

function inventoryMergeCounts(array $savedLines, array $incomingLines): array {
    $counts = [];
    foreach ($incomingLines as $line) {
        if (!is_array($line)) continue;
        $productId = (string)($line['productId'] ?? '');
        if ($productId === '') continue;
        $counts[$productId] = inventoryNormaliseCount($line['counted'] ?? null);
    }
    foreach ($savedLines as &$line) {
        $productId = (string)($line['productId'] ?? '');
        if ($productId !== '' && array_key_exists($productId, $counts)) {
            $line['counted'] = $counts[$productId];
        }
    }
    unset($line);
    return array_values($savedLines);
}

function inventoryCurrentStockMap($warehouseId): array {
    $map = [];
    foreach (loadData('stock') as $row) {
        if ((string)($row['warehouseId'] ?? '') !== (string)$warehouseId) continue;
        $map[(string)($row['productId'] ?? '')] = (float)($row['quantity'] ?? 0);
    }
    return $map;
}

function inventoryStockFingerprint(array $inventory): string {
    $currentStock = inventoryCurrentStockMap($inventory['warehouseId'] ?? null);
    $snapshot = [];
    foreach (($inventory['lines'] ?? []) as $line) {
        if (inventoryNormaliseCount($line['counted'] ?? null) === null) continue;
        $productId = (string)($line['productId'] ?? '');
        if ($productId === '') continue;
        $snapshot[$productId] = (float)($currentStock[$productId] ?? 0);
    }
    ksort($snapshot, SORT_STRING);
    return hash('sha256', json_encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION));
}

function inventorySummary(array $inventory): array {
    $lines = isset($inventory['lines']) && is_array($inventory['lines']) ? $inventory['lines'] : [];
    $currentStock = inventoryCurrentStockMap($inventory['warehouseId'] ?? null);
    $summary = [
        'total' => count($lines),
        'counted' => 0,
        'pending' => 0,
        'conform' => 0,
        'differences' => 0,
        'corrections' => 0,
        'positive' => 0,
        'negative' => 0,
        'stockChanged' => 0,
        'differenceLines' => [],
        'pendingLines' => [],
        'mode' => ($inventory['mode'] ?? 'full') === 'partial' ? 'partial' : 'full',
        'canApply' => false,
        'blockingReason' => '',
    ];

    foreach ($lines as $line) {
        $counted = inventoryNormaliseCount($line['counted'] ?? null);
        if ($counted === null) {
            $summary['pending']++;
            if (count($summary['pendingLines']) < 30) {
                $summary['pendingLines'][] = [
                    'productId' => $line['productId'] ?? null,
                    'name' => $line['name'] ?? 'Produit',
                    'sku' => $line['sku'] ?? '',
                ];
            }
            continue;
        }

        $summary['counted']++;
        $expected = (float)($line['expected'] ?? 0);
        $difference = $counted - $expected;
        $current = $currentStock[(string)($line['productId'] ?? '')] ?? 0;
        $stockChanged = abs($current - $expected) > 0.000001;
        $appliedDifference = $counted - $current;

        if (abs($difference) <= 0.000001) {
            $summary['conform']++;
        } else {
            $summary['differences']++;
            if ($difference > 0) $summary['positive']++; else $summary['negative']++;
        }
        if ($stockChanged) $summary['stockChanged']++;

        // La revue présente les corrections réellement appliquées au stock courant.
        // Une ligne conforme au lancement peut devenir une correction si le stock a évolué entre-temps.
        if (abs($appliedDifference) > 0.000001) {
            $summary['corrections']++;
            $summary['differenceLines'][] = [
                'productId' => $line['productId'] ?? null,
                'name' => $line['name'] ?? 'Produit',
                'sku' => $line['sku'] ?? '',
                'category' => $line['category'] ?? '',
                'image' => $line['image'] ?? '',
                'expected' => $expected,
                'counted' => $counted,
                'difference' => $difference,
                'appliedDifference' => $appliedDifference,
                'currentStock' => $current,
                'stockChanged' => $stockChanged,
            ];
        }
    }

    if ($summary['mode'] === 'full') {
        $summary['canApply'] = $summary['total'] > 0 && $summary['pending'] === 0;
        if (!$summary['canApply']) $summary['blockingReason'] = 'Un inventaire complet exige le comptage de tous les produits.';
    } else {
        $summary['canApply'] = $summary['counted'] > 0;
        if (!$summary['canApply']) $summary['blockingReason'] = 'Comptez au moins un produit avant de clôturer ce contrôle ciblé.';
    }
    return $summary;
}

function inventoryAdjustLocations(array &$stockRow, float $target): void {
    if (!isset($stockRow['locations']) || !is_array($stockRow['locations'])) {
        $stockRow['quantity'] = $target;
        return;
    }
    if (!isset($stockRow['locations']['default']) || !is_array($stockRow['locations']['default'])) {
        $stockRow['locations']['default'] = ['quantity' => 0, 'lots' => []];
    }
    $current = 0.0;
    foreach ($stockRow['locations'] as $location) {
        if (is_array($location)) $current += (float)($location['quantity'] ?? 0);
    }
    $delta = $target - $current;
    if ($delta >= 0) {
        $stockRow['locations']['default']['quantity'] = (float)($stockRow['locations']['default']['quantity'] ?? 0) + $delta;
    } else {
        $remaining = abs($delta);
        $keys = array_values(array_unique(array_merge(['default'], array_keys($stockRow['locations']))));
        foreach ($keys as $key) {
            if ($remaining <= 0.000001 || !isset($stockRow['locations'][$key]) || !is_array($stockRow['locations'][$key])) continue;
            $quantity = max(0, (float)($stockRow['locations'][$key]['quantity'] ?? 0));
            $take = min($quantity, $remaining);
            $stockRow['locations'][$key]['quantity'] = $quantity - $take;
            $remaining -= $take;
        }
    }
    $stockRow['quantity'] = $target;
}

function inventoryApplyDraft(array $draft, array $user): array {
    $summary = inventorySummary($draft);
    if (!$summary['canApply']) jsonError($summary['blockingReason'] ?: 'Ce brouillon ne peut pas être clôturé.', 422);

    $stock = loadData('stock');
    $movements = loadData('movements');
    $warehouseId = (int)($draft['warehouseId'] ?? 0);
    $now = date('c');
    $userId = (int)($user['id'] ?? 0);
    $operator = appUserDisplayName($user, $user['username'] ?? 'Inconnu');

    foreach (($draft['lines'] ?? []) as $line) {
        $counted = inventoryNormaliseCount($line['counted'] ?? null);
        if ($counted === null) continue;
        $productId = (int)($line['productId'] ?? 0);
        if (!$productId) continue;
        $before = 0.0;
        $found = false;
        foreach ($stock as &$row) {
            if ((string)($row['productId'] ?? '') !== (string)$productId || (string)($row['warehouseId'] ?? '') !== (string)$warehouseId) continue;
            $before = (float)($row['quantity'] ?? 0);
            inventoryAdjustLocations($row, $counted);
            $row['maxAuthorized'] = max((float)($row['maxAuthorized'] ?? 0), $counted);
            $row['updatedAt'] = $now;
            $row['lastMovementAt'] = $now;
            $found = true;
            break;
        }
        unset($row);
        if (!$found && $counted > 0) {
            $before = 0;
            $stock[] = [
                'id' => genId(), 'productId' => $productId, 'warehouseId' => $warehouseId,
                'quantity' => $counted, 'maxAuthorized' => $counted,
                'minStock' => null, 'maxStock' => null, 'safetyStock' => null,
                'lots' => [], 'locations' => ['default' => ['quantity' => $counted, 'lots' => []]],
                'lastMovementAt' => $now, 'createdAt' => $now, 'updatedAt' => $now,
            ];
        }

        $appliedDifference = $counted - $before;
        if (abs($appliedDifference) > 0.000001) {
            $snapshotDifference = $counted - (float)($line['expected'] ?? 0);
            $movements[] = [
                'id' => genId(),
                'productId' => $productId,
                'warehouseId' => $warehouseId,
                'type' => 'inventory',
                'quantity' => abs($appliedDifference),
                'baseQuantity' => abs($appliedDifference),
                'unit' => 'piece',
                'reason' => 'Correction inventaire (' . ($appliedDifference > 0 ? '+' : '') . $appliedDifference . ')',
                'site' => '',
                'operator' => $operator,
                'note' => 'Inventaire #' . ($draft['id'] ?? '') . ' — écart au lancement : ' . ($snapshotDifference > 0 ? '+' : '') . $snapshotDifference,
                'userId' => $userId,
                'inventoryId' => $draft['id'] ?? null,
                'beforeQuantity' => $before,
                'afterQuantity' => $counted,
                'date' => $now,
                'createdAt' => $now,
            ];
        }
    }

    if (!saveData('stock', array_values($stock))) jsonError('Impossible d’enregistrer le stock.', 500);
    if (!saveData('movements', array_values($movements))) jsonError('Impossible d’enregistrer les mouvements.', 500);

    $draft['status'] = 'validated';
    $draft['stage'] = 'closed';
    $draft['date'] = $now;
    $draft['completedAt'] = $now;
    $draft['completedBy'] = $userId;
    $draft['operator'] = $operator;
    $draft['discrepancies'] = (int)$summary['differences'];
    $draft['appliedCorrections'] = (int)($summary['corrections'] ?? 0);
    $draft['countedProducts'] = (int)$summary['counted'];
    $draft['ignoredProducts'] = (int)$summary['pending'];
    $draft['reviewSummary'] = $summary;
    $draft['updatedAt'] = $now;
    return $draft;
}

switch ($method) {
case 'GET':
    requirePerm('inventory.read');
    $scope = strtolower(trim((string)($_GET['scope'] ?? 'history')));
    $inventories = loadData('inventories');
    $allowedWarehouses = getUserAllowedWarehouses();
    if ($allowedWarehouses !== null) {
        $inventories = array_values(array_filter($inventories, static fn($inventory) => in_array((int)($inventory['warehouseId'] ?? 0), $allowedWarehouses, true)));
    }

    if ($scope === 'drafts' || $scope === 'draft') {
        requirePerm('inventory.write');
        $inventories = array_values(array_filter($inventories, static fn($inventory) => ($inventory['status'] ?? '') === 'draft'));
        if ($scope === 'draft') {
            $id = $_GET['id'] ?? null;
            $draft = findById($inventories, $id);
            if (!$draft) jsonError('Brouillon introuvable.', 404);
            jsonResponse($draft);
        }
        usort($inventories, static fn($a, $b) => strcmp((string)($b['updatedAt'] ?? $b['createdAt'] ?? ''), (string)($a['updatedAt'] ?? $a['createdAt'] ?? '')));
        jsonResponse($inventories);
    }

    $inventories = array_values(array_filter($inventories, static fn($inventory) => !isset($inventory['status']) || ($inventory['status'] ?? '') === 'validated'));
    usort($inventories, static fn($a, $b) => strcmp((string)($b['date'] ?? $b['completedAt'] ?? ''), (string)($a['date'] ?? $a['completedAt'] ?? '')));
    jsonResponse($inventories);

case 'POST':
    requirePerm('inventory.write');
    $action = strtolower(trim((string)($body['action'] ?? '')));

    if ($action === 'start') {
        $warehouseId = $body['warehouseId'] ?? null;
        inventoryAssertWarehouseAccess($warehouseId);
        $title = trim((string)($body['title'] ?? ''));
        if ($title === '') jsonError('Titre requis.', 422);
        if (function_exists('mb_substr')) $title = mb_substr($title, 0, 180, 'UTF-8'); else $title = substr($title, 0, 180);
        $categories = inventoryCategoriesFromPayload($body);
        $mode = inventoryModeFromPayload($body, $categories);
        if ($mode === 'full') $categories = [];
        if ($mode === 'partial' && !$categories) jsonError('Sélectionnez au moins une catégorie pour un contrôle ciblé.', 422);
        $lines = inventoryBuildLines($warehouseId, $categories);
        if (!$lines) jsonError('Aucun produit n’est disponible dans ce périmètre.', 422);
        $now = date('c');
        $operator = appUserDisplayName($user, $user['username'] ?? 'Inconnu');
        $draft = [
            'id' => genId(),
            'warehouseId' => (int)$warehouseId,
            'title' => $title,
            'mode' => $mode,
            'category' => $categories ? implode(', ', $categories) : '',
            'categories' => $categories,
            'operator' => $operator,
            'createdBy' => (int)($user['id'] ?? 0),
            'updatedBy' => (int)($user['id'] ?? 0),
            'startedAt' => $now,
            'date' => $now,
            'createdAt' => $now,
            'updatedAt' => $now,
            'status' => 'draft',
            'stage' => 'counting',
            'revision' => 1,
            'reviewedRevision' => null,
            'reviewedAt' => null,
            'lines' => $lines,
            'discrepancies' => 0,
        ];
        inventoryStoreTransaction(static function (array &$inventories) use ($draft): void {
            $inventories[] = $draft;
        });
        jsonResponse($draft, 201);
    }

    if (in_array($action, ['save', 'review', 'discard', 'validate'], true) && !empty($body['draftId'] ?? $body['id'] ?? null)) {
        $draftId = $body['draftId'] ?? $body['id'];
        $result = inventoryStoreTransaction(function (array &$inventories) use ($draftId, $body, $action, $user): array {
            $index = inventoryFindIndex($inventories, $draftId);
            if ($index < 0) jsonError('Brouillon introuvable.', 404);
            $draft = $inventories[$index];
            inventoryAssertDraft($draft);
            inventoryAssertRevision($draft, $body);

            if ($action === 'discard') {
                array_splice($inventories, $index, 1);
                return ['type' => 'discard', 'payload' => ['ok' => true]];
            }

            if ($action === 'save' || $action === 'review') {
                if (isset($body['lines']) && is_array($body['lines'])) {
                    $draft['lines'] = inventoryMergeCounts($draft['lines'] ?? [], $body['lines']);
                }
                $draft['revision'] = (int)($draft['revision'] ?? 1) + 1;
                $draft['updatedBy'] = (int)($user['id'] ?? 0);
                $draft['updatedAt'] = date('c');
                if ($action === 'review') {
                    $draft['stage'] = 'review';
                    $draft['reviewedAt'] = date('c');
                    $draft['reviewedRevision'] = $draft['revision'];
                    $draft['reviewSummary'] = inventorySummary($draft);
                    $draft['reviewStockFingerprint'] = inventoryStockFingerprint($draft);
                } else {
                    $draft['stage'] = 'counting';
                    $draft['reviewedAt'] = null;
                    $draft['reviewedRevision'] = null;
                    unset($draft['reviewSummary'], $draft['reviewStockFingerprint']);
                }
                $inventories[$index] = $draft;
                return [
                    'type' => $action,
                    'payload' => $action === 'review'
                        ? ['draft' => $draft, 'summary' => $draft['reviewSummary']]
                        : $draft,
                ];
            }

            $revision = (int)($draft['revision'] ?? 1);
            if (($draft['stage'] ?? '') !== 'review' || (int)($draft['reviewedRevision'] ?? 0) !== $revision) {
                jsonError('La revue des écarts doit être actualisée avant l’application au stock.', 409);
            }
            $reviewFingerprint = (string)($draft['reviewStockFingerprint'] ?? '');
            $currentFingerprint = inventoryStockFingerprint($draft);
            if ($reviewFingerprint === '' || !hash_equals($reviewFingerprint, $currentFingerprint)) {
                jsonError('Le stock a changé depuis la revue. Actualisez les écarts avant de clôturer.', 409);
            }
            $validated = inventoryApplyDraft($draft, $user);
            $inventories[$index] = $validated;
            return ['type' => 'validate', 'payload' => $validated];
        });

        if (($result['type'] ?? '') === 'validate') {
            $validated = $result['payload'];
            $warehouses = loadData('warehouses');
            $warehouse = findById($warehouses, $validated['warehouseId'] ?? 0);
            $warehouseName = $warehouse['name'] ?? 'Magasin';
            try {
                notifyInventoryComplete($warehouseName, (int)($validated['countedProducts'] ?? 0), (int)($validated['discrepancies'] ?? 0), (int)($user['id'] ?? 0));
            } catch (Throwable $exception) {
                error_log('[inventory] Notification push non bloquante : ' . $exception->getMessage());
            }
            jsonResponse($validated, 201);
        }
        jsonResponse($result['payload'] ?? ['ok' => true]);
    }

    // Compatibilité : ancien client sans brouillon persistant.
    if ($action === 'validate') {
        $warehouseId = $body['warehouseId'] ?? null;
        inventoryAssertWarehouseAccess($warehouseId);
        $categories = inventoryCategoriesFromPayload($body);
        $mode = inventoryModeFromPayload($body, $categories);
        $legacyDraft = [
            'id' => genId(),
            'warehouseId' => (int)$warehouseId,
            'title' => trim((string)($body['title'] ?? '')) ?: ('Inventaire du ' . date('d/m/Y')),
            'mode' => $mode,
            'category' => $categories ? implode(', ', $categories) : '',
            'categories' => $categories,
            'operator' => appUserDisplayName($user, $user['username'] ?? 'Inconnu'),
            'createdBy' => (int)($user['id'] ?? 0),
            'status' => 'draft',
            'stage' => 'review',
            'revision' => 1,
            'reviewedRevision' => 1,
            'reviewedAt' => date('c'),
            'lines' => isset($body['lines']) && is_array($body['lines']) ? $body['lines'] : [],
            'createdAt' => date('c'),
        ];
        $validated = inventoryApplyDraft($legacyDraft, $user);
        inventoryStoreTransaction(static function (array &$inventories) use ($validated): void {
            $inventories[] = $validated;
        });
        jsonResponse($validated, 201);
    }

    jsonError('Action inconnue.', 400);

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