<?php
// Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
// API que recebe heartbeats dos BIs clientes (Real Prime, Importados, etc).
// Cada BI manda POST com status do ultimo backup.
//
// Instalar: copiar este arquivo + index.php pra C:\Fire-Updates\public\painel-backups\
// no servidor 24h. Acessar via http://177.23.184.123:8084/painel-backups/

header('Content-Type: application/json; charset=utf-8');

$cfgFile = __DIR__ . '/config.php';
if (!is_file($cfgFile)) {
    http_response_code(500);
    echo json_encode(['ok'=>false, 'erro'=>'config.php não criado no servidor']);
    exit;
}
$cfg = include $cfgFile;

// Valida token compartilhado
$tokenRecebido = $_SERVER['HTTP_X_TOKEN'] ?? ($_POST['token'] ?? '');
if ($tokenRecebido !== ($cfg['token'] ?? '')) {
    http_response_code(403);
    echo json_encode(['ok'=>false, 'erro'=>'token inválido']);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['ok'=>false, 'erro'=>'use POST']);
    exit;
}

// SELF-UPDATE (v10+): tratado ANTES da validação JSON porque vem via multipart,
// não JSON. Uso: curl -H "X-Token:..." -F "tipo=self_update" -F "zip=@painel.zip" URL
if (($_POST['tipo'] ?? '') === 'self_update' || ($_GET['tipo'] ?? '') === 'self_update') {
    if (empty($_FILES['zip']['tmp_name']) || !is_uploaded_file($_FILES['zip']['tmp_name'])) {
        http_response_code(400);
        echo json_encode(['ok'=>false, 'erro'=>'Faltou arquivo .zip no campo "zip"']);
        exit;
    }
    $preservar = ['config.php', 'emails.php'];  // configs vivos — nunca sobrescrever

    // Estrategia 1: ZipArchive nativo (rapido, requer php_zip)
    $extraidos = [];
    $ignorados = [];
    if (class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($_FILES['zip']['tmp_name']) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $nome = $zip->getNameIndex($i);
                if ($nome === false || substr($nome, -1) === '/') continue;
                $base = basename($nome);
                if (in_array($base, $preservar, true) && is_file(__DIR__ . '/' . $base)) {
                    $ignorados[] = $nome; continue;
                }
                $conteudo = $zip->getFromIndex($i);
                if ($conteudo !== false && @file_put_contents(__DIR__ . '/' . $base, $conteudo) !== false) {
                    $extraidos[] = $nome;
                }
            }
            $zip->close();
            if (function_exists('opcache_reset')) @opcache_reset();
            echo json_encode(['ok'=>true, 'via'=>'ZipArchive', 'extraidos'=>$extraidos, 'preservados'=>$ignorados]);
            exit;
        }
    }

    // Estrategia 2: PowerShell Expand-Archive (fallback, sempre disponivel no Win)
    $zipTmp = $_FILES['zip']['tmp_name'] . '.zip';
    if (!@rename($_FILES['zip']['tmp_name'], $zipTmp)) $zipTmp = $_FILES['zip']['tmp_name'];
    $destDir = sys_get_temp_dir() . '/painel_deploy_' . bin2hex(random_bytes(4));
    @mkdir($destDir, 0777, true);
    $cmd = 'powershell -NoProfile -ExecutionPolicy Bypass -Command '
         . '"Expand-Archive -LiteralPath ' . escapeshellarg($zipTmp) . ' -DestinationPath ' . escapeshellarg($destDir) . ' -Force"';
    @exec($cmd . ' 2>&1', $psOut, $psRc);
    if ($psRc !== 0) {
        http_response_code(500);
        echo json_encode(['ok'=>false, 'erro'=>'Extracao falhou. Nem ZipArchive nem PowerShell disponiveis.', 'ps_saida'=>$psOut, 'ps_rc'=>$psRc]);
        exit;
    }
    // Copia arquivos extraidos pra pasta do painel
    foreach (scandir($destDir) ?: [] as $f) {
        if ($f === '.' || $f === '..') continue;
        $srcFile = $destDir . '/' . $f;
        if (!is_file($srcFile)) continue;
        if (in_array($f, $preservar, true) && is_file(__DIR__ . '/' . $f)) {
            $ignorados[] = $f; continue;
        }
        if (@copy($srcFile, __DIR__ . '/' . $f)) $extraidos[] = $f;
    }
    // Limpeza
    foreach (glob($destDir . '/*') ?: [] as $f) @unlink($f);
    @rmdir($destDir);
    @unlink($zipTmp);
    if (function_exists('opcache_reset')) @opcache_reset();
    echo json_encode(['ok'=>true, 'via'=>'PowerShell', 'extraidos'=>$extraidos, 'preservados'=>$ignorados]);
    exit;
}

// FIDELIZA_INSTALL / FIDELIZA_UPDATE — deploy do backend Fideliza no servidor 24h.
// Uso:
//   curl -H "X-Token:..." -F "tipo=fideliza_install" -F "zip=@fideliza-backend.zip" URL
// Instalação zero-touch:
//   1. Extrai ZIP pra C:\Fideliza\backend\
//   2. Mata php.exe rodando na porta 8086 (se houver)
//   3. Sobe novo php built-in server em 0.0.0.0:8086 via start /B (fica de pé)
//   4. Adiciona regra firewall pra 8086 (se admin permitir)
//   5. Testa /health e retorna estado
$tipoUpload = $_POST['tipo'] ?? $_GET['tipo'] ?? '';
if ($tipoUpload === 'fideliza_install' || $tipoUpload === 'fideliza_update') {
    if (empty($_FILES['zip']['tmp_name']) || !is_uploaded_file($_FILES['zip']['tmp_name'])) {
        http_response_code(400);
        echo json_encode(['ok'=>false, 'erro'=>'Faltou arquivo .zip no campo "zip"']);
        exit;
    }

    $destRoot = 'C:\\CashVolta';
    $destBackend = $destRoot . '\\backend';

    // Migração rebrand 2026-07-04: se já existir C:\Fideliza\backend\storage\
    // com dados (versão antiga do produto), copia pra C:\CashVolta\backend\storage\
    // na primeira instalação — depois disso ignora.
    $legacyStorage = 'C:\\Fideliza\\backend\\storage';
    if (!is_dir($destBackend . '\\storage') && is_dir($legacyStorage)) {
        @mkdir($destBackend, 0777, true);
        @exec('powershell -NoProfile -Command "Copy-Item -Path ' . escapeshellarg($legacyStorage) . ' -Destination ' . escapeshellarg($destBackend . '\\storage') . ' -Recurse -Force"');
    }

    // Preserva SQLite entre updates (fideliza_update); fideliza_install do zero
    // apaga tudo (mas preserva storage se já existir por segurança).
    $preservarStorage = ($tipoUpload === 'fideliza_update');
    $storageBackup = null;
    if ($preservarStorage && is_dir($destBackend . '\\storage')) {
        $storageBackup = sys_get_temp_dir() . '/cashvolta_storage_bkp_' . bin2hex(random_bytes(4));
        @mkdir($storageBackup, 0777, true);
        // Move storage pra tmp — depois de extrair, restaura por cima
        @rename($destBackend . '\\storage', $storageBackup . '\\storage');
    }

    // Cria pasta destino se não existe
    if (!is_dir($destBackend)) @mkdir($destBackend, 0777, true);

    // Extrai via PowerShell (mais confiável em ambiente Windows / ZIP grande)
    $zipTmp = $_FILES['zip']['tmp_name'] . '.zip';
    if (!@rename($_FILES['zip']['tmp_name'], $zipTmp)) $zipTmp = $_FILES['zip']['tmp_name'];

    $cmdExtract = 'powershell -NoProfile -ExecutionPolicy Bypass -Command '
        . '"Expand-Archive -LiteralPath ' . escapeshellarg($zipTmp) . ' -DestinationPath ' . escapeshellarg($destBackend) . ' -Force"';
    @exec($cmdExtract . ' 2>&1', $exOut, $exRc);
    @unlink($zipTmp);
    if ($exRc !== 0) {
        http_response_code(500);
        echo json_encode(['ok'=>false, 'erro'=>'Falha ao extrair ZIP', 'saida'=>$exOut, 'rc'=>$exRc]);
        exit;
    }

    // Restaura storage se era update
    if ($storageBackup && is_dir($storageBackup . '\\storage')) {
        // Remove storage recém-extraído (semente) e coloca o antigo
        @exec('powershell -NoProfile -Command "Remove-Item -Recurse -Force ' . escapeshellarg($destBackend . '\\storage') . ' -ErrorAction SilentlyContinue"');
        @rename($storageBackup . '\\storage', $destBackend . '\\storage');
        @rmdir($storageBackup);
    }

    // Mata PHP antigo em 8086 (best-effort — se falhar, tudo bem)
    $cmdKill = 'powershell -NoProfile -Command "'
        . 'Get-NetTCPConnection -LocalPort 8086 -State Listen -EA SilentlyContinue | '
        . 'ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -EA SilentlyContinue }"';
    @exec($cmdKill . ' 2>&1');
    // Aguarda 1s pro socket liberar
    usleep(1000000);

    // Descobre PHP local (do próprio painel central)
    $phpExe = PHP_BINARY;
    if (!is_file($phpExe)) $phpExe = 'php'; // fallback do PATH

    // Sobe PHP built-in server em background — usa VBS Shell.Run pra rodar sem
    // janela e sem travar o request PHP atual.
    $pubDir = $destBackend . '\\public';
    $indexPhp = $pubDir . '\\index.php';
    if (!is_file($indexPhp)) {
        http_response_code(500);
        echo json_encode(['ok'=>false, 'erro'=>'index.php nao achado em ' . $pubDir]);
        exit;
    }

    // Log do serviço PHP fica em C:\Fideliza\backend\storage\logs\
    $logDir = $destBackend . '\\storage\\logs';
    if (!is_dir($logDir)) @mkdir($logDir, 0777, true);
    $logFile = $logDir . '\\php-server.log';

    // Habilita extensões pdo_sqlite + sqlite3 via -d (não mexe no php.ini global do servidor)
    $extDir = dirname($phpExe) . '\\ext';
    $phpArgs = [
        '-d', 'extension_dir=' . $extDir,
        '-d', 'extension=pdo_sqlite',
        '-d', 'extension=sqlite3',
        '-S', '0.0.0.0:8086',
        '-t', $pubDir,
        $indexPhp,
    ];

    // Gera arquivo .cmd que sobe o servidor PHP
    $cmdFile = $destRoot . '\\start-cashvolta-server.cmd';
    $cmdContent = "@echo off\r\n"
        . "REM Auto-gerado — sobe o backend CashVolta\r\n"
        . '"' . $phpExe . '"';
    foreach ($phpArgs as $arg) {
        $cmdContent .= ' "' . $arg . '"';
    }
    $cmdContent .= ' >> "' . $logFile . '" 2>&1' . "\r\n";
    @file_put_contents($cmdFile, $cmdContent);

    // ESTRATÉGIA 1: WScript.Shell via VBS (não precisa admin, processo detached)
    // Gera um VBS que dispara o .cmd em background e some.
    $vbsFile = $destRoot . '\\start-cashvolta.vbs';
    $vbsContent = 'Set WshShell = CreateObject("WScript.Shell")' . "\r\n"
        . 'WshShell.Run """' . $cmdFile . '""", 0, False' . "\r\n";
    @file_put_contents($vbsFile, $vbsContent);
    @exec('wscript.exe "' . $vbsFile . '"', $vOut, $vRc);
    $viaVbs = ($vRc === 0);

    // Limpeza rebrand — remove artefatos antigos "Fideliza" caso existam
    @exec('schtasks /Delete /TN "FidelizaServer" /F 2>&1');
    @exec('netsh advfirewall firewall delete rule name="Fideliza 8086" 2>&1');

    // ESTRATÉGIA 2 (fallback se vbs falhar): schtasks CashVoltaServer
    $tRc = -1; $rRc = -1;
    if (!$viaVbs) {
        @exec('schtasks /Delete /TN "CashVoltaServer" /F 2>&1');
        @exec('schtasks /Create /TN "CashVoltaServer" /TR ' . escapeshellarg($cmdFile) . ' /SC ONLOGON /F 2>&1', $tOut, $tRc);
        @exec('schtasks /Run /TN "CashVoltaServer" 2>&1', $rOut, $rRc);
    }

    // ESTRATÉGIA 3 (fallback final): popen com bin nul redirect — processo
    // fica preso ao PHP atual mas roda o suficiente pra confirmar env.
    // (só executa se as duas anteriores falharam)
    $viaPopen = false;
    if (!$viaVbs && $rRc !== 0) {
        $h = @popen('start /B "" "' . $cmdFile . '"', 'r');
        if ($h) { pclose($h); $viaPopen = true; }
    }

    // Aguarda 3s pra PHP subir
    usleep(3000000);

    // Firewall (best-effort — só funciona se painel central roda como admin)
    @exec('netsh advfirewall firewall delete rule name="CashVolta 8086" 2>&1');
    @exec('netsh advfirewall firewall add rule name="CashVolta 8086" dir=in action=allow protocol=TCP localport=8086 2>&1', $fOut, $fRc);

    // Landing + proxy unificado em painel-backups/cashvolta/index.php.
    // Todas as URLs ficam sob /cashvolta/* pra o mundo externo.
    // Pasta física fica em painel-backups/cashvolta/ (novo, rebrand 2026-07-04).
    // Codename interno "fideliza" (código, endpoints install/update) mantido.
    //   /cashvolta/                → landing (escolhe painel)
    //   /cashvolta/master/login    → proxy pra localhost:8086/master/login
    //   /cashvolta/api/tablet/...  → proxy pra localhost:8086/api/tablet/...
    $landingDir = __DIR__ . '/cashvolta';
    if (!is_dir($landingDir)) @mkdir($landingDir, 0777, true);

    // FallbackResource — faz TODAS as requests sob /cashvolta/* que não achem
    // arquivo real caírem no index.php do proxy. Sem isso, Apache retorna 404
    // pra URLs com extensão (.css, .js, .png).
    @file_put_contents($landingDir . '/.htaccess',
        "FallbackResource /cashvolta/index.php\n" .
        "Options -Indexes\n"
    );
    $landingPhp = <<<'LANDING_PHP'
<?php
/*
 * Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
 * CashVolta — Landing + proxy unificado.
 * Prefixo público /cashvolta/* → backend em localhost:8086.
 */
$prefixo = '/cashvolta';
$path = ltrim((string)($_SERVER['PATH_INFO'] ?? ''), '/');

/* Landing quando path vazio */
if ($path === '') {
    ?><!doctype html>
    <html lang="pt-BR"><head><meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>CashVolta</title>
    <style>body{font-family:system-ui;background:linear-gradient(160deg,#0f172a,#1e1b4b);color:#f1f5f9;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:20px}
    .box{max-width:480px;width:100%;text-align:center}
    .logo{font-size:4rem;margin-bottom:.5rem}h1{color:#f59e0b;font-size:2.5rem;margin:0 0 .3rem}
    .sub{color:#94a3b8;margin-bottom:2.5rem}
    .card{display:block;background:rgba(30,41,59,.85);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:1.4rem 1.5rem;margin-bottom:1rem;text-decoration:none;color:#f1f5f9}
    .card:hover{border-color:#f59e0b}
    .card .titulo{font-size:1.2rem;font-weight:700;color:#f59e0b;margin-bottom:.3rem}
    .card .desc{color:#94a3b8;font-size:.9rem}
    .rodape{color:rgba(148,163,184,.4);font-size:.75rem;margin-top:2rem}</style>
    </head><body><div class="box">
    <div class="logo">💵</div><h1>CashVolta</h1>
    <div class="sub">Cashback que traz o cliente de volta</div>
    <a href="/cashvolta/master/login" class="card">
      <div class="titulo">🎛️ Painel Master</div>
      <div class="desc">Gerenciar lojistas, criar acessos, gerar chaves de tablet</div></a>
    <a href="/cashvolta/lojista/login" class="card">
      <div class="titulo">🏪 Painel do Lojista</div>
      <div class="desc">Dashboard executivo, clientes, configurações do programa</div></a>
    <div class="rodape">Depto de Engenharia e Automação</div>
    </div></body></html>
    <?php
    exit;
}

/* Proxy pra localhost:8086/$path */
$qs = $_GET;
$target = 'http://127.0.0.1:8086/' . $path;
if (!empty($qs)) $target .= '?' . http_build_query($qs);

$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$body = '';
if (!in_array($method, ['GET', 'HEAD'], true)) {
    $body = file_get_contents('php://input') ?: '';
}

$hdrLines = [];
foreach ($_SERVER as $k => $v) {
    if (!is_string($v)) continue;
    if (strpos($k, 'HTTP_') === 0) {
        $name = str_replace('_', '-', substr($k, 5));
        $lc = strtolower($name);
        if (in_array($lc, ['host', 'connection', 'content-length', 'accept-encoding', 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto'], true)) continue;
        $hdrLines[] = $name . ': ' . $v;
    }
}
if (!empty($_SERVER['CONTENT_TYPE'])) $hdrLines[] = 'Content-Type: ' . $_SERVER['CONTENT_TYPE'];
$hdrLines[] = 'X-Real-IP: ' . ($_SERVER['REMOTE_ADDR'] ?? '');
$hdrLines[] = 'X-Forwarded-Via: fideliza-proxy';

$ctx = stream_context_create([
    'http' => [
        'method'          => $method,
        'header'          => implode("\r\n", $hdrLines),
        'content'         => $body,
        'ignore_errors'   => true,
        'timeout'         => 60,
        'follow_location' => 0,
    ],
]);

$respBody = @file_get_contents($target, false, $ctx);
if ($respBody === false) {
    http_response_code(502);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['ok'=>false, 'erro'=>'proxy: backend Fideliza fora do ar', 'alvo'=>$target], JSON_UNESCAPED_UNICODE);
    exit;
}

$httpCode = 200;
foreach ($http_response_header ?? [] as $linha) {
    if (preg_match('#^HTTP/[\d.]+\s+(\d+)#', $linha, $m)) {
        $httpCode = (int)$m[1];
        continue;
    }
    if (strpos($linha, ':') === false) continue;
    $nome = strtolower(trim(strtok($linha, ':')));
    if (in_array($nome, ['transfer-encoding', 'content-length', 'connection', 'content-encoding'], true)) continue;

    /* Reescreve Location: /master → /fideliza/master */
    if ($nome === 'location') {
        $valor = trim(substr($linha, strpos($linha, ':') + 1));
        if (strlen($valor) > 0 && $valor[0] === '/' && strpos($valor, $prefixo) !== 0) {
            $linha = 'Location: ' . $prefixo . $valor;
        }
    }

    /* Set-Cookie: path=/ → path=/fideliza */
    if ($nome === 'set-cookie') {
        $linha = preg_replace('#(;\s*[Pp]ath\s*=\s*)/([;\s]|$)#', '$1' . $prefixo . '$2', $linha);
        if (stripos($linha, 'path=') === false) {
            $linha .= '; Path=' . $prefixo;
        }
    }

    header($linha, false);
}

/* Reescreve HTML: href/action/src que começam com "/" viram "/fideliza/..." */
$ct = '';
foreach ($http_response_header ?? [] as $h) {
    if (preg_match('/^content-type:\s*(.+)$/i', trim($h), $m)) { $ct = strtolower($m[1]); break; }
}
if (strpos($ct, 'text/html') !== false && $respBody !== '') {
    $respBody = preg_replace_callback(
        '#\b(href|action|src|formaction)\s*=\s*"(/[^"]*)"#i',
        function ($m) use ($prefixo) {
            $url = $m[2];
            if (strpos($url, $prefixo . '/') === 0 || $url === $prefixo) return $m[0];
            if (strpos($url, '//') === 0) return $m[0];
            return $m[1] . '="' . $prefixo . $url . '"';
        },
        $respBody
    );
    // Também versão single-quote
    $respBody = preg_replace_callback(
        "#\b(href|action|src|formaction)\s*=\s*'(/[^']*)'#i",
        function ($m) use ($prefixo) {
            $url = $m[2];
            if (strpos($url, $prefixo . '/') === 0 || $url === $prefixo) return $m[0];
            if (strpos($url, '//') === 0) return $m[0];
            return $m[1] . "='" . $prefixo . $url . "'";
        },
        $respBody
    );
}

http_response_code($httpCode);
echo $respBody;
LANDING_PHP;
    @file_put_contents($landingDir . '/index.php', $landingPhp);

    // Redirect 301 da URL antiga /fideliza/* → /cashvolta/* (rebrand 2026-07-04)
    $landingLegado = __DIR__ . '/fideliza';
    if (!is_dir($landingLegado)) @mkdir($landingLegado, 0777, true);
    $redirLegado = '<?php
/* Redirect 301 do rebrand: /fideliza/* → /cashvolta/* */
$sufixo = (string)($_SERVER["PATH_INFO"] ?? "");
$qs = !empty($_SERVER["QUERY_STRING"]) ? "?" . $_SERVER["QUERY_STRING"] : "";
header("Location: /cashvolta" . $sufixo . $qs, true, 301);
exit;
';
    @file_put_contents($landingLegado . '/index.php', $redirLegado);

    // Health check local
    $ctx = stream_context_create(['http' => ['timeout' => 3]]);
    $health = @file_get_contents('http://127.0.0.1:8086/health', false, $ctx);
    $healthOk = is_string($health) && str_contains($health, '"ok":true');

    echo json_encode([
        'ok' => $healthOk,
        'tipo' => $tipoUpload,
        'php' => $phpExe,
        'ext_dir' => $extDir,
        'ext_dir_existe' => is_dir($extDir),
        'pdo_sqlite_dll' => is_file($extDir . '\\php_pdo_sqlite.dll'),
        'destino' => $destBackend,
        'preservou_storage' => $preservarStorage && $storageBackup !== null,
        'via_vbs' => $viaVbs,
        'task_criada' => $tRc === 0,
        'task_executada' => $rRc === 0,
        'via_popen' => $viaPopen,
        'firewall_ok' => $fRc === 0,
        'health' => $healthOk ? 'up' : 'down',
        'health_body' => $health ?: null,
        'log_hint' => $logFile,
    ]);
    exit;
}

// DEBUG temporário — lista arquivos de C:\CashVolta\backend\public\app\
if (($_GET['tipo'] ?? '') === 'fideliza_ls') {
    header('Content-Type: application/json; charset=utf-8');
    $dir = 'C:\\CashVolta\\backend\\public\\app';
    $arquivos = [];
    if (is_dir($dir)) {
        foreach (scandir($dir) ?: [] as $f) {
            if ($f === '.' || $f === '..') continue;
            $p = $dir . '\\' . $f;
            $arquivos[] = [
                'nome' => $f,
                'tamanho' => is_file($p) ? filesize($p) : null,
                'mtime'   => is_file($p) ? date('c', filemtime($p)) : null,
                'is_dir'  => is_dir($p),
            ];
        }
    }
    echo json_encode(['dir' => $dir, 'existe' => is_dir($dir), 'arquivos' => $arquivos], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    exit;
}

// Le body JSON
$raw = file_get_contents('php://input');
$body = @json_decode($raw, true);
if (!is_array($body)) {
    http_response_code(400);
    echo json_encode(['ok'=>false, 'erro'=>'JSON inválido']);
    exit;
}

// AUDIT LOG (v5+): rastreia TODA request pra diagnosticar quem manda payload
// mal formado (achamos "cliente ?" mesmo depois de validacao — origem misteriosa).
// Grava em storage/access.log, mantem apenas ultimas 2000 linhas via rotacao simples.
$auditLog = __DIR__ . '/storage/access.log';
$auditLine = date('c')
    . ' | ip=' . ($_SERVER['REMOTE_ADDR'] ?? '?')
    . ' | ua=' . substr((string)($_SERVER['HTTP_USER_AGENT'] ?? '-'), 0, 80)
    . ' | tipo=' . ($body['tipo'] ?? 'backup')
    . ' | cliente=' . (($body['cliente'] ?? '') !== '' ? substr((string)$body['cliente'], 0, 40) : '(VAZIO)')
    . ' | job=' . (($body['job_id'] ?? '') !== '' ? substr((string)$body['job_id'], 0, 40) : '(VAZIO)')
    . ' | host=' . (($body['host_cliente'] ?? $body['hostname'] ?? '') !== '' ? substr((string)($body['host_cliente'] ?? $body['hostname']), 0, 40) : '(VAZIO)')
    . ' | versao_bi=' . ($body['versao_bi'] ?? '-')
    . ' | status=' . ($body['status'] ?? '-')
    . "\n";
@file_put_contents($auditLog, $auditLine, FILE_APPEND);
// Rotaciona quando passa de 500KB
if (@filesize($auditLog) > 512000) {
    @rename($auditLog, $auditLog . '.old');
}

// Tipo do payload — backup (default) ou atualizacao (v1.5.35+)
// (self_update já foi tratado la em cima, antes do json_decode)
$tipo = (string)($body['tipo'] ?? 'backup');
$dirStorage = __DIR__ . '/storage';
if (!is_dir($dirStorage)) @mkdir($dirStorage, 0777, true);
$host = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($body['host_cliente'] ?? $body['hostname'] ?? 'unk'));

// ATUALIZACOES (Logger do BI cliente apos rodar OTA) — vao pra arquivo separado
// em append (.jsonl), pra eu poder ver historico sem precisar PowerShell.
// IMPORTANTE: validado antes da checagem de cliente/job_id (atualizacao nao tem isso).
if ($tipo === 'atualizacao') {
    $dirAtu = $dirStorage . '/atualizacoes';
    if (!is_dir($dirAtu)) @mkdir($dirAtu, 0777, true);

    $body['ip_remoto']   = $_SERVER['REMOTE_ADDR'] ?? '?';
    $body['recebido_em'] = date('c');

    $arqAtu = $dirAtu . '/' . ($host !== '' ? $host : 'unk') . '.jsonl';
    @file_put_contents($arqAtu, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) . "\n", FILE_APPEND);
    echo json_encode(['ok'=>true, 'tipo'=>'atualizacao', 'salvo'=>basename($arqAtu)]);
    exit;
}

// USO (telemetria de acesso — v1.7.75+): cada login de um BI cliente manda
// {cliente, usuario, em, versao_bi}. Guardado em append por host (.jsonl).
// So metadado de acesso (quem/quando), NUNCA conteudo. Alimenta a aba uso.php.
if ($tipo === 'uso') {
    $dirUso = $dirStorage . '/uso';
    if (!is_dir($dirUso)) @mkdir($dirUso, 0777, true);

    $ev = [
        'cliente'     => trim((string)($body['cliente']  ?? '')),
        'usuario'     => trim((string)($body['usuario']  ?? '')),
        'evento'      => (string)($body['evento']    ?? 'login'),
        'versao_bi'   => (string)($body['versao_bi'] ?? ''),
        'em'          => (string)($body['em']        ?? date('c')),
        'ip'          => $_SERVER['REMOTE_ADDR'] ?? '?',
        'recebido_em' => date('c'),
    ];
    if ($ev['cliente'] === '' && $ev['usuario'] === '') {
        http_response_code(400);
        echo json_encode(['ok'=>false, 'erro'=>'uso: cliente/usuario vazios']);
        exit;
    }

    $arqUso = $dirUso . '/' . ($host !== '' ? $host : 'unk') . '.jsonl';
    @file_put_contents($arqUso, json_encode($ev, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) . "\n", FILE_APPEND);
    // Rotaciona quando passa de 1MB (preserva historico em .old datado).
    if (@filesize($arqUso) > 1048576) @rename($arqUso, $arqUso . '.' . date('Ymd') . '.old');

    echo json_encode(['ok'=>true, 'tipo'=>'uso', 'salvo'=>basename($arqUso)]);
    exit;
}

// BACKUP (fluxo legado): valida campos obrigatorios
$cliente = trim((string)($body['cliente'] ?? ''));
$jobId   = trim((string)($body['job_id'] ?? ''));
// Rejeita nao so vazio mas tambem valores placeholder ('?', 'unk', 'unknown', '-', 'null')
$invalidos = ['', '?', 'unk', 'unknown', '-', 'null', 'undefined'];
if (in_array(strtolower($cliente), $invalidos, true) || in_array(strtolower($jobId), $invalidos, true)) {
    http_response_code(400);
    echo json_encode(['ok'=>false, 'erro'=>'cliente e job_id sao obrigatorios (e nao podem ser placeholders)']);
    exit;
}

// Auto-cleanup: se detectar arquivos orfaos no storage (nomes conhecidos), apaga.
// Roda ~1% das vezes pra nao pesar. Cobre casos vestigios de api.php antigo.
if (mt_rand(1, 100) === 1) {
    foreach (['__.json', 'unk__unk.json', 'unknown__unknown.json'] as $orfao) {
        $p = $dirStorage . '/' . $orfao;
        if (is_file($p)) @unlink($p);
    }
}

$slug = preg_replace('/[^a-zA-Z0-9_-]/', '', $jobId);
$arq = $dirStorage . '/' . $host . '__' . $slug . '.json';

// Enriquece com IP do remoto e momento de recepcao
$body['ip_remoto']      = $_SERVER['REMOTE_ADDR'] ?? '?';
$body['recebido_em']    = date('c');

// Heartbeat de backup em andamento (v1.5.25+):
// Quando status='em_andamento' o payload TRAZ tambem: estagio, tamanho_atual,
// iniciado_em, tempo_decorrido. Sobrescreve o mesmo arquivo do job (=mesmo
// padrao do payload final sucesso/falha) — index.php diferencia pelo campo
// 'status' e mostra bolinha azul pulsante quando heartbeat < 90s.

@file_put_contents($arq, json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

echo json_encode(['ok'=>true, 'salvo'=>basename($arq)]);
