<?php
declare(strict_types=1);

const CHRONO_REMEMBER_COOKIE = 'chrono_party_remember';
const CHRONO_REMEMBER_LIFETIME = 31536000; // 365 jours

function session_guard_cookie_params(): array
{
    $isHttps = (
        (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off')
        || ((string) ($_SERVER['SERVER_PORT'] ?? '') === '443')
    );

    return [
        'lifetime' => CHRONO_REMEMBER_LIFETIME,
        'path' => '/',
        'domain' => '',
        'secure' => $isHttps,
        'httponly' => true,
        'samesite' => 'Lax',
    ];
}

function session_guard_start_session(): void
{
    if (session_status() === PHP_SESSION_ACTIVE) {
        return;
    }

    $params = session_guard_cookie_params();
    session_set_cookie_params($params);
    session_start();
}

function session_guard_remember_file(): string
{
    return dirname(__DIR__) . '/privatedata/remember-tokens.json';
}

function session_guard_read_remember_tokens(): array
{
    $file = session_guard_remember_file();
    if (!is_file($file)) {
        return [];
    }

    $data = json_decode((string) file_get_contents($file), true);
    return is_array($data) ? $data : [];
}

function session_guard_write_remember_tokens(array $tokens): void
{
    file_put_contents(
        session_guard_remember_file(),
        json_encode($tokens, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
        LOCK_EX
    );
}

function session_guard_clear_remember_cookie(): void
{
    $params = session_guard_cookie_params();
    setcookie(CHRONO_REMEMBER_COOKIE, '', [
        'expires' => time() - 3600,
        'path' => $params['path'],
        'domain' => $params['domain'],
        'secure' => $params['secure'],
        'httponly' => $params['httponly'],
        'samesite' => $params['samesite'],
    ]);
    unset($_COOKIE[CHRONO_REMEMBER_COOKIE]);
}

function session_guard_cleanup_expired_tokens(array $tokens, int $now): array
{
    foreach ($tokens as $selector => $entry) {
        $expiresAt = (int) ($entry['expires_at'] ?? 0);
        if ($expiresAt <= 0 || $expiresAt < $now) {
            unset($tokens[$selector]);
        }
    }

    return $tokens;
}

function session_guard_issue_remember_me_token(string $clientId, string $clientName, string $accessEnd, string $giftCode = '', int $devicesRevision = 0): void
{
    session_guard_start_session();

    $selector = bin2hex(random_bytes(9));
    $validator = bin2hex(random_bytes(32));
    $expiresAt = time() + CHRONO_REMEMBER_LIFETIME;

    $tokens = session_guard_cleanup_expired_tokens(session_guard_read_remember_tokens(), time());
    $tokens[$selector] = [
        'validator_hash' => hash('sha256', $validator),
        'client_id' => $clientId,
        'client_name' => $clientName,
        'access_end' => $accessEnd,
        'gift_code' => $giftCode,
        'devices_revision' => $devicesRevision,
        'expires_at' => $expiresAt,
    ];
    session_guard_write_remember_tokens($tokens);

    $params = session_guard_cookie_params();
    setcookie(CHRONO_REMEMBER_COOKIE, $selector . ':' . $validator, [
        'expires' => $expiresAt,
        'path' => $params['path'],
        'domain' => $params['domain'],
        'secure' => $params['secure'],
        'httponly' => $params['httponly'],
        'samesite' => $params['samesite'],
    ]);

    $_COOKIE[CHRONO_REMEMBER_COOKIE] = $selector . ':' . $validator;
}

function session_guard_try_restore_from_remember_cookie(): void
{
    session_guard_start_session();

    if (!empty($_SESSION['logged_in'])) {
        return;
    }

    $cookie = trim((string) ($_COOKIE[CHRONO_REMEMBER_COOKIE] ?? ''));
    if ($cookie === '' || strpos($cookie, ':') === false) {
        return;
    }

    [$selector, $validator] = explode(':', $cookie, 2);
    if ($selector === '' || $validator === '') {
        session_guard_clear_remember_cookie();
        return;
    }

    $now = time();
    $tokens = session_guard_cleanup_expired_tokens(session_guard_read_remember_tokens(), $now);
    $entry = $tokens[$selector] ?? null;

    if (!is_array($entry)) {
        session_guard_write_remember_tokens($tokens);
        session_guard_clear_remember_cookie();
        return;
    }

    $expectedHash = (string) ($entry['validator_hash'] ?? '');
    if ($expectedHash === '' || !hash_equals($expectedHash, hash('sha256', $validator))) {
        unset($tokens[$selector]);
        session_guard_write_remember_tokens($tokens);
        session_guard_clear_remember_cookie();
        return;
    }

    $accessEnd = trim((string) ($entry['access_end'] ?? ''));
    if ($accessEnd !== '' && date('Y-m-d') > $accessEnd) {
        unset($tokens[$selector]);
        session_guard_write_remember_tokens($tokens);
        session_guard_clear_remember_cookie();
        return;
    }

    session_regenerate_id(true);
    $_SESSION['logged_in'] = true;
    $_SESSION['client_id'] = trim((string) ($entry['client_id'] ?? ''));
    $_SESSION['client_name'] = trim((string) ($entry['client_name'] ?? ''));
    $_SESSION['access_end'] = $accessEnd;
    $_SESSION['gift_code'] = trim((string) ($entry['gift_code'] ?? ''));
    $_SESSION['devices_revision'] = (int) ($entry['devices_revision'] ?? 0);

    // Rotation du token à chaque reconnexion automatique.
    unset($tokens[$selector]);
    session_guard_write_remember_tokens($tokens);
    session_guard_issue_remember_me_token(
        $_SESSION['client_id'],
        $_SESSION['client_name'],
        $_SESSION['access_end'],
        trim((string) ($_SESSION['gift_code'] ?? '')),
        (int) ($_SESSION['devices_revision'] ?? 0)
    );
}

function session_guard_revoke_remember_cookie_if_present(): void
{
    $cookie = trim((string) ($_COOKIE[CHRONO_REMEMBER_COOKIE] ?? ''));
    if ($cookie !== '' && strpos($cookie, ':') !== false) {
        [$selector] = explode(':', $cookie, 2);
        $tokens = session_guard_read_remember_tokens();
        if (isset($tokens[$selector])) {
            unset($tokens[$selector]);
            session_guard_write_remember_tokens($tokens);
        }
    }

    session_guard_clear_remember_cookie();
}

function session_guard_respond_and_exit(int $statusCode, array $payload): void
{
    http_response_code($statusCode);
    echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function session_guard_build_error_payload(string $type): array
{
    if ($type === 'not_authenticated') {
        return [
            'statusCode' => 401,
            'okPayload' => [
                'ok' => false,
                'message' => 'Accès non authentifié'
            ],
            'trackPayload' => [
                'status' => 'error',
                'message' => 'Accès non authentifié',
                'errors' => ['not_authenticated']
            ],
        ];
    }

    return [
        'statusCode' => 400,
        'okPayload' => [
            'ok' => false,
            'message' => 'Requête invalide'
        ],
        'trackPayload' => [
            'status' => 'error',
            'message' => 'Requête invalide',
            'errors' => ['invalid_request']
        ],
    ];
}

function session_guard_refuse(string $type, bool $trackFormat = false): void
{
    $error = session_guard_build_error_payload($type);
    session_guard_respond_and_exit(
        $error['statusCode'],
        $trackFormat ? $error['trackPayload'] : $error['okPayload']
    );
}



const CHRONO_DEVICE_COOKIE = 'chrono_party_device';
const CHRONO_MAX_AUTHORIZED_DEVICES = 2;

function session_guard_device_cookie_params(): array
{
    $params = session_guard_cookie_params();
    $params['httponly'] = true;
    return $params;
}

function session_guard_get_or_create_device_id(): string
{
    $deviceId = trim((string) ($_COOKIE[CHRONO_DEVICE_COOKIE] ?? ''));
    if ($deviceId === '' || !preg_match('/^[a-f0-9]{32}$/', $deviceId)) {
        $deviceId = bin2hex(random_bytes(16));
        $params = session_guard_device_cookie_params();
        setcookie(CHRONO_DEVICE_COOKIE, $deviceId, [
            'expires' => time() + CHRONO_REMEMBER_LIFETIME,
            'path' => $params['path'],
            'domain' => $params['domain'],
            'secure' => $params['secure'],
            'httponly' => $params['httponly'],
            'samesite' => $params['samesite'],
        ]);
        $_COOKIE[CHRONO_DEVICE_COOKIE] = $deviceId;
    }

    return $deviceId;
}

function session_guard_devices_file(): string
{
    return dirname(__DIR__) . '/privatedata/authorized-devices.json';
}

function session_guard_activation_attempts_file(): string
{
    return dirname(__DIR__) . '/privatedata/activation-attempts.json';
}

function session_guard_read_activation_attempts(): array
{
    $file = session_guard_activation_attempts_file();
    if (!is_file($file)) {
        return [];
    }
    $data = json_decode((string) file_get_contents($file), true);
    return is_array($data) ? $data : [];
}

function session_guard_write_activation_attempts(array $attempts): void
{
    file_put_contents(
        session_guard_activation_attempts_file(),
        json_encode($attempts, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
        LOCK_EX
    );
}

function session_guard_log_activation_attempt(string $accessKey, string $label, string $reason, int $used, int $max): void
{
    $attempts = session_guard_read_activation_attempts();
$now = time();

foreach (array_reverse($attempts) as $existing) {
    if (
        is_array($existing)
        && ($existing['access_key'] ?? '') === $accessKey
        && ($existing['reason'] ?? '') === $reason
        && ($existing['ip'] ?? '') === ($_SERVER['REMOTE_ADDR'] ?? '')
    ) {
        $existingTime = strtotime((string)($existing['created_at'] ?? ''));

        if ($existingTime && ($now - $existingTime) < 180) {
            return;
        }
    }
}
    $attempts[] = [
        'created_at' => date('c'),
        'access_key' => $accessKey,
        'code' => str_starts_with($accessKey, 'gift:') ? substr($accessKey, 5) : $accessKey,
        'label' => $label,
        'reason' => $reason,
        'used' => $used,
        'max' => $max,
        'browser' => session_guard_user_agent_label(),
        'ip' => (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
        'user_agent' => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''),
    ];

    // On garde seulement les 300 dernières tentatives pour éviter un fichier trop lourd.
    if (count($attempts) > 300) {
        $attempts = array_slice($attempts, -300);
    }

    session_guard_write_activation_attempts($attempts);
}

function session_guard_read_authorized_devices(): array
{
    $file = session_guard_devices_file();
    if (!is_file($file)) {
        return [];
    }
    $data = json_decode((string) file_get_contents($file), true);
    return is_array($data) ? $data : [];
}

function session_guard_write_authorized_devices(array $devices): void
{
    file_put_contents(
        session_guard_devices_file(),
        json_encode($devices, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
        LOCK_EX
    );
}

function session_guard_device_hash(): string
{
    return hash('sha256', session_guard_get_or_create_device_id());
}

function session_guard_user_agent_label(): string
{
    $ua = strtolower((string) ($_SERVER['HTTP_USER_AGENT'] ?? 'navigateur inconnu'));
    if (strpos($ua, 'firefox') !== false) return 'Firefox';
    if (strpos($ua, 'edg/') !== false || strpos($ua, 'edge') !== false) return 'Edge';
    if (strpos($ua, 'safari') !== false && strpos($ua, 'chrome') === false) return 'Safari';
    if (strpos($ua, 'chrome') !== false || strpos($ua, 'crios') !== false) return 'Chrome';
    return 'Navigateur';
}

function session_guard_authorize_current_device(string $accessKey, string $label = '', ?int $maxDevicesOverride = null): array
{
    $accessKey = trim($accessKey);
    if ($accessKey === '') {
        return ['ok' => false, 'reason' => 'missing_access_key'];
    }

    $now = time();
    $deviceHash = session_guard_device_hash();
    $all = session_guard_read_authorized_devices();

    if (!isset($all[$accessKey]) || !is_array($all[$accessKey])) {
        $all[$accessKey] = [
            'label' => $label,
            'max_devices' => CHRONO_MAX_AUTHORIZED_DEVICES,
            'devices' => [],
        ];
    }

    if (!isset($all[$accessKey]['devices']) || !is_array($all[$accessKey]['devices'])) {
        $all[$accessKey]['devices'] = [];
    }

    if ($maxDevicesOverride !== null && $maxDevicesOverride > 0) {
        $all[$accessKey]['max_devices'] = $maxDevicesOverride;
    }

    $maxDevices = (int) ($all[$accessKey]['max_devices'] ?? CHRONO_MAX_AUTHORIZED_DEVICES);
    if ($maxDevices <= 0) $maxDevices = CHRONO_MAX_AUTHORIZED_DEVICES;

    if (isset($all[$accessKey]['devices'][$deviceHash])) {
        $all[$accessKey]['devices'][$deviceHash]['last_seen_at'] = $now;
        session_guard_write_authorized_devices($all);
        return ['ok' => true, 'status' => 'existing', 'used' => count($all[$accessKey]['devices']), 'max' => $maxDevices];
    }

    if (count($all[$accessKey]['devices']) >= $maxDevices) {
        session_guard_log_activation_attempt($accessKey, $label, 'device_limit', count($all[$accessKey]['devices']), $maxDevices);
        return ['ok' => false, 'reason' => 'device_limit', 'used' => count($all[$accessKey]['devices']), 'max' => $maxDevices];
    }

    $all[$accessKey]['devices'][$deviceHash] = [
        'created_at' => $now,
        'last_seen_at' => $now,
        'browser' => session_guard_user_agent_label(),
        'ip_hash' => hash('sha256', (string) ($_SERVER['REMOTE_ADDR'] ?? '')),
    ];
    $all[$accessKey]['label'] = $label;
    session_guard_write_authorized_devices($all);

    return ['ok' => true, 'status' => 'added', 'used' => count($all[$accessKey]['devices']), 'max' => $maxDevices];
}

function session_guard_access_key_from_session(): string
{
    $gift = trim((string) ($_SESSION['gift_code'] ?? ''));
    if ($gift !== '') {
        return 'gift:' . $gift;
    }
    return trim((string) ($_SESSION['client_id'] ?? ''));
}

function session_guard_access_tokens_file(): string
{
    return dirname(__DIR__) . '/privatedata/access-tokens.json';
}

function session_guard_read_access_tokens(): array
{
    $file = session_guard_access_tokens_file();
    if (!is_file($file)) {
        return [];
    }
    $data = json_decode((string) file_get_contents($file), true);
    return is_array($data) ? $data : [];
}

function session_guard_find_gift_entry(string $gift): ?array
{
    $gift = strtoupper(trim($gift));
    foreach (session_guard_read_access_tokens() as $entry) {
        if (is_array($entry) && strtoupper((string) ($entry['gift'] ?? '')) === $gift) {
            return $entry;
        }
    }
    return null;
}

function session_guard_current_access_is_valid(): bool
{
    $gift = trim((string) ($_SESSION['gift_code'] ?? ''));
    if ($gift !== '') {
        $entry = session_guard_find_gift_entry($gift);
        if (!is_array($entry)) {
            return false;
        }
        $active = array_key_exists('active', $entry) ? (bool) $entry['active'] : true;
        if (!$active) {
            return false;
        }
        $accessEnd = trim((string) ($entry['access_end'] ?? ''));
        if ($accessEnd !== '' && date('Y-m-d') > $accessEnd) {
            return false;
        }

        $currentRevision = (int) ($entry['devices_revision'] ?? 0);
        $sessionRevision = (int) ($_SESSION['devices_revision'] ?? 0);
        if ($sessionRevision !== $currentRevision) {
            return false;
        }

        $_SESSION['access_end'] = $accessEnd;
        $_SESSION['client_name'] = trim((string) ($entry['label'] ?? $_SESSION['client_name'] ?? '')) ?: ('Code cadeau ' . $gift);
        return true;
    }

    $accessEnd = trim((string) ($_SESSION['access_end'] ?? ''));
    if ($accessEnd !== '' && date('Y-m-d') > $accessEnd) {
        return false;
    }

    return true;
}

function session_guard_refuse_expired_or_disabled(bool $json = false): void
{
    session_guard_revoke_remember_cookie_if_present();
    $_SESSION = [];

    if ($json) {
        session_guard_respond_and_exit(403, [
            'ok' => false,
            'message' => 'Accès expiré ou désactivé.',
            'errors' => ['access_expired_or_disabled']
        ]);
    }

    http_response_code(403);
    echo '<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Accès refusé - Chrono Party</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#120b2f;color:#fff;font-family:Arial,Helvetica,sans-serif}.box{max-width:520px;margin:20px;padding:28px;border-radius:24px;background:rgba(255,255,255,.08);box-shadow:0 20px 80px rgba(0,0,0,.35);text-align:center}h1{font-size:26px;margin:0 0 12px}p{line-height:1.5;color:rgba(255,255,255,.82)}a{display:inline-block;margin-top:14px;padding:12px 18px;border-radius:999px;background:#7c5cff;color:white;text-decoration:none;font-weight:700}</style></head><body><div class="box"><h1>Accès refusé</h1><p>Cet accès Chrono Party est expiré ou désactivé.</p><a href="./activation/">Retour</a></div></body></html>';
    exit;
}

function session_guard_device_limit_message_html(): string
{
    return '<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Limite atteinte - Chrono Party</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#120b2f;color:#fff;font-family:Arial,Helvetica,sans-serif}.box{max-width:520px;margin:20px;padding:28px;border-radius:24px;background:rgba(255,255,255,.08);box-shadow:0 20px 80px rgba(0,0,0,.35);text-align:center}.badge{font-size:42px;margin-bottom:10px}h1{font-size:26px;margin:0 0 12px}p{line-height:1.5;color:rgba(255,255,255,.82)}a{display:inline-block;margin-top:14px;padding:12px 18px;border-radius:999px;background:#7c5cff;color:white;text-decoration:none;font-weight:700}</style></head><body><div class="box"><div class="badge">🔒</div><h1>Limite d’appareils atteinte</h1><p>Ce code est déjà activé sur 2 navigateurs/appareils.</p><p>Pour protéger la licence Chrono Party, aucun nouvel appareil ne peut être ajouté.</p><p>Si vous pensez qu’il s’agit d’une erreur, contactez l’organisateur.</p><a href="./activation/">Retour</a></div></body></html>';
}

function session_guard_enforce_current_device_or_exit(bool $json = false): void
{
    $accessKey = session_guard_access_key_from_session();
    $label = trim((string) ($_SESSION['client_name'] ?? 'Chrono Party'));
    $maxDevicesOverride = null;
    $gift = trim((string) ($_SESSION['gift_code'] ?? ''));
    if ($gift !== '') {
        $entry = session_guard_find_gift_entry($gift);
        if (is_array($entry)) {
            $candidateMax = (int) ($entry['max_devices'] ?? 0);
            if ($candidateMax > 0) {
                $maxDevicesOverride = $candidateMax;
            }
        }
    }
    $result = session_guard_authorize_current_device($accessKey, $label, $maxDevicesOverride);
    if (!empty($result['ok'])) {
        $_SESSION['device_status'] = $result['status'] ?? 'existing';
        $_SESSION['device_used'] = $result['used'] ?? 1;
        $_SESSION['device_max'] = $result['max'] ?? CHRONO_MAX_AUTHORIZED_DEVICES;
        return;
    }

    session_guard_revoke_remember_cookie_if_present();
    $_SESSION = [];
    if ($json) {
        session_guard_respond_and_exit(403, [
            'ok' => false,
            'message' => 'Limite d’appareils atteinte. Ce code est déjà activé sur 2 navigateurs/appareils.',
            'errors' => ['device_limit']
        ]);
    }

    http_response_code(403);
    echo session_guard_device_limit_message_html();
    exit;
}

function register_active_client_session(string $clientId): void
{
    // volontairement vide
}

function require_active_client_session(bool $trackFormat = false): array
{
    session_guard_start_session();
    session_guard_try_restore_from_remember_cookie();

    if (empty($_SESSION['logged_in'])) {
        session_guard_refuse('not_authenticated', $trackFormat);
    }

    $clientId = trim((string) ($_SESSION['client_id'] ?? ''));
    if ($clientId === '') {
        session_guard_refuse('not_authenticated', $trackFormat);
    }

    if (!session_guard_current_access_is_valid()) {
        session_guard_refuse_expired_or_disabled(!$trackFormat ? false : true);
    }

    session_guard_enforce_current_device_or_exit(!$trackFormat ? false : true);

    return [
        'client_id' => $clientId,
        'client_name' => trim((string) ($_SESSION['client_name'] ?? '')),
        'access_end' => trim((string) ($_SESSION['access_end'] ?? '')),
        'session_id' => session_id(),
    ];
}

function release_active_client_session_if_current(): void
{
    session_guard_revoke_remember_cookie_if_present();
}
