<?php
/**
 * Optimistic Locking Helper for High-Concurrency Stock Operations
 * Reduces database lock contention by using version-based concurrency control
 */

class OptimisticLock {
    /**
     * Get current stock with version for optimistic locking
     */
    public static function getStockWithVersion(int $productId, int $locationId, int $tenantId): ?array {
        $stmt = db()->prepare("
            SELECT id, product_id, location_id, quantity, batch_id, 
                   updated_at as version
            FROM stock 
            WHERE product_id = ? AND location_id = ? AND tenant_id = ?
            LIMIT 1
        ");
        $stmt->execute([$productId, $locationId, $tenantId]);
        return $stmt->fetch();
    }

    /**
     * Update stock with optimistic locking (version check)
     * Returns true on success, false on version mismatch
     */
    public static function updateStockWithVersion(
        int $stockId, 
        float $newQuantity, 
        string $version, 
        int $tenantId
    ): bool {
        try {
            $stmt = db()->prepare("
                UPDATE stock 
                SET quantity = ?, updated_at = NOW()
                WHERE id = ? 
                  AND tenant_id = ? 
                  AND updated_at = ?
            ");
            $stmt->execute([$newQuantity, $stockId, $tenantId, $version]);
            
            return $stmt->rowCount() > 0;
        } catch (PDOException $e) {
            return false;
        }
    }

    /**
     * Adjust stock with retry logic for optimistic locking
     * Automatically retries on version mismatch (up to max retries)
     */
    public static function adjustStockWithRetry(
        int $productId, 
        int $locationId, 
        float $quantity, 
        int $tenantId,
        ?string $batchId = null,
        int $maxRetries = 3
    ): bool {
        $attempt = 0;
        
        while ($attempt < $maxRetries) {
            $attempt++;
            
            // Get current stock with version
            $stock = self::getStockWithVersion($productId, $locationId, $tenantId);
            
            if (!$stock) {
                // Stock doesn't exist, create it
                try {
                    $stmt = db()->prepare("
                        INSERT INTO stock (product_id, location_id, quantity, batch_id, tenant_id, created_at, updated_at)
                        VALUES (?, ?, ?, ?, ?, NOW(), NOW())
                    ");
                    $stmt->execute([$productId, $locationId, $quantity, $batchId, $tenantId]);
                    return true;
                } catch (PDOException $e) {
                    // Might have been created by another process, retry
                    continue;
                }
            }
            
            $newQuantity = $stock['quantity'] + $quantity;
            
            // Validate stock level
            if ($newQuantity < 0) {
                return false; // Insufficient stock
            }
            
            // Try to update with version check
            if (self::updateStockWithVersion($stock['id'], $newQuantity, $stock['version'], $tenantId)) {
                return true;
            }
            
            // Version mismatch, wait briefly and retry
            usleep(10000 * $attempt); // Exponential backoff: 10ms, 20ms, 30ms
        }
        
        return false; // Max retries exceeded
    }

    /**
     * Batch stock adjustment with optimistic locking
     * Processes multiple stock updates atomically
     */
    public static function batchAdjustStockWithRetry(
        array $adjustments, // [[productId, locationId, quantity, batchId?], ...]
        int $tenantId,
        int $maxRetries = 3
    ): array {
        $results = [];
        $successCount = 0;
        $failureCount = 0;
        
        foreach ($adjustments as $adjustment) {
            $productId = $adjustment[0];
            $locationId = $adjustment[1];
            $quantity = $adjustment[2];
            $batchId = $adjustment[3] ?? null;
            
            if (self::adjustStockWithRetry($productId, $locationId, $quantity, $tenantId, $batchId, $maxRetries)) {
                $results[] = ['product_id' => $productId, 'success' => true];
                $successCount++;
            } else {
                $results[] = ['product_id' => $productId, 'success' => false, 'error' => 'Version conflict or insufficient stock'];
                $failureCount++;
            }
        }
        
        return [
            'results' => $results,
            'success_count' => $successCount,
            'failure_count' => $failureCount
        ];
    }

    /**
     * Check if a product has sufficient stock (without locking)
     */
    public static function checkStockAvailability(
        int $productId, 
        int $locationId, 
        float $requiredQuantity, 
        int $tenantId
    ): bool {
        $stmt = db()->prepare("
            SELECT SUM(quantity) as total_quantity
            FROM stock 
            WHERE product_id = ? AND location_id = ? AND tenant_id = ?
        ");
        $stmt->execute([$productId, $locationId, $tenantId]);
        $result = $stmt->fetch();
        
        return $result && $result['total_quantity'] >= $requiredQuantity;
    }

    /**
     * Get stock across all locations (for inventory checks)
     */
    public static function getTotalStock(int $productId, int $tenantId): float {
        $stmt = db()->prepare("
            SELECT SUM(quantity) as total_quantity
            FROM stock 
            WHERE product_id = ? AND tenant_id = ?
        ");
        $stmt->execute([$productId, $tenantId]);
        $result = $stmt->fetch();
        
        return $result ? (float)$result['total_quantity'] : 0;
    }
}
