<?php
declare(strict_types=1);

namespace PSLauncher\Routes;

use PSLauncher\Crypto;
use PSLauncher\Db;
use PSLauncher\Response;

final class ValidateLicense
{
    public static function handle(array $config): void
    {
        // Rate limit basique par IP
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
        self::enforceRateLimit($config, $ip);

        $body = json_decode(file_get_contents('php://input') ?: '[]', true) ?? [];
        $licenseKey = trim((string)($body['licenseKey'] ?? ''));
        $machineId  = trim((string)($body['machineId'] ?? ''));
        $machineLabel = trim((string)($body['machineLabel'] ?? ''));
        $launcherVer = trim((string)($body['launcherVersion'] ?? ''));

        if ($licenseKey === '' || $machineId === '') {
            Response::error('invalid_request', 'licenseKey et machineId sont requis', 400);
        }

        $db = Db::get($config);
        $now = (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s');

        $stmt = $db->prepare(
            'SELECT id, license_key, owner_name, issued_at, download_entitlement_until,
                    max_machines, revoked_at
             FROM licenses WHERE license_key = ? LIMIT 1'
        );
        $stmt->execute([$licenseKey]);
        $lic = $stmt->fetch();

        if (!$lic) {
            self::audit($db, null, $ip, 'validate_invalid', ['key_prefix' => substr($licenseKey, 0, 6)]);
            Response::error('invalid', 'Clé de license invalide', 401);
        }
        if ($lic['revoked_at'] !== null) {
            self::audit($db, (int)$lic['id'], $ip, 'validate_revoked', []);
            Response::error('revoked', 'License révoquée', 403);
        }

        // Slot machine
        $slotStmt = $db->prepare(
            'SELECT id FROM license_machines WHERE license_id = ? AND machine_id = ? LIMIT 1'
        );
        $slotStmt->execute([$lic['id'], $machineId]);
        $existingSlot = $slotStmt->fetch();

        if ($existingSlot) {
            $db->prepare('UPDATE license_machines SET last_seen = ? WHERE id = ?')
               ->execute([$now, $existingSlot['id']]);
        } else {
            $countStmt = $db->prepare('SELECT COUNT(*) AS c FROM license_machines WHERE license_id = ?');
            $countStmt->execute([$lic['id']]);
            $count = (int)($countStmt->fetch()['c'] ?? 0);
            if ($count >= (int)$lic['max_machines']) {
                self::audit($db, (int)$lic['id'], $ip, 'machine_limit', ['count' => $count]);
                Response::error('machine_limit_exceeded',
                    "Cette license autorise {$lic['max_machines']} machine(s) ; toutes les places sont prises.",
                    403, ['ownerName' => $lic['owner_name']]);
            }
            $db->prepare(
                'INSERT INTO license_machines (license_id, machine_id, machine_label, first_seen, last_seen)
                 VALUES (?, ?, ?, ?, ?)'
            )->execute([$lic['id'], $machineId, $machineLabel ?: null, $now, $now]);
        }

        // Construit la réponse, signe-la, renvoie.
        // Tous les timestamps sont émis en UTC + format figé "yyyy-MM-ddTHH:mm:ssZ"
        // pour que la canonicalisation côté client produise EXACTEMENT les mêmes bytes.
        $utc = new \DateTimeZone('UTC');
        $fmt = 'Y-m-d\TH:i:s\Z';

        $issuedAt = (new \DateTimeImmutable($lic['issued_at'], $utc))->format($fmt);
        $entUntil = (new \DateTimeImmutable($lic['download_entitlement_until'], $utc))->format($fmt);
        $serverTime = (new \DateTimeImmutable('now', $utc))->format($fmt);
        $expired = strtotime($lic['download_entitlement_until']) < time();

        $payload = [
            'status'                     => $expired ? 'expired' : 'valid',
            'licenseId'                  => 'lic_' . $lic['id'],
            'ownerName'                  => $lic['owner_name'],
            'issuedAt'                   => $issuedAt,
            'downloadEntitlementUntil'   => $entUntil,
            'maxMachines'                => (int)$lic['max_machines'],
            'serverTime'                 => $serverTime,
        ];

        // Signature Ed25519 du payload canonical
        $sk = $config['ed25519']['private_key_hex'] ?? '';
        if ($sk !== '') {
            $payload['signature'] = Crypto::signEd25519(Crypto::canonicalJson($payload), $sk);
        }

        self::audit($db, (int)$lic['id'], $ip, $expired ? 'validate_expired' : 'validate_ok', [
            'launcher_version' => $launcherVer,
        ]);

        Response::json($payload, $expired ? 200 : 200);
    }

    private static function enforceRateLimit(array $config, string $ip): void
    {
        $max = (int)($config['validate_max_per_minute_per_ip'] ?? 10);
        if ($max <= 0) return;

        $db = Db::get($config);
        $now = new \DateTimeImmutable('now');
        $window = $now->modify('-1 minute')->format('Y-m-d H:i:s');

        $stmt = $db->prepare('SELECT counter, window_start FROM rate_limit WHERE ip = ?');
        $stmt->execute([$ip]);
        $row = $stmt->fetch();

        if ($row === false || $row['window_start'] < $window) {
            $db->prepare('REPLACE INTO rate_limit (ip, window_start, counter) VALUES (?, ?, 1)')
               ->execute([$ip, $now->format('Y-m-d H:i:s')]);
            return;
        }

        if ((int)$row['counter'] >= $max) {
            Response::error('rate_limited', 'Trop de tentatives. Réessaie dans 1 minute.', 429);
        }
        $db->prepare('UPDATE rate_limit SET counter = counter + 1 WHERE ip = ?')->execute([$ip]);
    }

    private static function audit(\PDO $db, ?int $licenseId, string $ip, string $event, array $detail): void
    {
        try {
            $db->prepare(
                'INSERT INTO audit_log (ts, license_id, ip, event, detail) VALUES (NOW(), ?, ?, ?, ?)'
            )->execute([$licenseId, $ip, $event, json_encode($detail, JSON_UNESCAPED_UNICODE)]);
        } catch (\Throwable) { /* best-effort */ }
    }
}
