<?php
/**
 * Projet                    : MageStock
 * Logiciel                  : MageStock
 * Fichier                   : api/update.php
 * Rôle du fichier           : API administrateur du moteur sécurisé, de la découverte distante et du planificateur cumulatif
 * Auteur                    : Régis KREMER
 * Version du fichier        : 2.6.120-client1
 * Version générale MageStock: 2.6.120
 * Lot                       : MAGEKORP MAGESTOCK SIGNED UPDATE CLIENT INTEGRATION1
 * Base cumulative requise   : MageStock 2.6.120
 * Date de création          : 2026-07-21
 * Date de modification      : 2026-07-25
 * Dépendances               : config.php, includes/update-helper.php, includes/update-rollup.php, includes/update-discovery.php
 * Données persistantes      : état, historique, transaction UPDATE et cache de découverte distante
 * Permissions requises      : Administrateur et update.manage
 * Contraintes de sécurité   : session, CSRF sur écriture, aucune exposition des chemins privés
 * Compatibilité             : PHP 8.1 à 8.4
 *
 * Résumé des modifications :
 * - expose la découverte distante MageKorp et son cache limité à douze heures ;
 * - télécharge uniquement l’artefact sélectionné depuis l’origine officielle épinglée ;
 * - conserve le calcul de parcours, l’import manuel, le précontrôle, l’installation, la reprise et le rollback.
 *
 * Historique :
 * - 2.6.110 — API du moteur UPDATE signé et précontrôle du système de fichiers.
 * - 2.6.117 — métadonnées cumulatives et planificateur de parcours.
 * - 2.6.118 — découverte distante signée, téléchargement contrôlé et remise au moteur interne.
 * - 2.6.120-client1 — déclaration du contrat v2, signature publique d’artefact et repli v1.
 */
require_once dirname(__DIR__) . '/config.php';
startSession();
requireAdmin();
requirePerm('update.manage');

function updatePublicAnalysis(array $analysis): array {
    return [
        'packageType'=>$analysis['packageType'] ?? null,
        'packageMode'=>$analysis['packageMode'] ?? null,
        'schemaVersion'=>$analysis['schemaVersion'] ?? null,
        'targetVersion'=>$analysis['targetVersion'] ?? null,
        'installedVersion'=>$analysis['installedVersion'] ?? null,
        'minimumSourceVersion'=>$analysis['minimumSourceVersion'] ?? null,
        'maximumSourceVersion'=>$analysis['maximumSourceVersion'] ?? null,
        'migrationBaseline'=>$analysis['migrationBaseline'] ?? null,
        'migrationCount'=>is_array($analysis['migrations'] ?? null) ? count($analysis['migrations']) : 0,
        'fileCount'=>$analysis['fileCount'] ?? null,
        'archiveSize'=>$analysis['archiveSize'] ?? null,
        'expandedSize'=>$analysis['expandedSize'] ?? null,
        'keyId'=>$analysis['keyId'] ?? null,
        'packageSha256'=>$analysis['packageSha256'] ?? null,
        'preflight'=>isset($analysis['preflight']) && is_array($analysis['preflight']) ? $analysis['preflight'] : null,
    ];
}
function updatePublicTransaction(array $tx): array {
    $packagePath = trim((string)($tx['packagePath'] ?? ''));
    $rollbackDir = trim((string)($tx['rollbackDir'] ?? ''));
    $tx['packageAvailable'] = $packagePath !== '' && is_file($packagePath);
    $tx['rollbackAvailable'] = $rollbackDir !== '' && is_dir($rollbackDir);
    $tx['orphanRecoverable'] = (string)($tx['status'] ?? '') === 'rollback_required'
        && !$tx['packageAvailable']
        && !$tx['rollbackAvailable'];
    unset($tx['packagePath'], $tx['rollbackDir']);
    if (isset($tx['analysis']) && is_array($tx['analysis'])) $tx['analysis'] = updatePublicAnalysis($tx['analysis']);
    return $tx;
}
function updatePublicResult(array $result): array {
    if (isset($result['transaction']) && is_array($result['transaction'])) $result['transaction'] = updatePublicTransaction($result['transaction']);
    if (isset($result['analysis']) && is_array($result['analysis'])) $result['analysis'] = updatePublicAnalysis($result['analysis']);
    return $result;
}
function updateJsonBody(): array {
    $raw = (string)file_get_contents('php://input');
    if (trim($raw) === '') return [];
    $body = json_decode($raw, true);
    if (!is_array($body)) throw new MageStockUpdateException('Corps JSON invalide.', 'json_invalid');
    return $body;
}

$method = $_SERVER['REQUEST_METHOD'];
$action = trim((string)($_GET['action'] ?? 'status'));
try {
    if ($method === 'GET') {
        $status = magestockUpdateStatus();
        $status['transaction'] = updatePublicTransaction($status['transaction'] ?? []);
        $release = $status['release'] ?? [];
        $status['release'] = [
            'application'=>$release['application'] ?? null,
            'version'=>$release['version'] ?? null,
            'fromVersion'=>$release['fromVersion'] ?? null,
            'minimumUpgradeFrom'=>$release['minimumUpgradeFrom'] ?? null,
            'keyId'=>$release['keyId'] ?? null,
            'generatedAt'=>$release['generatedAt'] ?? null,
        ];
        $status['rollupCapabilities'] = [
            'manifestSchemaVersions'=>[1,2],
            'packageModes'=>['exact','cumulative','bridge'],
            'inclusiveSourceRanges'=>true,
            'conditionalMigrations'=>true,
            'shortestPathPlanner'=>true,
            'automaticMultiPackageApply'=>false,
        ];
        $status['discovery'] = magestockDiscoveryPublicCache();
        $status['discoveryCapabilities'] = [
            'provider'=>'MageKorp Hub',
            'channel'=>MAGESTOCK_DISCOVERY_CHANNEL,
            'httpsOnly'=>true,
            'pinnedOrigin'=>MAGESTOCK_DISCOVERY_ORIGIN,
            'pinnedKeyId'=>(string)(magestockUpdatePublicKey()['keyId'] ?? ''),
            'automaticIntervalSeconds'=>MAGESTOCK_DISCOVERY_CACHE_TTL,
            'preferredContract'=>'v2',
            'contracts'=>[
                'magekorp.update-check.v2',
                'magekorp.release-manifest.v2',
                'magekorp.artifact-signature-payload.v1',
                'magekorp.update-check.v1',
                'magekorp.release-manifest.v1',
            ],
            'v1Fallback'=>true,
            'publicArtifactSignature'=>true,
            'internalPackageSignature'=>true,
            'automaticInstall'=>false,
            'manualImportPreserved'=>true,
        ];
        jsonResponse($status);
    }
    csrfCheck();
    $actor = currentUser();
    if ($action === 'discover') {
        $body = updateJsonBody();
        $force = (bool)($body['force'] ?? false);
        $result = magestockDiscoveryCheck($force);
        unset($result['artifactUrl'], $result['manifestUrl'], $result['signatureUrl'], $result['trustStoreUrl']);
        jsonResponse(['discovery'=>$result,'cache'=>magestockDiscoveryPublicCache()]);
    }
    if ($action === 'downloadRemote') {
        jsonResponse(updatePublicResult(magestockDiscoveryDownload($actor)));
    }
    if ($action === 'upload') {
        if (empty($_FILES['package']) || !is_array($_FILES['package'])) jsonError('Aucun ZIP reçu.', 422);
        jsonResponse(updatePublicResult(magestockUpdateStoreUpload($_FILES['package'], $actor)));
    }
    if ($action === 'plan') {
        $body = updateJsonBody();
        $state = magestockUpdateState();
        $installed = (string)($state['installedVersion'] ?? APP_VER);
        $target = (string)($body['targetVersion'] ?? '');
        $packages = is_array($body['packages'] ?? null) ? $body['packages'] : [];
        jsonResponse(['plan'=>magestockUpdatePlanShortestPath($installed, $target, $packages)]);
    }
    if ($action === 'preflight') jsonResponse(updatePublicResult(magestockUpdateRecheckPreflight($actor)));
    if ($action === 'apply') jsonResponse(updatePublicResult(magestockUpdateApplyVerified($actor)));
    if ($action === 'resume') jsonResponse(updatePublicResult(magestockUpdateResume($actor)));
    if ($action === 'rollback') jsonResponse(updatePublicResult(magestockUpdateRollbackCurrent()));
    if ($action === 'finalizeManual') jsonResponse(magestockUpdateFinalizeManual($actor));
    if ($action === 'integrity') jsonResponse(magestockUpdateIntegrity());
    jsonError('Action de mise à jour inconnue.', 404);
} catch (MageStockUpdateException $e) {
    $payload = ['error'=>$e->getMessage(),'code'=>$e->reasonCode];
    if ($e->details) $payload['details'] = $e->details;
    jsonResponse($payload, $e->httpStatus);
} catch (Throwable $e) {
    error_log('[MageStock Update] ' . $e->getMessage());
    jsonError('La mise à jour a échoué. Consultez le diagnostic administrateur.', 500);
}
