<?php
/**
 * SyncByRule Checksum Verification API
 *
 * Verifies checksums for imported JSON data to prevent manipulation.
 * Includes rate limiting and brute-force protection.
 *
 * @author enured
 * @version 1.0
 */

namespace IMATHUZH\Qfq\Api;

require_once(__DIR__ . '/../../vendor/autoload.php');

use IMATHUZH\Qfq\Core\Store\Store;

// Define API marker
define('QFQ_API', 'Api call');

// Response constants
const CHECKSUM_RESPONSE_VALID = 'valid';
const CHECKSUM_RESPONSE_MESSAGE = 'message';

// Rate limiting constants (can be overridden by constants if defined)
$RATE_LIMIT_ATTEMPTS = defined('SYNC_BY_RULE_CHECKSUM_RATE_LIMIT_ATTEMPTS')
    ? SYNC_BY_RULE_CHECKSUM_RATE_LIMIT_ATTEMPTS : 5;
$RATE_LIMIT_WINDOW = defined('SYNC_BY_RULE_CHECKSUM_RATE_LIMIT_WINDOW')
    ? SYNC_BY_RULE_CHECKSUM_RATE_LIMIT_WINDOW : 60;
$RATE_LIMIT_LOCKOUT = defined('SYNC_BY_RULE_CHECKSUM_RATE_LIMIT_LOCKOUT')
    ? SYNC_BY_RULE_CHECKSUM_RATE_LIMIT_LOCKOUT : 300;

// Rate limit storage file (per-IP tracking)
$rateLimitDir = sys_get_temp_dir() . '/qfq_checksum_ratelimit';

/**
 * Get client IP address
 */
function getClientIp(): string {
    // Check for forwarded IP (behind proxy/load balancer)
    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        $ip = trim($ips[0]);
        if (filter_var($ip, FILTER_VALIDATE_IP)) {
            return $ip;
        }
    }

    if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
        $ip = $_SERVER['HTTP_X_REAL_IP'];
        if (filter_var($ip, FILTER_VALIDATE_IP)) {
            return $ip;
        }
    }

    return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}

/**
 * Get rate limit file path for an IP
 */
function getRateLimitFile(string $ip, string $dir): string {
    // Sanitize IP for filename
    $safeIp = preg_replace('/[^a-zA-Z0-9\.\-_]/', '_', $ip);
    return $dir . '/ratelimit_' . $safeIp . '.json';
}

/**
 * Check if IP is rate limited
 *
 * @param string $ip Client IP
 * @param string $dir Rate limit storage directory
 * @param int $maxAttempts Maximum attempts allowed
 * @param int $window Time window in seconds
 * @param int $lockout Lockout duration in seconds
 * @return array ['allowed' => bool, 'remaining' => int, 'resetIn' => int]
 */
function checkRateLimit(string $ip, string $dir, int $maxAttempts, int $window, int $lockout): array {
    // Ensure directory exists
    if (!is_dir($dir)) {
        mkdir($dir, 0750, true);
    }

    $file = getRateLimitFile($ip, $dir);
    $now = time();

    // Load existing data
    $data = [
        'attempts' => [],
        'lockedUntil' => 0
    ];

    if (file_exists($file)) {
        $content = file_get_contents($file);
        $loaded = json_decode($content, true);
        if (is_array($loaded)) {
            $data = $loaded;
        }
    }

    // Check if currently locked out
    if ($data['lockedUntil'] > $now) {
        return [
            'allowed' => false,
            'remaining' => 0,
            'resetIn' => $data['lockedUntil'] - $now,
            'locked' => true
        ];
    }

    // Clean old attempts (outside window)
    $data['attempts'] = array_filter($data['attempts'], function($timestamp) use ($now, $window) {
        return $timestamp > ($now - $window);
    });
    $data['attempts'] = array_values($data['attempts']); // Re-index

    $attemptCount = count($data['attempts']);

    // Check if under limit
    if ($attemptCount < $maxAttempts) {
        return [
            'allowed' => true,
            'remaining' => $maxAttempts - $attemptCount - 1,
            'resetIn' => empty($data['attempts']) ? 0 : ($data['attempts'][0] + $window - $now),
            'locked' => false
        ];
    }

    // Rate limit exceeded - apply lockout
    $data['lockedUntil'] = $now + $lockout;
    $data['attempts'] = []; // Reset attempts for next window

    // Save lockout state
    file_put_contents($file, json_encode($data), LOCK_EX);

    return [
        'allowed' => false,
        'remaining' => 0,
        'resetIn' => $lockout,
        'locked' => true
    ];
}

/**
 * Record a rate limit attempt
 */
function recordAttempt(string $ip, string $dir): void {
    // Ensure directory exists
    if (!is_dir($dir)) {
        mkdir($dir, 0750, true);
    }

    $file = getRateLimitFile($ip, $dir);
    $now = time();

    // Load existing data
    $data = [
        'attempts' => [],
        'lockedUntil' => 0
    ];

    if (file_exists($file)) {
        $content = file_get_contents($file);
        $loaded = json_decode($content, true);
        if (is_array($loaded)) {
            $data = $loaded;
        }
    }

    // Add current attempt
    $data['attempts'][] = $now;

    // Save
    file_put_contents($file, json_encode($data), LOCK_EX);
}

/**
 * Calculate checksum using HMAC-SHA256
 */
function calculateChecksum(string $jsonData, string $authKey): string {
    return hash_hmac('sha256', $jsonData, $authKey);
}

/**
 * Send JSON response
 */
function sendResponse(array $data, int $httpCode = 200): void {
    http_response_code($httpCode);
    header('Content-Type: application/json');
    header('Cache-Control: no-store, no-cache, must-revalidate');
    header('X-Content-Type-Options: nosniff');
    echo json_encode($data);
    exit;
}

// === MAIN EXECUTION ===

// Set security headers
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');

// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    sendResponse([
        CHECKSUM_RESPONSE_VALID => false,
        CHECKSUM_RESPONSE_MESSAGE => 'Method not allowed'
    ], 405);
}

// Get client IP for rate limiting
$clientIp = getClientIp();

// Check rate limit BEFORE processing
$rateLimitStatus = checkRateLimit(
    $clientIp,
    $rateLimitDir,
    $RATE_LIMIT_ATTEMPTS,
    $RATE_LIMIT_WINDOW,
    $RATE_LIMIT_LOCKOUT
);

if (!$rateLimitStatus['allowed']) {
    $resetIn = $rateLimitStatus['resetIn'];
    header('Retry-After: ' . $resetIn);
    sendResponse([
        CHECKSUM_RESPONSE_VALID => false,
        CHECKSUM_RESPONSE_MESSAGE => "Rate limit exceeded. Try again in {$resetIn} seconds."
    ], 429);
}

// Record this attempt
recordAttempt($clientIp, $rateLimitDir);

try {
    // Get POST data
    $input = file_get_contents('php://input');
    $requestData = json_decode($input, true);

    if (json_last_error() !== JSON_ERROR_NONE) {
        sendResponse([
            CHECKSUM_RESPONSE_VALID => false,
            CHECKSUM_RESPONSE_MESSAGE => 'Invalid JSON request'
        ], 400);
    }

    // Validate required fields
    $checksum = $requestData['checksum'] ?? null;
    $jsonData = $requestData['jsonData'] ?? null;

    if (empty($checksum) || empty($jsonData)) {
        sendResponse([
            CHECKSUM_RESPONSE_VALID => false,
            CHECKSUM_RESPONSE_MESSAGE => 'Missing required fields: checksum and jsonData'
        ], 400);
    }

    // Validate checksum format (SHA256 hex = 64 characters)
    if (!preg_match('/^[a-f0-9]{64}$/i', $checksum)) {
        sendResponse([
            CHECKSUM_RESPONSE_VALID => false,
            CHECKSUM_RESPONSE_MESSAGE => 'Invalid checksum format'
        ], 400);
    }

    // Get encryption key from Store
    $store = Store::getInstance('', false);
    $authKey = $store->getVar(SYSTEM_ENCRYPTION_KEY, STORE_SYSTEM);

    if (empty($authKey)) {
        error_log('SyncByRule checksum.php: SYSTEM_ENCRYPTION_KEY not configured');
        sendResponse([
            CHECKSUM_RESPONSE_VALID => false,
            CHECKSUM_RESPONSE_MESSAGE => 'Server configuration error'
        ], 500);
    }

    // Calculate expected checksum
    $expectedChecksum = calculateChecksum($jsonData, $authKey);

    // Use timing-safe comparison to prevent timing attacks
    $isValid = hash_equals($expectedChecksum, strtolower($checksum));

    if ($isValid) {
        sendResponse([
            CHECKSUM_RESPONSE_VALID => true,
            CHECKSUM_RESPONSE_MESSAGE => 'Checksum verified'
        ]);
    } else {
        // Don't reveal details about why it failed
        sendResponse([
            CHECKSUM_RESPONSE_VALID => false,
            CHECKSUM_RESPONSE_MESSAGE => 'Checksum verification failed'
        ], 403);
    }

} catch (\Throwable $e) {
    // Log error but don't expose details
    error_log('SyncByRule checksum.php error: ' . $e->getMessage());

    sendResponse([
        CHECKSUM_RESPONSE_VALID => false,
        CHECKSUM_RESPONSE_MESSAGE => 'Internal server error'
    ], 500);
}