<?php

declare(strict_types=1);

$root = (string)getenv('POC_TARGET_ROOT');
$baseUrl = rtrim((string)getenv('POC_BASE_URL'), '/');
$dialect = (string)getenv('POC_DIALECT');
if ($root === '' || $baseUrl === '' || !in_array($dialect, ['mysql', 'dm'], true)) {
    throw new RuntimeException('Isolated meal-diary target required');
}
if ($dialect === 'mysql') {
    $local = parse_ini_file((string)getenv('POC_MYSQL_LOCAL_ENV'), true, INI_SCANNER_RAW);
    if (!is_array($local) || !isset($local['DATABASE'], $local['DATABASE_ZHCT'])) {
        throw new RuntimeException('Selected local development databases unavailable');
    }
    foreach (['DATABASE' => 'AI_', 'DATABASE_ZHCT' => ''] as $section => $prefix) {
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
            'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $from => $to) {
            putenv('POC_' . $prefix . $to . '=' . (string)$local[$section][$from]);
        }
    }
}
foreach (['HOST', 'PORT', 'SCHEMA', 'USER', 'PASSWORD',
    'AI_HOST', 'AI_PORT', 'AI_SCHEMA', 'AI_USER', 'AI_PASSWORD'] as $part) {
    if (getenv('POC_' . $part) === false) {
        throw new RuntimeException('Selected two-database environment incomplete');
    }
}
$boundary = json_decode((string)file_get_contents($baseUrl . '/__poc_boundary'), true);
if (empty($boundary['external_stubs'])) {
    throw new RuntimeException('External-call substitutes unavailable');
}
define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName('poc')->initialize();
set_exception_handler(static function (Throwable $error): void {
    fwrite(STDERR, 'MEAL_DIARY_FAIL ' . get_class($error) . ': '
        . mb_substr($error->getMessage(), 0, 360) . PHP_EOL);
    exit(1);
});
$pdo = static function (string $prefix) use ($dialect): PDO {
    $host = getenv('POC_' . $prefix . 'HOST');
    $port = getenv('POC_' . $prefix . 'PORT');
    $schema = getenv('POC_' . $prefix . 'SCHEMA');
    $dsn = $dialect === 'dm' ? "dm:host=$host;port=$port;schema=$schema"
        : "mysql:host=$host;port=$port;dbname=$schema;charset=utf8mb4";
    return new PDO($dsn, getenv('POC_' . $prefix . 'USER'),
        getenv('POC_' . $prefix . 'PASSWORD'),
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
};
$ai = $pdo('AI_');
$store = $pdo('');
$table = static function (string $name) use ($dialect): string {
    return $dialect === 'dm' ? strtoupper($name) : $name;
};
$aUser = $table('yoshop_user');
$aStaff = $table('yoshop_staff');
$aMeal = $table('yoshop_meal');
$aInfos = $table('yoshop_meal_infos');
$aScore = $table('yoshop_user_score');
$aScoreLog = $table('yoshop_user_score_log');
$aDish = $table('yoshop_dishes');
$aDishFoods = $table('yoshop_dishes_foods');
$aFoods = $table('yoshop_foods');
$sStaff = $table('ydy_staff');
$sDateMenu = $table('ydy_date_menu');
$sDish = $table('ydy_dishes');
$dishId = 8;
$mealDate = '2099-01-01';
$mealTimes = 2;
$marker = 'CDXPARDIET' . strtoupper(bin2hex(random_bytes(4)));
$staffUuid = 'CDXPARDIET-' . strtoupper(bin2hex(random_bytes(10)));
$token = $marker . 'T' . strtoupper(bin2hex(random_bytes(10)));
$aiId = 0;
$aiStaffId = 0;
$storeStaffId = 0;
$mealId = 0;
$infoId = 0;
$read = static function (PDO $db, string $sql, array $args = []): array {
    $q = $db->prepare($sql);
    $q->execute($args);
    $row = $q->fetch(PDO::FETCH_ASSOC);
    return $row === false ? [] : array_change_key_case($row);
};
$count = static function (PDO $db, string $sql, array $args = []): int {
    $q = $db->prepare($sql);
    $q->execute($args);
    return (int)$q->fetchColumn();
};
$assert = static function (bool $ok, string $message): void {
    if (!$ok) throw new RuntimeException($message);
};
$jsonContractMaps = static function (string $raw): array {
    $json = json_decode($raw);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new RuntimeException('Response JSON type-map decode failed');
    }
    $paths = [];
    $stringFormats = [];
    $classifyString = static function (string $value): string {
        if ($value === '') return 'empty';
        if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.(\d{1,9})$/D',
            $value, $match)) {
            $zero = trim($match[1], '0') === '' ? 'zero' : 'nonzero';
            return 'datetime-fraction-' . $zero . '-scale-' . strlen($match[1]);
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/D', $value)) {
            return 'datetime-seconds';
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/D', $value)) return 'date';
        if (preg_match('/^[+-]?\d+\.(\d+)$/D', $value, $match)) {
            return 'numeric-decimal-scale-' . strlen($match[1]);
        }
        if (preg_match('/^[+-]?\d+$/D', $value)) return 'numeric-integer';
        if (preg_match('/^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/D', $value)) {
            return 'numeric-exponent';
        }
        return 'other';
    };
    $walk = static function ($value, string $path, int $depth)
        use (&$walk, &$paths, &$stringFormats, $classifyString): void {
        if ($depth > 16 || count($paths) > 1000) {
            $paths[$path . '<truncated>']['truncated'] = true;
            return;
        }
        if ($value instanceof stdClass) {
            $type = 'object';
        } elseif (is_array($value)) {
            $type = 'array';
        } elseif (is_string($value)) {
            $type = 'string';
        } elseif (is_int($value) || is_float($value)) {
            $type = 'number';
        } elseif (is_bool($value)) {
            $type = 'boolean';
        } else {
            $type = 'null';
        }
        $paths[$path][$type] = true;
        if ($type === 'string') {
            $stringFormats[$path][$classifyString($value)] = true;
        }
        if ($type === 'object') {
            $fields = get_object_vars($value);
            ksort($fields);
            foreach ($fields as $key => $child) {
                if ($path === '$' && $key === 'trace_id') continue;
                // Dynamic object keys could themselves contain user data; redact them.
                $safeKey = preg_match('/^[A-Za-z_][A-Za-z_0-9]*$/D', (string)$key)
                    ? (string)$key : '<dynamic-key>';
                $walk($child, $path . '.' . $safeKey, $depth + 1);
            }
        } elseif ($type === 'array') {
            foreach ($value as $index => $child) {
                if ($index >= 100) {
                    $paths[$path . '[]<truncated>']['truncated'] = true;
                    break;
                }
                $walk($child, $path . '[]', $depth + 1);
            }
        }
    };
    $walk($json, '$', 0);
    ksort($paths);
    $result = [];
    foreach ($paths as $path => $types) {
        $result[$path] = array_keys($types);
        sort($result[$path]);
    }
    ksort($stringFormats);
    $formats = [];
    foreach ($stringFormats as $path => $classes) {
        $formats[$path] = array_keys($classes);
        sort($formats[$path]);
    }
    return ['types' => $result, 'string_formats' => $formats];
};
$request = static function (string $action, ?string $auth, array $payload)
    use ($baseUrl, $root, $jsonContractMaps): array {
    $headers = "platform: POC\r\nContent-Type: application/x-www-form-urlencoded\r\n";
    if ($auth !== null) $headers .= 'Access-Token: ' . $auth . "\r\n";
    $timeout = (int)(getenv('POC_HTTP_TIMEOUT') ?: 30);
    if ($timeout < 5 || $timeout > 120) {
        throw new RuntimeException('Unsafe HTTP test timeout');
    }
    $context = stream_context_create(['http' => [
        'method' => 'POST', 'header' => $headers,
        'content' => http_build_query($payload),
        'timeout' => $timeout, 'ignore_errors' => true,
    ]]);
    $started = microtime(true);
    $raw = file_get_contents($baseUrl . '/api/meal.meal/' . $action
        . '?storeId=10001', false, $context);
    $elapsedMs = (int)round((microtime(true) - $started) * 1000);
    $httpLine = $http_response_header[0] ?? '';
    $decoded = is_string($raw) ? json_decode($raw, true) : null;
    if (!is_array($decoded)) throw new RuntimeException('Non-JSON meal diary response: '
        . $action . ' ' . $httpLine);
    unset($decoded['trace_id']);
    $decoded['message'] = str_replace($root, '[runtime]', (string)($decoded['message'] ?? ''));
    $maps = $jsonContractMaps($raw);
    return ['http' => (int)(preg_match('/HTTP\/\S+\s+(\d+)/', $httpLine, $m)
        ? $m[1] : 0), 'elapsed_ms' => $elapsedMs, 'body' => $decoded,
        'json_type_map' => $maps['types'],
        'json_string_format_map' => $maps['string_formats']];
};
$status = static function (array $response): int {
    return (int)($response['body']['status'] ?? -1);
};
$shape = static function (array $response): array {
    $body = $response['body'];
    return ['http' => $response['http'], 'elapsed_ms' => $response['elapsed_ms'],
        'status' => $body['status'] ?? null,
        'message' => $body['message'] ?? '', 'body_keys' => array_keys($body),
        'data_type' => gettype($body['data'] ?? null),
        'data_keys' => is_array($body['data'] ?? null) ? array_keys($body['data']) : [],
        'json_type_map' => $response['json_type_map'],
        'json_string_format_map' => $response['json_string_format_map']];
};
$dish = $read($ai, "SELECT ID,WEIGHT,SOURCE FROM $aDish WHERE ID=? AND IS_DELETE=0",
    [$dishId]);
$mapped = $read($store, "SELECT UUID FROM $sDish WHERE AI_DISHES_ID=? AND IS_DELETE=0",
    [$dishId]);
$foodsCount = $count($ai, 'SELECT COUNT(*) FROM ' . $aDishFoods . ' df JOIN '
    . $aFoods . ' f ON f.ID=df.FOODS_ID WHERE df.DISHES_ID=?', [$dishId]);
$assert((int)($dish['id'] ?? 0) === $dishId && (float)$dish['weight'] > 0
    && in_array((int)$dish['source'], [1, 2], true)
    && (string)($mapped['uuid'] ?? '') !== '' && $foodsCount > 0,
    'Existing mapped dish/food fixture not available in both business databases');
$dishUuid = (string)$mapped['uuid'];
$mealRows = static function () use ($count, $ai, $aMeal, &$aiStaffId, $mealDate): int {
    return $count($ai, "SELECT COUNT(*) FROM $aMeal WHERE STAFF_ID=? AND MEAL_DATE=?",
        [$aiStaffId, $mealDate]);
};
$infoRows = static function () use ($count, $ai, $aInfos, &$aiStaffId, $mealDate): int {
    return $count($ai, "SELECT COUNT(*) FROM $aInfos WHERE STAFF_ID=? AND MEAL_DATE=?",
        [$aiStaffId, $mealDate]);
};
$menuRows = static function () use ($count, $store, $sDateMenu, $staffUuid, $mealDate): int {
    return $count($store, "SELECT COUNT(*) FROM $sDateMenu WHERE STAFF_UUID=? AND MENU_DATE=?",
        [$staffUuid, $mealDate]);
};
$form = ['meal_date' => $mealDate, 'meal_times' => $mealTimes, 'type' => 1,
    'dishes' => [['dishes_id' => $dishId, 'dishes_weight' => '1000',
        'dishes_number' => '10']]];
try {
    $insertUser = $ai->prepare("INSERT INTO $aUser"
        . ' (STORE_ID,MOBILE,NICK_NAME,BALANCE,GIFT_BALANCE,IS_DELETE,CREATE_TIME)'
        . ' VALUES (10001,?,?,?,?,0,?)');
    $insertUser->execute(['', $marker, '0.00', '0.00', time()]);
    $aiId = (int)$ai->lastInsertId();
    $insertAiStaff = $ai->prepare("INSERT INTO $aStaff"
        . " (NAME,USER_ID,STORE_ID,HEIGHT,WEIGHT,GENDER,BIRTHDAY,PHONE,IS_DELETE)"
        . " VALUES (?,?,10001,170,60,1,?,'',0)");
    $insertAiStaff->execute([$marker, $aiId, '1990-01-01']);
    $aiStaffId = (int)$ai->lastInsertId();
    $now = date('Y-m-d H:i:s');
    $insertStoreStaff = $store->prepare("INSERT INTO $sStaff"
        . ' (UUID,STATION_UUID,NAME,BIRTHDAY,SPORT_UUID,SPORT_EVENT_UUID,IN_TIME,'
        . 'CREATE_TIME,CREATE_BY,PERSONNEL_UMBER,PERMANENT_ADDRESS,COACH_NAME,SPORT_YEAR,'
        . 'IS_SHOW,AI_USER_ID) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,1,?)');
    $insertStoreStaff->execute([$staffUuid, 'CDXPARDIET-STATION', $marker,
        '1990-01-01', 'CDXPARDIET-SPORT', '[]', date('Y-m-d'), $now,
        'CODEX', $marker, 'CODEX TEST', 'CODEX TEST', '0', $aiId]);
    $storeStaffId = (int)$store->lastInsertId();
    $assert(min($aiId, $aiStaffId, $storeStaffId) > 0
        && $mealRows() === 0 && $infoRows() === 0 && $menuRows() === 0,
        'Synthetic user/staff precondition or empty diary slot failed');
    \think\facade\Cache::set($token, ['user' => ['user_id' => $aiId],
        'password_version' => 0, 'login_type' => 'thirdparty'], 300);

    $unauth = $request('add', null, ['form' => $form]);
    $assert($status($unauth) === 401 && $mealRows() === 0 && $menuRows() === 0,
        'Unauthenticated diary add persisted data');
    $invalid = $request('add', $token, ['form' => [
        'meal_date' => $mealDate, 'meal_times' => $mealTimes, 'dishes' => []]]);
    $assert($status($invalid) !== 200 && $mealRows() === 0 && $menuRows() === 0,
        'Missing dishes accepted or persisted');
    $emptyInfos = $request('infos', $token,
        ['meal_date' => $mealDate, 'meal_times' => $mealTimes]);
    $assert($status($emptyInfos) === 200
        && empty($emptyInfos['body']['data']['list']),
        'Empty meal info boundary failed: '
            . json_encode($shape($emptyInfos), JSON_UNESCAPED_UNICODE));
    if (getenv('POC_ONLY_EMPTY') === '1') {
        echo json_encode(['dialect' => $dialect, 'status' => 'empty-infos-only',
            'marker' => $marker, 'response' => $shape($emptyInfos)],
            JSON_UNESCAPED_UNICODE) . PHP_EOL;
        return;
    }

    $first = $request('add', $token, ['form' => $form]);
    $assert($status($first) === 200, 'Diary first add failed: '
        . mb_substr((string)($first['body']['message'] ?? ''), 0, 220));
    $meal = $read($ai, "SELECT ID,STAFF_ID,MEAL_DATE,MEAL_TIMES,IS_DELETE,ENERGY"
        . " FROM $aMeal WHERE STAFF_ID=? AND MEAL_DATE=?", [$aiStaffId, $mealDate]);
    $mealId = (int)($meal['id'] ?? 0);
    $info = $read($ai, "SELECT ID,MEAL_ID,DISHES_ID,DISHES_WEIGHT,DISHES_NUMBER,IS_DELETE"
        . " FROM $aInfos WHERE STAFF_ID=? AND MEAL_DATE=?", [$aiStaffId, $mealDate]);
    $infoId = (int)($info['id'] ?? 0);
    $menu = $read($store, "SELECT ID,DISHES_UUID,WEIGHT,NUMBER FROM $sDateMenu"
        . ' WHERE STAFF_UUID=? AND MENU_DATE=?', [$staffUuid, $mealDate]);
    $scoreLogCount = $count($ai, "SELECT COUNT(*) FROM $aScoreLog"
        . ' WHERE USER_ID=? AND TYPE=2 AND DATE=? AND MEAL_TIMES=?',
        [$aiId, $mealDate, $mealTimes]);
    $score = $read($ai, "SELECT SCORE FROM $aScore WHERE USER_ID=? AND TYPE=2",
        [$aiId]);
    $assert($mealId > 0 && $infoId > 0 && $mealRows() === 1 && $infoRows() === 1
        && $menuRows() === 1 && (int)$meal['is_delete'] === 0
        && (int)$info['meal_id'] === $mealId && (int)$info['dishes_id'] === $dishId
        && (float)$info['dishes_weight'] === 1000.0
        && (float)$info['dishes_number'] === 10.0
        && $menu['dishes_uuid'] === $dishUuid
        && (float)$menu['weight'] === 1000.0
        && (float)$menu['number'] === 10.0 && $scoreLogCount === 1
        && (int)($score['score'] ?? 0) === 1,
        'Diary first add Store/AI original-table readback mismatch');

    $repeatAdd = $request('add', $token, ['form' => $form]);
    $detailTwice = $read($ai, "SELECT ID,DISHES_WEIGHT,DISHES_NUMBER FROM $aInfos"
        . ' WHERE ID=?', [$infoId]);
    $menuTwice = $read($store, "SELECT WEIGHT,NUMBER FROM $sDateMenu"
        . ' WHERE STAFF_UUID=? AND MENU_DATE=?', [$staffUuid, $mealDate]);
    $assert($status($repeatAdd) === 200 && $mealRows() === 1 && $infoRows() === 1
        && $menuRows() === 1 && (float)$detailTwice['dishes_weight'] === 2000.0
        && (float)$detailTwice['dishes_number'] === 20.0
        && (float)$menuTwice['weight'] === 2000.0
        && (float)$menuTwice['number'] === 20.0
        && $count($ai, "SELECT COUNT(*) FROM $aScoreLog WHERE USER_ID=? AND TYPE=2"
            . ' AND DATE=? AND MEAL_TIMES=?', [$aiId, $mealDate, $mealTimes]) === 1,
        'Repeat diary add did not accumulate exactly once or duplicated points');

    $infos = $request('infos', $token,
        ['meal_date' => $mealDate, 'meal_times' => $mealTimes]);
    $calendar = $request('calendar', $token,
        ['start_date' => $mealDate, 'end_date' => $mealDate]);
    $index = $request('index', $token, ['date' => $mealDate]);
    $infoList = (array)($infos['body']['data']['list'] ?? []);
    $calendarData = (array)($calendar['body']['data'] ?? []);
    $nutritionPaths = ['carbohydrate', 'protein', 'fat', 'zinc', 'magnesium',
        'dishes_number'];
    foreach ($nutritionPaths as $field) {
        $path = '$.data.list[].' . $field;
        $assert(isset($infos['json_type_map'][$path]),
            'Nonempty diary response omits nested contract path ' . $path);
    }
    $assert(!in_array('truncated', array_merge(...array_values($infos['json_type_map'])), true),
        'Nonempty diary type map was truncated');
    $assert($status($infos) === 200 && count($infoList) === 1
        && (int)($infoList[0]['id'] ?? 0) === $infoId
        && (int)($infos['body']['data']['weight_sum'] ?? 0) === 2000
        && ($infoList[0]['dishes_number'] ?? null) === '20'
        && $status($calendar) === 200 && count($calendarData) === 1
        && (int)($calendarData[0]['status'] ?? 0) === 1
        && $status($index) === 200
        && (int)($index['body']['data']['meal_data']['noon_power']['meal_num'] ?? 0) === 1,
        'Diary nonempty infos/calendar/index response disagreed with database: '
            . json_encode([
                'infos' => $shape($infos), 'list_count' => count($infoList),
                'list_id' => $infoList[0]['id'] ?? null,
                'expected_id' => $infoId,
                'weight_sum' => $infos['body']['data']['weight_sum'] ?? null,
                'dishes_number' => $infoList[0]['dishes_number'] ?? null,
                'calendar' => $shape($calendar),
                'calendar_count' => count($calendarData),
                'calendar_status' => $calendarData[0]['status'] ?? null,
                'index' => $shape($index),
                'noon_meal_num' => $index['body']['data']['meal_data']['noon_power']['meal_num'] ?? null,
            ], JSON_UNESCAPED_UNICODE));

    $update = $request('updateSingle', $token,
        ['id' => $infoId, 'dishes_weight' => '150', 'dishes_number' => '1']);
    $updated = $read($ai, "SELECT DISHES_WEIGHT,DISHES_NUMBER,IS_DELETE FROM $aInfos WHERE ID=?",
        [$infoId]);
    $menuUpdated = $read($store, "SELECT WEIGHT,NUMBER FROM $sDateMenu"
        . ' WHERE STAFF_UUID=? AND MENU_DATE=?', [$staffUuid, $mealDate]);
    $infosUpdated = $request('infos', $token,
        ['meal_date' => $mealDate, 'meal_times' => $mealTimes]);
    $updatedList = (array)($infosUpdated['body']['data']['list'] ?? []);
    foreach ($nutritionPaths as $field) {
        $path = '$.data.list[].' . $field;
        $assert(isset($infosUpdated['json_type_map'][$path]),
            'Updated diary response omits nested contract path ' . $path);
    }
    $assert($status($update) === 200 && (float)$updated['dishes_weight'] === 150.0
        && (float)$updated['dishes_number'] === 1.5
        && (int)$updated['is_delete'] === 0
        && (float)$menuUpdated['weight'] === 150.0
        && (float)$menuUpdated['number'] === 1.5
        && $status($infosUpdated) === 200
        && ($updatedList[0]['dishes_number'] ?? null) === '1.5',
        'Diary single-weight update Store/AI readback mismatch');
    $invalidUpdate = $request('updateSingle', $token,
        ['id' => 0, 'dishes_weight' => '150', 'dishes_number' => '1']);
    $assert($status($invalidUpdate) !== 200
        && (float)$read($ai, "SELECT DISHES_WEIGHT FROM $aInfos WHERE ID=?",
            [$infoId])['dishes_weight'] === 150.0,
        'Invalid meal-info ID accepted or changed real row');

    $deleted = $request('delSingle', $token, ['id' => $infoId]);
    $afterDelete = $read($ai, "SELECT IS_DELETE FROM $aInfos WHERE ID=?", [$infoId]);
    $infosAfter = $request('infos', $token,
        ['meal_date' => $mealDate, 'meal_times' => $mealTimes]);
    $assert($status($deleted) === 200 && (int)$afterDelete['is_delete'] === 1
        && $menuRows() === 0 && $status($infosAfter) === 200
        && empty($infosAfter['body']['data']['list']),
        'Diary delete did not soft-delete AI detail and remove Store date menu');
    $repeatDelete = $request('delSingle', $token, ['id' => $infoId]);
    $assert($status($repeatDelete) !== 200 && $menuRows() === 0,
        'Repeat diary delete unexpectedly succeeded or recreated menu');

    echo json_encode([
        'dialect' => $dialect, 'status' => 'passed', 'marker' => $marker,
        'mapped_dish_id' => $dishId, 'food_relation_count' => $foodsCount,
        'meal_id' => $mealId, 'meal_infos_id' => $infoId,
        'first_rows' => ['ai_meal' => 1, 'ai_meal_infos' => 1,
            'store_date_menu' => 1, 'score_log' => $scoreLogCount],
        'initial_quantity' => 10, 'repeat_quantity_response' => '20',
        'updated_quantity_response' => '1.5',
        'repeat_weight' => 2000, 'updated_weight' => 150,
        'deleted_ai_detail_is_delete' => 1, 'store_date_menu_after_delete' => 0,
        'response_contract' => [
            'unauth' => $shape($unauth), 'invalid_add' => $shape($invalid),
            'empty_infos' => $shape($emptyInfos),
            'add' => $shape($first), 'repeat_add' => $shape($repeatAdd),
            'infos' => $shape($infos), 'calendar' => $shape($calendar),
            'index' => $shape($index), 'update' => $shape($update),
            'infos_after_update' => $shape($infosUpdated),
            'invalid_update' => $shape($invalidUpdate),
            'delete' => $shape($deleted), 'repeat_delete' => $shape($repeatDelete),
        ],
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
} finally {
    \think\facade\Cache::delete($token);
    if ($staffUuid !== '') {
        $store->prepare("DELETE FROM $sDateMenu WHERE STAFF_UUID=?")
            ->execute([$staffUuid]);
    }
    if ($storeStaffId > 0) {
        $store->prepare("DELETE FROM $sStaff WHERE ID=? AND UUID=?")
            ->execute([$storeStaffId, $staffUuid]);
    }
    if ($aiStaffId > 0) {
        $ai->prepare("DELETE FROM $aInfos WHERE STAFF_ID=?")->execute([$aiStaffId]);
        $ai->prepare("DELETE FROM $aMeal WHERE STAFF_ID=?")->execute([$aiStaffId]);
        $ai->prepare("DELETE FROM $aStaff WHERE ID=? AND USER_ID=?")
            ->execute([$aiStaffId, $aiId]);
    }
    if ($aiId > 0) {
        $ai->prepare("DELETE FROM $aScoreLog WHERE USER_ID=?")->execute([$aiId]);
        $ai->prepare("DELETE FROM $aScore WHERE USER_ID=?")->execute([$aiId]);
        $ai->prepare("DELETE FROM $aUser WHERE USER_ID=? AND NICK_NAME=?")
            ->execute([$aiId, $marker]);
    }
    $remainingStore = $count($store, "SELECT COUNT(*) FROM $sStaff WHERE UUID=?",
        [$staffUuid]) + $menuRows();
    $remainingAi = $aiId > 0
        ? $count($ai, "SELECT COUNT(*) FROM $aUser WHERE USER_ID=?", [$aiId])
            + $count($ai, "SELECT COUNT(*) FROM $aScore WHERE USER_ID=?", [$aiId])
            + $count($ai, "SELECT COUNT(*) FROM $aScoreLog WHERE USER_ID=?", [$aiId])
        : 0;
    if ($aiStaffId > 0) {
        $remainingAi += $count($ai, "SELECT COUNT(*) FROM $aStaff WHERE ID=?",
            [$aiStaffId]) + $mealRows() + $infoRows();
    }
    if ($remainingStore !== 0 || $remainingAi !== 0) {
        throw new RuntimeException('Synthetic diary cleanup left rows: store='
            . $remainingStore . ' ai=' . $remainingAi);
    }
    fwrite(STDERR, 'Synthetic meal diary cleanup: marker=' . $marker
        . ' store_rows=0 ai_rows=0' . PHP_EOL);
}
