<?php

declare(strict_types=1);

// Bounded synchronous Excel2007 (XLSX) report exports. Default is read-only.
$lane = (string)getenv('CDX_NEXT3_LANE');
$action = (string)($argv[1] ?? '');
$modes = [
    '--preflight-dish' => ['dish', false],
    '--preflight-restaurant' => ['restaurant', false],
    '--preflight-stall' => ['stall', false],
    '--execute-dish' => ['dish', true],
    '--execute-restaurant' => ['restaurant', true],
    '--execute-stall' => ['stall', true],
];
$targets = [
    'A' => ['/tmp/zhct-http-store-a.l5omH8', 'http://127.0.0.1:18097', 21023, 'mysql',
        '04e3f80cb0dddff6ec657f731185013ffffebcb4da48be8402b6d86547e14742',
        'fb3e2fb496b7b56a16969f2d01491d006aeb4cf2515fb48e3b62001468e74a1b',
        '95ce9bcee0f908ace8c86c1978ed9e41791e56a3ab53d53de17972d12e908341'],
    'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 10001, 'mysql',
        '08528bd3ac98c3dc8ac8c5f442313e86aacb6b6011b27861101afd00f7b7a998',
        '468f4d22f73e0bf3e6e5fac3551c690f0aafb60678b4ec38ca88ce60afad52d0',
        '8953e4be37bb3a1f6adf40c9da9b19de5e3eb2f61d595812b080a66bec10b7c2'],
    'C' => ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', 112175, 'dm',
        '08528bd3ac98c3dc8ac8c5f442313e86aacb6b6011b27861101afd00f7b7a998',
        '468f4d22f73e0bf3e6e5fac3551c690f0aafb60678b4ec38ca88ce60afad52d0',
        '8953e4be37bb3a1f6adf40c9da9b19de5e3eb2f61d595812b080a66bec10b7c2'],
];
if (!isset($targets[$lane], $modes[$action])) {
    fwrite(STDERR, "Next3 export execution disabled\n");
    exit(78);
}
[$mode, $execute] = $modes[$action];
if ($execute && ((string)getenv('CDX_NEXT3_EXECUTE') !== 'ROOT_APPROVED_NEXT3_V1'
    || (string)getenv('CDX_NEXT3_SHA') !== hash_file('sha256', __FILE__))) {
    fwrite(STDERR, "Next3 business HTTP/DML disabled\n");
    exit(78);
}
[$root, $base, $pid, $dialect, $mealSha, $excelSha, $userSha] = $targets[$lane];
if ((string)getenv('POC_APP_ROOT') !== $root
    || (string)getenv('POC_BASE_URL') !== $base
    || (string)getenv('POC_DIALECT') !== $dialect
    || is_link($root)
    || hash_file('sha256', $root . '/application/p/controller/Report.php')
        !== '1673113e55dc7f247f697a77ece16ebdb21d1572d8a202c40abb8764cbc83f7e'
    || hash_file('sha256', $root . '/application/p/logic/MealOrder.php') !== $mealSha
    || hash_file('sha256', $root . '/application/model/Excel.php') !== $excelSha
    || hash_file('sha256', $root . '/application/model/User.php') !== $userSha) {
    throw new RuntimeException('Next3 approved source/runtime mismatch');
}
$runtimeReady = static function () use ($root, $base, $pid): bool {
    $cmdline = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
    $environment = (string)@file_get_contents('/proc/' . $pid . '/environ');
    $children = (string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children');
    $boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
    $port = (int)parse_url($base, PHP_URL_PORT);
    $hex = strtoupper(str_pad(dechex($port), 4, '0', STR_PAD_LEFT));
    $inode = '';
    foreach (@file('/proc/net/tcp', FILE_IGNORE_NEW_LINES) ?: [] as $line) {
        $parts = preg_split('/\s+/', trim($line));
        if (($parts[1] ?? '') === '0100007F:' . $hex && ($parts[3] ?? '') === '0A') {
            $inode = (string)($parts[9] ?? '');
            break;
        }
    }
    $ownsSocket = false;
    if ($inode !== '') {
        foreach (@scandir('/proc/' . $pid . '/fd') ?: [] as $fd) {
            if ($fd !== '.' && $fd !== '..'
                && @readlink('/proc/' . $pid . '/fd/' . $fd) === 'socket:[' . $inode . ']') {
                $ownsSocket = true;
                break;
            }
        }
    }
    $multiWorker = preg_match('/(?:^|\x00)PHP_CLI_SERVER_WORKERS=(\d+)(?:\x00|$)/',
        $environment, $match) && (int)$match[1] > 1;
    return strpos($cmdline, '127.0.0.1:' . $port) !== false
        && strpos($cmdline, $root . '/public') !== false
        && $environment !== '' && trim($children) === '' && !$multiWorker
        && $ownsSocket && is_array($boundary)
        && !empty($boundary['external_stubs'])
        && !empty($boundary['redis_isolated']) && empty($boundary['real_queue']);
};
if (!$runtimeReady()) throw new RuntimeException('Next3 isolated HTTP boundary absent');

$runtime = (static function (string $path): array {
    $config = require $path;
    if (!is_array($config)) throw new RuntimeException('Next3 runtime DB config invalid');
    return $config;
})($root . '/application/database.php');
if ((string)($runtime['prefix'] ?? '') !== 'ydy_') {
    throw new RuntimeException('Next3 Store prefix mismatch');
}
if ($dialect === 'mysql') {
    $local = (static function (): array {
        $config = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
        if (!is_array($config)) throw new RuntimeException('Next3 code local config invalid');
        return $config;
    })();
    if ((string)($runtime['type'] ?? '') !== 'mysql'
        || !in_array((string)($runtime['charset'] ?? ''), ['utf8', 'utf8mb4'], true)) {
        throw new RuntimeException('Next3 MySQL driver/charset mismatch');
    }
    foreach (['hostname', 'database', 'username', 'password'] as $key) {
        if (($runtime[$key] ?? null) !== ($local[$key] ?? null)) {
            throw new RuntimeException('Next3 runtime differs from code local');
        }
    }
    if ((int)($runtime['hostport'] ?? 3306) !== (int)($local['hostport'] ?? 3306)) {
        throw new RuntimeException('Next3 MySQL port differs from code local');
    }
    $dsn = 'mysql:host=' . $runtime['hostname'] . ';port='
        . (int)($runtime['hostport'] ?? 3306) . ';dbname=' . $runtime['database']
        . ';charset=' . (string)$runtime['charset'];
    $approvedDbSha = '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401';
} else {
    if ((string)($runtime['type'] ?? '') !== 'app\\cores\\database\\Dm'
        || (string)getenv('POC_HOST') !== '127.0.0.1'
        || (string)getenv('POC_PORT') !== '5237'
        || strtoupper((string)getenv('POC_SCHEMA')) !== 'ZHCT_APP'
        || strtoupper((string)getenv('POC_USER')) !== 'ZHCT_APP') {
        throw new RuntimeException('Next3 DM target mismatch');
    }
    foreach (['hostname' => 'POC_HOST', 'hostport' => 'POC_PORT',
        'database' => 'POC_SCHEMA', 'username' => 'POC_USER',
        'password' => 'POC_PASSWORD'] as $key => $envKey) {
        if ((string)($runtime[$key] ?? '') !== (string)getenv($envKey)) {
            throw new RuntimeException('Next3 DM worker context mismatch');
        }
    }
    $dsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
    $approvedDbSha = 'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194';
    $machineId = trim((string)@file_get_contents('/etc/machine-id'));
    if ($machineId === '' || gethostname() === false
        || hash('sha256', gethostname() . '|' . $machineId)
            !== 'd56d81988bb0f106946cce91c608fdd0ef1116e5150b23376ea87e086eace4b9') {
        throw new RuntimeException('Next3 DM host mismatch');
    }
    $lines = [];
    $exitCode = 0;
    @exec('/usr/sbin/ss -ltnp 2>/dev/null', $lines, $exitCode);
    $localListener = false;
    if ($exitCode === 0) {
        foreach ($lines as $line) {
            if (preg_match('/\*:5237\s+.*users:\(\("dmserver",pid=(\d+),/',
                $line, $match)) {
                $exe = @readlink('/proc/' . $match[1] . '/exe');
                $localListener = is_string($exe) && basename($exe) === 'dmserver';
                break;
            }
        }
    }
    if (!$localListener) throw new RuntimeException('Next3 DM listener not local');
}
$connect = static function () use ($runtime, $dsn, $dialect, $approvedDbSha): PDO {
    $pdo = new PDO($dsn, (string)$runtime['username'],
        (string)$runtime['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
    $identity = $dialect === 'mysql'
        ? (string)$pdo->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())"
        )->fetchColumn()
        : 'ZHCT_APP|5237|' . (string)$pdo->query(
            "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL"
        )->fetchColumn();
    if (!hash_equals($approvedDbSha, hash('sha256', $identity))) {
        throw new RuntimeException('Next3 DB outside approved target');
    }
    return $pdo;
};
$readRows = static function (PDO $pdo, string $sql, array $args = []): array {
    $stmt = $pdo->prepare($sql);
    $stmt->execute($args);
    $rows = [];
    foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $raw) {
        $row = [];
        foreach (array_change_key_case($raw, CASE_LOWER) as $key => $value) {
            $row[$key] = is_resource($value) ? stream_get_contents($value) : $value;
        }
        $rows[] = $row;
        if (count($rows) > 100) throw new RuntimeException('Next3 source over 100 rows');
    }
    return $rows;
};
$store = $connect();
$prefix = $dialect === 'dm' ? 'YDY_' : 'ydy_';
$menuDate = $dialect === 'dm' ? '2025-01-16' : '2026-10-01';
$bindDate = $dialect === 'dm' ? '2025-01-16' : '2026-09-30';
$restaurantId = 1;
$menuSql = 'SELECT menu.ID AS MENU_ID,menu.MENU_DATE,menu.NUMBER,'
    . ' menu.MEAL_TIMES AS MENU_MEAL_TIMES,menu.DISHES_UUID AS DISH_UUID,'
    . ' d.NAME AS DISH_NAME,r.NAME AS STALL_NAME,rs.NAME AS RESTAURANT_NAME,'
    . ' meal.ID AS MEAL_ID FROM ' . $prefix . 'initial_menu menu'
    . ' JOIN ' . $prefix . 'meal_order meal ON meal.ID=menu.MEAL_ORDER_ID'
    . ' JOIN ' . $prefix . 'dishes d ON d.UUID=menu.DISHES_UUID'
    . ' LEFT JOIN ' . $prefix . 'restaurants r ON r.ID=meal.RESTAURANT_ID'
    . ' LEFT JOIN ' . $prefix . 'restaurant_shop rs ON rs.ID=r.RESTAURANT_SHOP_ID'
    . ' WHERE meal.ORDER_STATUS=30 AND meal.REFUND_STATUS IN (0,1,2)'
    . ' AND meal.IS_DELETE=0 AND menu.MENU_DATE=? AND meal.RESTAURANT_ID=?'
    . ($dialect === 'dm' ? ' AND ROWNUM<=101 ORDER BY menu.ID'
        : ' ORDER BY menu.ID LIMIT 101');
$mealSql = 'SELECT meal.ID AS MEAL_ID,meal.BIND_TIME,meal.MEAL_TIMES,'
    . ' meal.PAY_PRICE,meal.REFUND_AMOUNT,meal.RESTAURANT_ID,'
    . ' r.NAME AS STALL_NAME,rs.NAME AS RESTAURANT_NAME FROM '
    . $prefix . 'meal_order meal'
    . ' LEFT JOIN ' . $prefix . 'restaurants r ON r.ID=meal.RESTAURANT_ID'
    . ' LEFT JOIN ' . $prefix . 'restaurant_shop rs ON rs.ID=r.RESTAURANT_SHOP_ID'
    . ' WHERE meal.ORDER_STATUS=30 AND meal.REFUND_STATUS IN (0,1,2)'
    . ' AND meal.IS_DELETE=0 AND meal.BIND_TIME BETWEEN ? AND ?'
    . ' AND meal.RESTAURANT_ID=?'
    . ($dialect === 'dm' ? ' AND ROWNUM<=101 ORDER BY meal.ID'
        : ' ORDER BY meal.ID LIMIT 101');
$menuArgs = [$menuDate, $restaurantId];
$mealArgs = [$bindDate . ' 00:00:00', $bindDate . ' 23:59:59', $restaurantId];
$sourceSql = $mode === 'dish' ? $menuSql : $mealSql;
$sourceArgs = $mode === 'dish' ? $menuArgs : $mealArgs;
$source = $readRows($store, $sourceSql, $sourceArgs);
if (count($source) !== ($mode === 'dish'
        ? ($dialect === 'dm' ? 5 : 1) : ($dialect === 'dm' ? 10 : 7))) {
    throw new RuntimeException('Next3 audited source window changed');
}
$money = static function ($value): string {
    return number_format((float)$value, 2, '.', '');
};
$dishGroups = [];
$sums = array_fill_keys(['breakfast', 'lunch', 'dinner', 'snack', 'total'], 0.0);
if ($mode === 'dish') {
    foreach ($source as $row) {
        $uuid = (string)$row['dish_uuid'];
        if (!isset($dishGroups[$uuid])) {
            $dishGroups[$uuid] = ['restaurant_name' => (string)$row['restaurant_name'],
                'stall_name' => (string)$row['stall_name'],
                'dish_name' => (string)$row['dish_name']]
                + array_fill_keys(['breakfast', 'lunch', 'dinner', 'snack', 'total'], 0.0);
        }
        $count = (float)$row['number'];
        $dishGroups[$uuid]['total'] += $count;
        $sums['total'] += $count;
        $meal = (int)$row['menu_meal_times'];
        if ($meal >= 1 && $meal <= 4) {
            $field = ['breakfast', 'lunch', 'dinner', 'snack'][$meal - 1];
            $dishGroups[$uuid][$field] += $count;
            $sums[$field] += $count;
        }
    }
    if (count($dishGroups) !== ($dialect === 'dm' ? 3 : 1)) {
        throw new RuntimeException('Next3 audited dish group changed');
    }
    $names = array_column(array_values($dishGroups), 'dish_name');
    if (count(array_unique($names)) !== count($names)) {
        throw new RuntimeException('Next3 duplicate dish name makes row matching ambiguous');
    }
} else {
    $counts = array_fill_keys(['breakfast', 'lunch', 'dinner', 'snack', 'allday'], 0);
    $amounts = array_fill_keys(['breakfast', 'lunch', 'dinner', 'snack', 'allday'], 0.0);
    foreach ($source as $row) {
        $net = (float)$row['pay_price'] - (float)$row['refund_amount'];
        $counts['allday']++;
        $amounts['allday'] += $net;
        $meal = (int)$row['meal_times'];
        if ($meal >= 1 && $meal <= 4) {
            $field = ['breakfast', 'lunch', 'dinner', 'snack'][$meal - 1];
            $counts[$field]++;
            $amounts[$field] += $net;
        }
    }
}
$businessRows = $mode === 'dish' ? count($dishGroups) : 1;
if ($businessRows < 1 || $businessRows > 100) {
    throw new RuntimeException('Next3 export business row bound exceeded');
}
$autoload = $root . '/vendor/autoload.php';
if (!is_file($autoload) || !is_readable($autoload)) {
    throw new RuntimeException('Next3 PHPExcel autoload unavailable');
}
require_once $autoload;
if (!class_exists('ZipArchive') || !class_exists('PHPExcel_IOFactory')) {
    throw new RuntimeException('Next3 ZIP/Excel2007 parser unavailable');
}
try {
    $readerReady = \PHPExcel_IOFactory::createReader('Excel2007');
    if (!($readerReady instanceof \PHPExcel_Reader_IReader)) {
        throw new RuntimeException('Next3 Excel2007 reader type unavailable');
    }
    unset($readerReady);
} catch (Throwable $e) {
    throw new RuntimeException('Next3 Excel2007 reader unavailable');
}
$readQueue = static function (PDO $pdo) use ($prefix): array {
    $raw = $pdo->query('SELECT COUNT(*) AS TOTAL,'
        . ' SUM(CASE WHEN STATUS=0 THEN 1 ELSE 0 END) AS PENDING FROM '
        . $prefix . 'export_log')->fetch(PDO::FETCH_ASSOC);
    $raw = array_change_key_case(is_array($raw) ? $raw : [], CASE_LOWER);
    return ['total' => (int)($raw['total'] ?? -1),
        'pending' => (int)($raw['pending'] ?? -1)];
};
$queueBeforeActor = $readQueue($store);
if ($queueBeforeActor['total'] < 0 || $queueBeforeActor['pending'] !== 0) {
    throw new RuntimeException('Next3 preflight queue unavailable or pending');
}
$markerCount = (int)$store->query('SELECT COUNT(*) FROM ' . $prefix . 'user'
    . " WHERE ACCOUNT LIKE 'CDXEXN3%'")->fetchColumn();
$sourceSha = hash('sha256', serialize($source));
$preflight = ['lane' => $lane, 'mode' => $mode, 'preflight' => true,
    'source_rows' => count($source), 'business_rows' => $businessRows,
    'source_sha' => $sourceSha, 'existing_actors' => $markerCount,
    'export_log_before_total' => $queueBeforeActor['total'],
    'export_log_before_pending' => $queueBeforeActor['pending'],
    'db_identity_approved' => true, 'xlsx_reader_ready' => true];
if (!$execute) {
    echo json_encode($preflight, JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
if ($markerCount !== 0) throw new RuntimeException('Old Next3 actor requires review');
if ((string)getenv('CDX_NEXT3_DB_SHA') !== $approvedDbSha) {
    throw new RuntimeException('Next3 execution DB identity confirmation missing');
}

// Business HTTP and the single synthetic actor INSERT are below this line.
$marker = 'CDXEXN3' . strtoupper(bin2hex(random_bytes(8)));
$privateDir = sys_get_temp_dir() . '/zhct-next3-' . $marker;
if (file_exists($privateDir) || !mkdir($privateDir, 0700)) {
    throw new RuntimeException('Next3 private directory unavailable');
}
chmod($privateDir, 0700);
$xlsxPath = $privateDir . '/response.xlsx';
$journalPath = $privateDir . '/recovery.json';
$journal = ['lane' => $lane, 'mode' => $mode, 'marker' => $marker,
    'actor_id' => 0, 'state' => 'prepared'];
$saveJournal = static function () use (&$journal, $privateDir, $journalPath): void {
    $next = $privateDir . '/recovery.next';
    if (file_put_contents($next, json_encode($journal, JSON_UNESCAPED_SLASHES)) === false) {
        throw new RuntimeException('Next3 journal write failed');
    }
    chmod($next, 0600);
    if (!rename($next, $journalPath)) throw new RuntimeException('Next3 journal rename failed');
};
$saveJournal();
$actorId = 0;
$token = '';
$listMatches = false;
$xlsxMatches = false;
$sourceUnchanged = false;
$queueUnchanged = false;
$cleaned = false;
$privateCleared = false;
$bytes = 0;
$rowsChecked = 0;
$failure = '';
$queueBefore = null;
$queueAfter = null;
$workbookDataSha = '';

$http = static function (string $route, array $params, string $method,
    ?string $targetFile = null) use ($base, $runtimeReady, &$token): array {
    if (!$runtimeReady()) throw new RuntimeException('Next3 runtime drift before business HTTP');
    $url = $base . '/p/report/' . $route;
    $options = ['method' => $method, 'timeout' => 20, 'ignore_errors' => true,
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
            . 'token: ' . $token . "\r\n"];
    if ($method === 'GET') {
        $url .= '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
    } else {
        $options['content'] = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
    }
    $ctx = stream_context_create(['http' => $options]);
    $input = @fopen($url, 'rb', false, $ctx);
    if (!$input) throw new RuntimeException('Next3 HTTP open failed');
    $headers = $http_response_header ?? [];
    $first = (string)($headers[0] ?? '');
    if (strpos($first, ' 200 ') === false) {
        fclose($input);
        throw new RuntimeException('Next3 HTTP status mismatch');
    }
    $cap = $targetFile === null ? 262144 : 1048576;
    $body = stream_get_contents($input, $cap + 1);
    fclose($input);
    if (!is_string($body) || strlen($body) > $cap) {
        throw new RuntimeException('Next3 HTTP response exceeds byte cap');
    }
    if ($targetFile === null) {
        $json = json_decode($body, true);
        if (!is_array($json) || ($json['code'] ?? null) !== 0
            || ($json['message'] ?? null) !== '查询成功'
            || !is_array($json['data']['data'] ?? null)
            || !is_array($json['data']['summary'] ?? null)) {
            throw new RuntimeException('Next3 paired list response mismatch');
        }
        return $json;
    }
    $headerText = implode("\n", $headers);
    if (stripos($headerText,
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') === false
        || stripos($headerText, 'attachment;') === false
        || substr($body, 0, 4) !== "PK\x03\x04") {
        throw new RuntimeException('Next3 XLSX header/signature mismatch');
    }
    $file = @fopen($targetFile, 'xb');
    if (!$file) throw new RuntimeException('Next3 private XLSX unavailable');
    chmod($targetFile, 0600);
    $written = fwrite($file, $body);
    fclose($file);
    if ($written !== strlen($body)) throw new RuntimeException('Next3 private XLSX truncated');
    return ['bytes' => strlen($body), 'sha256' => hash('sha256', $body)];
};
$readXlsx = static function (string $path, array $header,
    int $businessCount) use ($root): array {
    $zip = new ZipArchive();
    if ($zip->open($path, ZipArchive::CHECKCONS) !== true) {
        throw new RuntimeException('Next3 XLSX ZIP corrupt');
    }
    foreach (['[Content_Types].xml', 'xl/workbook.xml',
        'xl/worksheets/sheet1.xml'] as $required) {
        if ($zip->locateName($required) === false) {
            $zip->close();
            throw new RuntimeException('Next3 XLSX OOXML part absent');
        }
    }
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $name = (string)$zip->getNameIndex($i);
        if (strpos($name, 'xl/externalLinks/') === 0
            || strpos($name, 'vbaProject.bin') !== false) {
            $zip->close();
            throw new RuntimeException('Next3 XLSX external/macro part present');
        }
    }
    $zip->close();
    require_once $root . '/vendor/autoload.php';
    $reader = \PHPExcel_IOFactory::createReader('Excel2007');
    $reader->setReadDataOnly(true);
    $book = $reader->load($path);
    if ($book->getSheetCount() !== 1) {
        throw new RuntimeException('Next3 XLSX sheet count mismatch');
    }
    $sheet = $book->getSheet(0);
    if ((int)$sheet->getHighestRow() !== $businessCount + 2
        || \PHPExcel_Cell::columnIndexFromString($sheet->getHighestColumn())
            !== count($header)) {
        throw new RuntimeException('Next3 XLSX dimensions mismatch');
    }
    $rows = [];
    for ($rowNum = 1; $rowNum <= $businessCount + 2; $rowNum++) {
        $row = [];
        for ($column = 0; $column < count($header); $column++) {
            $value = $sheet->getCellByColumnAndRow($column, $rowNum)->getValue();
            $row[] = $value === null ? '' : (string)$value;
        }
        $rows[] = $row;
    }
    $book->disconnectWorksheets();
    unset($book, $reader);
    if ($rows[0] !== $header) throw new RuntimeException('Next3 XLSX header mismatch');
    return array_slice($rows, 1);
};

try {
    $store->query('SELECT ID,UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE'
        . ' FROM ' . $prefix . 'user WHERE 1=0');
    $insert = $store->prepare('INSERT INTO ' . $prefix . 'user'
        . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
        . ' VALUES (?,?,?,?,?,?,?,?,?)');
    $insert->execute([$marker, $marker, 'DISABLED',
        strtoupper(bin2hex(random_bytes(8))), $marker, 0,
        date('Y-m-d H:i:s'), 'CODEX', 0]);
    $own = $store->prepare('SELECT ID FROM ' . $prefix . 'user'
        . ' WHERE UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
    $own->execute([$marker, $marker, 'CODEX']);
    $actorId = (int)$own->fetchColumn();
    if ($actorId <= 0) throw new RuntimeException('Next3 actor insert readback failed');
    $journal['actor_id'] = $actorId;
    $journal['state'] = 'actor_inserted';
    $saveJournal();
    require_once $root . '/application/base/Security.php';
    $token = \app\base\Security::encrypt(implode("\t",
        [(string)$actorId, $marker, $marker, '0', '', '', $marker]));
    if (!is_string($token) || $token === '') {
        throw new RuntimeException('Next3 actor token unavailable');
    }
    $queueBefore = $readQueue($store);
    $journal['export_log_before_total'] = $queueBefore['total'];
    $journal['export_log_before_pending'] = $queueBefore['pending'];
    $journal['state'] = 'queue_before_recorded';
    $saveJournal();
    if ($queueBefore['total'] < 0 || $queueBefore['pending'] !== 0) {
        throw new RuntimeException('Next3 queue unsafe before business HTTP');
    }
    $date = $mode === 'dish' ? $menuDate : $bindDate;
    $form = ['start_date' => $date, 'end_date' => $date,
        'restaurantIds' => (string)$restaurantId];
    $routes = ['dish' => ['dishSalesVolumeList', 'dishSalesVolumeExport'],
        'restaurant' => ['restaurantOperatingSummaryList',
            'restaurantOperatingSummaryExport'],
        'stall' => ['stallRevenueSettlementList', 'stallRevenueSettlementExport']];
    [$listRoute, $exportRoute] = $routes[$mode];
    $list = $http($listRoute, $form, 'POST');
    $listRows = $list['data']['data'];
    $summary = $list['data']['summary'];
    if ((int)($list['data']['total'] ?? -1) !== $businessRows
        || count($listRows) !== $businessRows) {
        throw new RuntimeException('Next3 paired list row count mismatch');
    }
    if ($mode === 'dish') {
        $expectedByName = [];
        foreach ($dishGroups as $group) $expectedByName[$group['dish_name']] = $group;
        foreach ($listRows as $row) {
            $group = $expectedByName[(string)($row['dish_name'] ?? '')] ?? null;
            if (!$group || (string)($row['restaurant_name'] ?? '')
                    !== $group['restaurant_name']
                || (string)($row['stall_name'] ?? '') !== $group['stall_name']) {
                throw new RuntimeException('Next3 dish list/source identity mismatch');
            }
            foreach (['breakfast', 'lunch', 'dinner', 'snack', 'total'] as $field) {
                if ($money($row[$field] ?? 0) !== $money($group[$field])) {
                    throw new RuntimeException('Next3 dish list/source number mismatch');
                }
            }
            unset($expectedByName[$group['dish_name']]);
        }
        if ($expectedByName) throw new RuntimeException('Next3 dish list source incomplete');
        foreach ($sums as $field => $value) {
            if ($money($summary[$field] ?? 0) !== $money($value)) {
                throw new RuntimeException('Next3 dish summary/source mismatch');
            }
        }
    } elseif ($mode === 'restaurant') {
        $row = $listRows[0];
        if (substr((string)($row['date'] ?? ''), 0, 10) !== $bindDate) {
            throw new RuntimeException('Next3 restaurant date mismatch');
        }
        foreach ($counts as $field => $count) {
            if ((int)($row[$field . '_count'] ?? -1) !== $count
                || (int)($summary[$field . '_count'] ?? -1) !== $count
                || $money($row[$field . '_amount'] ?? 0) !== $money($amounts[$field])
                || $money($summary[$field . '_amount'] ?? 0)
                    !== $money($amounts[$field])) {
                throw new RuntimeException('Next3 restaurant list/source mismatch');
            }
        }
    } else {
        $row = $listRows[0];
        $first = $source[0];
        if ((string)($row['restaurant_name'] ?? '')
                !== (string)$first['restaurant_name']
            || (string)($row['stall_name'] ?? '') !== (string)$first['stall_name']
            || (int)($row['total_count'] ?? -1) !== count($source)
            || (int)($summary['total_count'] ?? -1) !== count($source)
            || $money($row['total_amount'] ?? 0) !== $money($amounts['allday'])
            || $money($summary['total_amount'] ?? 0)
                !== $money($amounts['allday'])) {
            throw new RuntimeException('Next3 stall list/source mismatch');
        }
    }
    $listMatches = true;

    $download = $http($exportRoute, $form, 'GET', $xlsxPath);
    $bytes = (int)$download['bytes'];
    $journal['state'] = 'downloaded_private_xlsx';
    $saveJournal();
    if ($mode === 'dish') {
        $header = ['餐厅名称', '档口名称', '菜品名称', '早餐数量',
            '午餐数量', '晚餐数量', '夜宵数量', '销售汇总'];
    } elseif ($mode === 'restaurant') {
        $header = ['日期', '早餐实收合计笔数', '早餐实收合计金额',
            '午餐实收合计笔数', '午餐实收合计金额',
            '晚餐实收合计笔数', '晚餐实收合计金额',
            '夜宵实收合计笔数', '夜宵实收合计金额',
            '全日实收合计笔数', '全日实收合计金额'];
    } else {
        $header = ['餐厅名称', '档口名称', '实收笔数合计', '实收金额合计'];
    }
    $sheetRows = $readXlsx($xlsxPath, $header, $businessRows);
    if ($mode === 'dish') {
        foreach ($listRows as $index => $row) {
            $actual = $sheetRows[$index];
            $expected = [(string)$row['restaurant_name'], (string)$row['stall_name'],
                (string)$row['dish_name'], $money($row['breakfast']),
                $money($row['lunch']), $money($row['dinner']),
                $money($row['snack']), $money($row['total'])];
            foreach ($expected as $column => $value) {
                $got = $column >= 3 ? $money($actual[$column]) : $actual[$column];
                if ($got !== $value) throw new RuntimeException('Next3 dish XLSX row mismatch');
            }
        }
        $totalExpected = ['合计', '', '', $money($sums['breakfast']),
            $money($sums['lunch']), $money($sums['dinner']),
            $money($sums['snack']), $money($sums['total'])];
        $actual = $sheetRows[$businessRows];
        foreach ($totalExpected as $column => $value) {
            $got = $column >= 3 ? $money($actual[$column]) : $actual[$column];
            if ($got !== $value) throw new RuntimeException('Next3 dish XLSX total mismatch');
        }
    } elseif ($mode === 'restaurant') {
        $business = $sheetRows[0];
        $total = $sheetRows[1];
        if ($business[0] !== $bindDate || $total[0] !== '合计') {
            throw new RuntimeException('Next3 restaurant XLSX date/total mismatch');
        }
        $column = 1;
        foreach (['breakfast', 'lunch', 'dinner', 'snack', 'allday'] as $field) {
            foreach ([$business, $total] as $actual) {
                if ((int)$actual[$column] !== $counts[$field]
                    || $money($actual[$column + 1]) !== $money($amounts[$field])) {
                    throw new RuntimeException('Next3 restaurant XLSX source cell mismatch');
                }
            }
            $column += 2;
        }
    } else {
        $business = $sheetRows[0];
        $total = $sheetRows[1];
        $first = $source[0];
        if ($business[0] !== (string)$first['restaurant_name']
            || $business[1] !== (string)$first['stall_name']
            || (int)$business[2] !== count($source)
            || $money($business[3]) !== $money($amounts['allday'])
            || $total[0] !== '合计' || $total[1] !== ''
            || (int)$total[2] !== count($source)
            || $money($total[3]) !== $money($amounts['allday'])) {
            throw new RuntimeException('Next3 stall XLSX source/total mismatch');
        }
    }
    $rowsChecked = $businessRows;
    $workbookDataSha = hash('sha256', serialize($sheetRows));
    $xlsxMatches = true;
    $token = '';
    $afterDb = $connect();
    $sourceAfter = $readRows($afterDb, $sourceSql, $sourceArgs);
    $sourceUnchanged = hash('sha256', serialize($sourceAfter)) === $sourceSha;
    if (!$sourceUnchanged) throw new RuntimeException('Next3 source changed during export');
    $queueAfter = $readQueue($afterDb);
    $queueUnchanged = $queueAfter === $queueBefore;
    if (!$queueUnchanged) throw new RuntimeException('Next3 export queue changed');
} catch (Throwable $e) {
    $failure = get_class($e) . ':' . hash('sha256', $e->getMessage());
} finally {
    $token = '';
    if (is_file($xlsxPath)) @unlink($xlsxPath);
    $workerStopped = !is_dir('/proc/' . $pid);
    $boundarySafe = $workerStopped || $runtimeReady();
    if ($boundarySafe) {
        try {
            $cleanupDb = $connect();
            $check = $cleanupDb->prepare('SELECT ID FROM ' . $prefix . 'user'
                . ' WHERE UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
            $check->execute([$marker, $marker, 'CODEX']);
            $owned = $check->fetchAll(PDO::FETCH_ASSOC);
            if (count($owned) > 1) throw new RuntimeException('Next3 ambiguous actor');
            if ($owned) {
                $id = (int)(array_change_key_case($owned[0])['id'] ?? 0);
                if ($id <= 0 || ($actorId > 0 && $id !== $actorId)) {
                    throw new RuntimeException('Next3 actor ownership changed');
                }
                $delete = $cleanupDb->prepare('DELETE FROM ' . $prefix . 'user'
                    . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
                $delete->execute([$id, $marker, $marker, 'CODEX']);
                if ($delete->rowCount() !== 1) {
                    throw new RuntimeException('Next3 actor exact delete failed');
                }
            }
            $check->execute([$marker, $marker, 'CODEX']);
            if ($check->fetch(PDO::FETCH_ASSOC)) {
                throw new RuntimeException('Next3 actor remains after delete');
            }
            $cleaned = true;
        } catch (Throwable $e) {
            $failure .= ($failure === '' ? '' : ';')
                . 'cleanup:' . get_class($e) . ':' . hash('sha256', $e->getMessage());
        }
    } else {
        $failure .= ($failure === '' ? '' : ';') . 'cleanup:runtime_boundary_drift';
    }
    if ($cleaned) {
        if (is_file($journalPath)) @unlink($journalPath);
        if (is_file($privateDir . '/recovery.next')) @unlink($privateDir . '/recovery.next');
        $privateCleared = @rmdir($privateDir);
    } else {
        $journal['state'] = 'requires_manual_recovery';
        try { $saveJournal(); } catch (Throwable $e) { /* preserve old journal */ }
    }
}
$result = ['lane' => $lane, 'mode' => $mode,
    'source_rows' => count($source), 'business_rows' => $businessRows,
    'list_source_matched' => $listMatches,
    'xlsx_cells_matched' => $xlsxMatches,
    'xlsx_bytes' => $bytes, 'xlsx_business_rows_checked' => $rowsChecked,
    'workbook_data_sha' => $workbookDataSha,
    'source_unchanged' => $sourceUnchanged,
    'queue_before_total' => $queueBefore['total'] ?? null,
    'queue_before_pending' => $queueBefore['pending'] ?? null,
    'queue_after_total' => $queueAfter['total'] ?? null,
    'queue_after_pending' => $queueAfter['pending'] ?? null,
    'export_queue_unchanged' => $queueUnchanged,
    'actor_exact_cleanup_zero' => $cleaned,
    'private_xlsx_directory_removed' => $privateCleared,
    'failure' => $failure];
echo json_encode($result, JSON_UNESCAPED_SLASHES), PHP_EOL;
if ($failure !== '' || !$listMatches || !$xlsxMatches || !$sourceUnchanged
    || !$queueUnchanged || !$cleaned || !$privateCleared) exit(1);
