<?php

declare(strict_types=1);

// Five bounded Store PC read routes. Default mode performs SELECT only.
set_exception_handler(static function (Throwable $e): void {
    fwrite(STDERR, 'Small5 preflight failed ' . get_class($e) . ':'
        . hash('sha256', $e->getMessage()) . PHP_EOL);
    exit(1);
});
$lane = (string)getenv('CDX_SMALL5_LANE');
$mode = (string)($argv[1] ?? '');
$targets = [
    'A' => ['/tmp/zhct-http-store-a.l5omH8', 'http://127.0.0.1:18097', 21023,
        'mysql', '0e7ffb0f59e886dcdcc4693697b4b9a5472c5c2a1ee8da40e3308e34d47c25b4',
        1, 'e8189d2c8d10'],
    'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 10001,
        'mysql', '1762e1c07ed104dc39afb77667e62439b89cefc01776a69728016ecf20e40de7',
        1, 'e8189d2c8d10'],
    'C' => ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', 112175,
        'dm', '1762e1c07ed104dc39afb77667e62439b89cefc01776a69728016ecf20e40de7',
        2, '407d05f42f03'],
];
if (!isset($targets[$lane]) || !in_array($mode, ['--preflight', '--execute'], true)
    || ($mode === '--execute'
        && ((string)getenv('CDX_SMALL5_EXECUTE') !== 'ROOT_APPROVED_SMALL5_V1'
            || (string)getenv('CDX_SMALL5_SHA') !== hash_file('sha256', __FILE__)))) {
    fwrite(STDERR, "Small5 business HTTP and fixture DML disabled\n");
    exit(78);
}
[$root, $base, $pid, $dialect, $approvedSourceDigest,
    $expectedStaffId, $expectedStaffUuidSha12] = $targets[$lane];
if ((string)getenv('POC_APP_ROOT') !== $root
    || (string)getenv('POC_BASE_URL') !== $base
    || (string)getenv('POC_DIALECT') !== $dialect || is_link($root)) {
    throw new RuntimeException('Small5 target root/URL drift');
}
$coreFiles = [
    'application/config.php' => 'f80bb19246c84a13b2000c445aa3174aed03a9e5d6666d559eabde773c6ca7a3',
    'application/database.php' => $dialect === 'dm'
        ? '86c29ff71adfe33e02f345e4f0892d3be0772c5b97912f6602c4ad54f97d780d'
        : 'e152fe35b9c792fd7edf20dc4131cf9633d8fc0bc03075b65c2836251bae2108',
    'public/router.php' => '2d444eeca77269f60826653e5d0d4a51071f7d7f1826b623dda2391dd2f35380',
    'public/index.php' => $dialect === 'dm'
        ? 'f5e26a7b4bc9bd5ced50dfba5f43972e07a692abece1d8fdde52a0713c5c8448'
        : 'c333605da346af58b46bdea84038e183f87850c1c2d162eb60ab2aa5292d776b',
];
foreach ($coreFiles as $file => $sha) {
    $path = $root . '/' . $file;
    if (!is_file($path) || is_link($path)
        || hash_file('sha256', $path) !== $sha) {
        throw new RuntimeException('Small5 isolated core config/router/index drift');
    }
}
$configRoot = $root . '/application/config';
$cacheRoot = $root . '/runtime/cache';
if (!is_dir($configRoot) || is_link($configRoot)
    || !is_dir($cacheRoot) || is_link($cacheRoot)
    || strpos((string)realpath($cacheRoot), $root . '/runtime/cache') !== 0) {
    throw new RuntimeException('Small5 isolated config/cache directory drift');
}
$configFiles = [];
$configIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
    $configRoot, FilesystemIterator::SKIP_DOTS));
foreach ($configIterator as $file) {
    if ($file->isLink() || !$file->isFile()) {
        throw new RuntimeException('Small5 config tree has non-file/symlink');
    }
    $relative = substr($file->getPathname(), strlen($configRoot) + 1);
    $configFiles[] = $relative . ':' . hash_file('sha256', $file->getPathname());
}
sort($configFiles, SORT_STRING);
$expectedConfig = $lane === 'A'
    ? [62, '631ed07380400b9bf3f69f11f67699d287fe784677dadc3a721ded34eab43d9c']
    : ($lane === 'B'
        ? [64, '8e7f8932cdccefff532273b886488070e5b5f97c4810408d488cd7064cc3f303']
        : [2, '460c5f4141aafb21679669ab037c77217ac28c964732bf858632a7bae967f15b']);
if (count($configFiles) !== $expectedConfig[0]
    || hash('sha256', implode('|', $configFiles)) !== $expectedConfig[1]) {
    throw new RuntimeException('Small5 isolated config tree drift');
}
$files = ['application/base/AdminController.php', 'application/common.php',
    'application/route.php', 'application/p/controller/MealConfig.php',
    'application/p/controller/Ontoanalysis.php', 'application/p/controller/Role.php',
    'application/p/controller/MetabolismPal.php', 'application/p/logic/MealConfig.php',
    'application/p/logic/Cater.php', 'application/p/logic/Ontoanalysis.php',
    'application/p/logic/Role.php', 'application/p/logic/MetabolismPal.php',
    'application/model/Staff.php', 'application/model/Config.php',
    'application/model/Station.php', 'application/model/Cater.php',
    'application/model/Sport.php', 'application/model/StaffDepartment.php',
    'application/model/User.php', 'application/model/BaseModel.php'];
$sourceHashes = [];
foreach ($files as $file) {
    $sha = hash_file('sha256', $root . '/' . $file);
    if (!is_string($sha)) throw new RuntimeException('Small5 source file absent');
    $sourceHashes[] = $sha;
}
if (hash('sha256', implode('|', $sourceHashes)) !== $approvedSourceDigest) {
    throw new RuntimeException('Small5 source bundle drift');
}
$ontoLogicSha = $lane === 'A'
    ? 'e06744a7463a59c3563e3b6d405dfd287da32b879460b7cb5bd81a18e3d40ec0'
    : 'fa80caf1c0f9975341181ed9dadcca583736125caf147c237ef698684afc60f6';
if (hash_file('sha256', $root . '/application/p/logic/Ontoanalysis.php')
    !== $ontoLogicSha) {
    throw new RuntimeException('Small5 StaffInfo height formatter source drift');
}
$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) {
        $part = preg_split('/\s+/', trim($line));
        if (($part[1] ?? '') === '0100007F:' . $hex && ($part[3] ?? '') === '0A') {
            $inode = (string)($part[9] ?? '');
            break;
        }
    }
    $owns = false;
    if ($inode !== '') foreach (@scandir('/proc/' . $pid . '/fd') ?: [] as $fd) {
        if ($fd !== '.' && $fd !== '..'
            && @readlink('/proc/' . $pid . '/fd/' . $fd) === 'socket:[' . $inode . ']') {
            $owns = 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
        && $owns && is_array($boundary) && !empty($boundary['external_stubs'])
        && !empty($boundary['redis_isolated']) && empty($boundary['real_queue']);
};
if (!$runtimeReady()) throw new RuntimeException('Small5 isolated HTTP boundary absent');
$runtime = require $root . '/application/database.php';
if (!is_array($runtime) || (string)($runtime['prefix'] ?? '') !== 'ydy_') {
    throw new RuntimeException('Small5 DB config invalid');
}
if ($dialect === 'mysql') {
    $local = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
    if (!is_array($local) || (string)($runtime['type'] ?? '') !== 'mysql'
        || !in_array((string)($runtime['charset'] ?? ''), ['utf8', 'utf8mb4'], true)) {
        throw new RuntimeException('Small5 code-local MySQL required');
    }
    foreach (['hostname', 'database', 'username', 'password'] as $key) {
        if (($runtime[$key] ?? null) !== ($local[$key] ?? null)) {
            throw new RuntimeException('Small5 runtime differs from code local');
        }
    }
    if ((int)($runtime['hostport'] ?? 3306) !== (int)($local['hostport'] ?? 3306)) {
        throw new RuntimeException('Small5 MySQL port mismatch');
    }
    $dsn = 'mysql:host=' . $runtime['hostname'] . ';port='
        . (int)($runtime['hostport'] ?? 3306) . ';dbname=' . $runtime['database']
        . ';charset=' . $runtime['charset'];
    $approvedDbSha = '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401';
    $hostSha = null;
} 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('Small5 nonproduction DM target drift');
    }
    foreach (['hostname' => 'POC_HOST', 'hostport' => 'POC_PORT',
        'database' => 'POC_SCHEMA', 'username' => 'POC_USER',
        'password' => 'POC_PASSWORD'] as $key => $env) {
        if ((string)($runtime[$key] ?? '') !== (string)getenv($env)) {
            throw new RuntimeException('Small5 DM worker context drift');
        }
    }
    $machine = trim((string)@file_get_contents('/etc/machine-id'));
    if ($machine === '' || gethostname() === false) {
        throw new RuntimeException('Small5 DM host unknown');
    }
    $hostSha = hash('sha256', gethostname() . '|' . $machine);
    if ($hostSha !== 'd56d81988bb0f106946cce91c608fdd0ef1116e5150b23376ea87e086eace4b9') {
        throw new RuntimeException('Small5 DM host mismatch');
    }
    $lines = [];
    $exit = 0;
    @exec('/usr/sbin/ss -ltnp 2>/dev/null', $lines, $exit);
    $dmLocal = false;
    if ($exit === 0) foreach ($lines as $line) {
        if (preg_match('/\*:5237\s+.*users:\(\("dmserver",pid=(\d+),/', $line, $m)) {
            $exe = @readlink('/proc/' . $m[1] . '/exe');
            $dmLocal = is_string($exe) && basename($exe) === 'dmserver';
            break;
        }
    }
    if (!$dmLocal) throw new RuntimeException('Small5 DM listener not local');
    $dsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
    $approvedDbSha = 'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194';
}
$connect = static function () use ($dsn, $runtime): PDO {
    return 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]);
};
try { $db = $connect(); }
catch (Throwable $e) {
    fwrite(STDERR, 'Small5 approved DB unavailable: ' . get_class($e) . PHP_EOL);
    exit(75);
}
$identity = static function (PDO $pdo) use ($dialect): string {
    return $dialect === 'dm'
        ? 'ZHCT_APP|5237|' . (string)$pdo->query(
            "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn()
        : (string)$pdo->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
            ->fetchColumn();
};
if (!hash_equals($approvedDbSha, hash('sha256', $identity($db)))) {
    throw new RuntimeException('Small5 DB identity mismatch');
}
if ($mode === '--execute'
    && ((string)getenv('CDX_SMALL5_DB_SHA') !== $approvedDbSha
        || ($dialect === 'dm'
            && (string)getenv('CDX_SMALL5_HOST_SHA') !== $hostSha))) {
    throw new RuntimeException('Small5 explicit DB/host approval absent');
}
$p = $dialect === 'dm' ? 'YDY_' : 'ydy_';
$read = 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;
    }
    return $rows;
};
$count = static function (PDO $pdo, string $sql, array $args = []): int {
    $stmt = $pdo->prepare($sql);
    $stmt->execute($args);
    return (int)$stmt->fetchColumn();
};
$keys = ['morning_start', 'morning_end', 'noon_start', 'noon_end',
    'night_start', 'night_end'];
$configSql = 'SELECT CONFIG_KEY,CONFIG_VALUE FROM ' . $p . 'config'
    . ' WHERE STATUS=1 AND CONFIG_KEY IN (?,?,?,?,?,?) ORDER BY CONFIG_KEY';
$configRows = $read($db, $configSql, $keys);
$configMap = [];
foreach ($configRows as $row) $configMap[(string)$row['config_key']] = (string)$row['config_value'];
if (count($configRows) !== 6 || count($configMap) !== 6) {
    throw new RuntimeException('Small5 six meal-config source keys absent/duplicated');
}
foreach ($configMap as $value) if (strtotime($value) === false) {
    throw new RuntimeException('Small5 meal-config time invalid');
}
$nearMidnight = date('H:i') >= '23:58' || date('H:i') <= '00:02';
if ($mode === '--execute' && $nearMidnight) {
    throw new RuntimeException('Small5 date-derived meal config near midnight');
}
$dishWhere = ' FROM ' . $p . 'dishes WHERE STORE_ID=10001 AND STATUS=1'
    . ' AND IS_DEL=1 AND IS_DELETE=0 AND AI_DISHES_ID>0';
$dishTotal = $count($db, 'SELECT COUNT(*)' . $dishWhere);
$dishSql = 'SELECT ID,UUID,AI_DISHES_ID,NAME,WEIGHT,ENERGY,PROTEIN,ZINC,MAGNESIUM'
    . $dishWhere . ' ORDER BY NAME,ID'
    . ($dialect === 'dm' ? ' FETCH FIRST 11 ROWS ONLY' : ' LIMIT 11');
$dishes = $read($db, $dishSql);
$stations = $read($db, 'SELECT UUID,NAME FROM ' . $p . 'station WHERE STATUS=1');
$visibleStaff = $count($db, 'SELECT COUNT(*) FROM ' . $p . 'staff WHERE IS_SHOW=1');
$candidateSql = 'SELECT s.ID,s.UUID,s.NAME,s.SPORT_UUID,s.BIRTHDAY'
    . ' FROM ' . $p . 'staff s JOIN ' . $p . 'sport sp ON sp.UUID=s.SPORT_UUID'
    . ' WHERE s.ID=? AND s.IS_SHOW=1 AND s.JOB_STATUS=1'
    . ' AND EXISTS(SELECT 1 FROM ' . $p . 'staff_department sd'
    . ' WHERE sd.STAFF_UUID=s.UUID) ORDER BY s.ID'
    . ($dialect === 'dm' ? ' FETCH FIRST 1 ROW ONLY' : ' LIMIT 1');
$candidateRows = $read($db, $candidateSql, [$expectedStaffId]);
$staff = $candidateRows[0] ?? null;
if ($dishTotal < 1000 || count($dishes) !== 11
    || count($stations) < 1 || count($stations) > 20 || $visibleStaff < 1000
    || count($candidateRows) !== 1 || (int)$staff['id'] !== $expectedStaffId
    || substr(hash('sha256', (string)$staff['uuid']), 0, 12)
        !== $expectedStaffUuidSha12
    || preg_match('/^[A-Fa-f0-9-]{36}$/D', (string)$staff['uuid']) !== 1
    || preg_match('/[%_\\\\]/', (string)$staff['name'])) {
    throw new RuntimeException('Small5 natural source count/candidate drift');
}
$staffInfoSql = 'SELECT a.UUID,a.NAME,a.HEAD_IMG,a.SEX,a.HEIGHT,a.BIRTHDAY,a.WEIGHT,'
    . ' f.UUID D_UUID,f.NAME D_NAME,c.NAME S_NAME,a.SPORT_EVENT_UUID,'
    . ' IFNULL(b.STATUS,2) STATUS,a.GRADE,a.SPORT_UUID'
    . ' FROM ' . $p . 'staff a'
    . ' LEFT JOIN ' . $p . 'member_weight_status b ON a.UUID=b.STAFF_UUID'
    . ' LEFT JOIN ' . $p . 'sport c ON a.SPORT_UUID=c.UUID'
    . ' LEFT JOIN ' . $p . 'staff_department sd ON a.UUID=sd.STAFF_UUID'
    . ' LEFT JOIN ' . $p . 'department f ON sd.DEPARTMENT_UUID=f.UUID'
    . ' WHERE a.UUID=? ORDER BY b.ID DESC'
    . ($dialect === 'dm' ? ' FETCH FIRST 1 ROW ONLY' : ' LIMIT 1');
$infoRows = $read($db, $staffInfoSql, [(string)$staff['uuid']]);
$queryWhere = ' FROM ' . $p . 'staff a LEFT JOIN ' . $p . 'sport c'
    . ' ON a.SPORT_UUID=c.UUID WHERE a.IS_SHOW=1 AND a.SPORT_UUID=?'
    . ' AND a.NAME LIKE ?';
$queryArgs = [(string)$staff['sport_uuid'], '%' . (string)$staff['name'] . '%'];
$queryTotal = $count($db, 'SELECT COUNT(*)' . $queryWhere, $queryArgs);
$querySql = 'SELECT a.UUID,a.SPORT_UUID,a.SPORT_EVENT_UUID,a.PERSONNEL_UMBER,'
    . ' a.NAME,a.SEX,a.PAL_MTIME,a.PAL_BY_NAME,c.NAME L_NAME'
    . $queryWhere
    . ($dialect === 'dm' ? ' FETCH FIRST 11 ROWS ONLY' : ' LIMIT 11');
$queryRows = $read($db, $querySql, $queryArgs);
if (count($infoRows) !== 1 || $queryTotal < 1 || $queryTotal > 10
    || count($queryRows) !== $queryTotal || strlen(serialize($queryRows)) > 64 * 1024) {
    throw new RuntimeException('Small5 joined staff/querylist source drift');
}
$relatedNames = static function (PDO $pdo, string $uuid, string $sportEvents)
    use ($read, $count, $p): array {
    if ($count($pdo, 'SELECT COUNT(*) FROM ' . $p . 'staff WHERE UUID=?', [$uuid]) !== 1) {
        throw new RuntimeException('Small5 related staff UUID ambiguous');
    }
    $departments = $read($pdo, 'SELECT dep.NAME FROM ' . $p . 'staff_department sd'
        . ' LEFT JOIN ' . $p . 'department dep ON dep.UUID=sd.DEPARTMENT_UUID'
        . ' WHERE sd.STAFF_UUID=?', [$uuid]);
    $sportIds = array_values(array_filter(array_map('trim', explode(',', $sportEvents)),
        static function (string $x): bool { return $x !== ''; }));
    $sports = [];
    if ($sportIds) {
        $sports = $read($pdo, 'SELECT NAME FROM ' . $p . 'sport WHERE UUID IN ('
            . implode(',', array_fill(0, count($sportIds), '?')) . ')', $sportIds);
    }
    return ['department_names' => array_map(static function (array $x): string {
            return (string)($x['name'] ?? '');
        }, $departments),
        'sport_names' => array_map(static function (array $x): string {
            return (string)($x['name'] ?? '');
        }, $sports)];
};
$infoRelated = $relatedNames($db, (string)$infoRows[0]['uuid'],
    (string)($infoRows[0]['sport_event_uuid'] ?? ''));
$queryRelated = [];
foreach ($queryRows as $row) {
    $queryRelated[(string)$row['uuid']] = $relatedNames($db,
        (string)$row['uuid'], (string)($row['sport_event_uuid'] ?? ''));
}
$source = ['config' => $configRows, 'dishes' => $dishes,
    'stations' => $stations, 'staff' => $staff,
    'staff_info' => $infoRows[0], 'staff_info_related' => $infoRelated,
    'querylist' => $queryRows, 'querylist_related' => $queryRelated,
    'totals' => [$dishTotal, $visibleStaff, $queryTotal]];
$sourceSha = hash('sha256', serialize($source));
$sourceTableCounts = [];
foreach (['config', 'dishes', 'station', 'staff', 'staff_department', 'sport',
    'member_weight_status', 'department'] as $table) {
    $sourceTableCounts[$table] = $count($db, 'SELECT COUNT(*) FROM ' . $p . $table);
}
$actorRows = $count($db, 'SELECT COUNT(*) FROM ' . $p . 'user');
$stale = $count($db, 'SELECT COUNT(*) FROM ' . $p . 'user'
    . " WHERE ACCOUNT LIKE 'CDXSM5%'");
if ($stale !== 0) throw new RuntimeException('Small5 previous actor residue');
if ($dialect === 'dm') {
    $stmt = $db->prepare('CALL SP_TABLEDEF(?, ?)');
    $stmt->execute(['ZHCT_APP', 'YDY_USER']);
    $ddl = $stmt->fetchColumn();
    if (is_resource($ddl)) $ddl = stream_get_contents($ddl);
    $autoId = preg_match('/"ID"\s+[A-Z0-9]+\s+AUTO_INCREMENT\s+NOT\s+NULL/i',
        (string)$ddl) === 1;
    $trigger = $count($db, 'SELECT COUNT(*) FROM USER_TRIGGERS WHERE TABLE_NAME=?',
        ['YDY_USER']);
    $fk = $count($db, "SELECT COUNT(*) FROM USER_CONSTRAINTS"
        . " WHERE TABLE_NAME=? AND CONSTRAINT_TYPE='R'", ['YDY_USER'])
        + $count($db, 'SELECT COUNT(*) FROM ALL_CONSTRAINTS c'
            . ' JOIN ALL_CONSTRAINTS parent'
            . ' ON c.R_OWNER=parent.OWNER AND c.R_CONSTRAINT_NAME=parent.CONSTRAINT_NAME'
            . " WHERE c.CONSTRAINT_TYPE='R' AND parent.OWNER='ZHCT_APP'"
            . ' AND parent.TABLE_NAME=?', ['YDY_USER']);
} else {
    $autoId = $count($db, 'SELECT COUNT(*) FROM information_schema.COLUMNS'
        . ' WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND COLUMN_NAME=?'
        . " AND EXTRA LIKE '%auto_increment%'", ['ydy_user', 'id']) === 1;
    $trigger = $count($db, 'SELECT COUNT(*) FROM information_schema.TRIGGERS'
        . ' WHERE TRIGGER_SCHEMA=DATABASE() AND EVENT_OBJECT_TABLE=?', ['ydy_user']);
    $fk = $count($db, 'SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE'
        . ' WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=?'
        . ' AND REFERENCED_TABLE_NAME IS NOT NULL', ['ydy_user'])
        + $count($db, 'SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE'
            . ' WHERE REFERENCED_TABLE_SCHEMA=DATABASE()'
            . ' AND REFERENCED_TABLE_NAME=?', ['ydy_user']);
}
if (!$autoId || $trigger !== 0 || $fk !== 0) {
    throw new RuntimeException('Small5 actor table identity/FK/trigger drift');
}
$types = static function (array $row): array {
    $out = array_map('gettype', $row);
    ksort($out);
    return $out;
};
$aStaffInfoTypes = ['age' => 'integer', 'birthday' => 'string',
    'd_name' => 'string', 'd_uuid' => 'string', 'grade' => 'integer',
    'head_img' => 'string', 'height' => 'string', 'name' => 'string',
    's_name' => 'string', 'sex' => 'integer',
    'sport_event_uuid' => 'string', 'sport_uuid' => 'string',
    'status' => 'string', 'uuid' => 'string',
    'weight' => 'number-by-value', 'y_name' => 'string'];
$pre = ['lane' => $lane, 'dialect' => $dialect, 'mode' => 'select-only-preflight',
    'approved_database_identity' => true, 'approved_source_digest' => true,
    'approved_staffinfo_height_source_sha' => true,
    'core_config_router_cache_boundary' => true,
    'config_file_count' => count($configFiles),
    'config_keys' => count($configMap), 'dish_options_total' => $dishTotal,
    'dish_page_sample' => count($dishes), 'station_rows' => count($stations),
    'visible_staff_total' => $visibleStaff,
    'staff_candidate_id' => (int)$staff['id'],
    'staff_uuid_sha12' => $expectedStaffUuidSha12,
    'querylist_rows' => $queryTotal,
    'near_midnight_execution_block' => $nearMidnight,
    'source_sha' => $sourceSha, 'source_table_counts' => $sourceTableCounts,
    'staff_info_source_types' => $types($infoRows[0]),
    'a_staff_info_http_type_contract' => $aStaffInfoTypes,
    'querylist_first_source_types' => $types($queryRows[0]),
    'actor_autoincrement_fk0_trigger0' => true,
    'old_actor_rows' => $stale,
    'business_http_executed' => false, 'dml_executed' => false];
if ($mode === '--preflight') {
    echo json_encode($pre, JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}

// Only the self-SHA and explicit DB/host approved execution branch reaches here.
$marker = 'CDXSM5' . strtoupper(bin2hex(random_bytes(6)));
if ($count($db, 'SELECT COUNT(*) FROM ' . $p . 'user'
    . ' WHERE UUID=? OR ACCOUNT=?', [$marker, $marker]) !== 0) {
    throw new RuntimeException('Small5 random marker collision');
}
$privateDir = sys_get_temp_dir() . '/zhct-small5-' . $marker;
if (file_exists($privateDir) || !mkdir($privateDir, 0700)) {
    throw new RuntimeException('Small5 private journal directory unavailable');
}
chmod($privateDir, 0700);
$journalPath = $privateDir . '/recovery.json';
$journal = ['lane' => $lane, 'marker' => $marker,
    'source_sha' => $sourceSha, '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('Small5 journal write failed');
    }
    chmod($next, 0600);
    if (!rename($next, $journalPath)) throw new RuntimeException('Small5 journal rename failed');
};
$saveJournal();
$actorId = 0;
$httpSent = false;
$httpConfirmed = true;
$results = [];
$sourceUnchanged = false;
$cleaned = false;
$privateCleared = false;
$failure = '';
$http = static function (string $path, array $form, string $token) use ($base,
    $runtimeReady, &$httpSent, &$httpConfirmed): array {
    if (!$runtimeReady()) throw new RuntimeException('Small5 HTTP runtime drift');
    $context = stream_context_create(['http' => ['method' => 'POST',
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
            . 'token: ' . $token . "\r\n",
        'content' => http_build_query($form, '', '&', PHP_QUERY_RFC3986),
        'timeout' => 20, 'ignore_errors' => true]]);
    $httpConfirmed = false;
    $httpSent = true;
    $stream = @fopen($base . $path, 'rb', false, $context);
    $body = is_resource($stream) ? stream_get_contents($stream, 262145) : false;
    $meta = is_resource($stream) ? stream_get_meta_data($stream) : [];
    if (is_resource($stream)) fclose($stream);
    if (!is_string($body) || !empty($meta['timed_out']) || strlen($body) > 262144) {
        throw new RuntimeException('Small5 HTTP outcome incomplete or oversized');
    }
    $httpConfirmed = true;
    $headers = $http_response_header ?? [];
    if (strpos((string)($headers[0] ?? ''), ' 200 ') === false) {
        throw new RuntimeException('Small5 HTTP status mismatch');
    }
    $json = json_decode($body, true);
    if (!is_array($json) || !array_key_exists('code', $json)) {
        throw new RuntimeException('Small5 JSON envelope invalid');
    }
    return $json;
};
$matchesImage = static function ($actual, $source): bool {
    $suffix = str_replace('\\', '/', (string)($source ?? ''));
    $value = (string)($actual ?? '');
    return $suffix === '' ? $value === ''
        : ($value !== '' && substr($value, -strlen($suffix)) === $suffix);
};
$expectedMetric = static function ($value) {
    if (!is_numeric($value) || (float)$value <= 0) return null;
    return round((float)$value, 2);
};
try {
    $stmt = $db->prepare('INSERT INTO ' . $p . 'user'
        . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
        . ' VALUES (?,?,?,?,?,?,?,?,?)');
    $stmt->execute([$marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
        $marker, 0, date('Y-m-d H:i:s'), 'CODEX', 0]);
    if ($stmt->rowCount() !== 1) throw new RuntimeException('Small5 actor insert count');
    $stmt = $db->prepare('SELECT ID FROM ' . $p . 'user'
        . ' WHERE UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
    $stmt->execute([$marker, $marker, 'CODEX']);
    $actorId = (int)$stmt->fetchColumn();
    if ($actorId <= 0) throw new RuntimeException('Small5 actor ID missing');
    $journal['actor_id'] = $actorId;
    $journal['state'] = 'actor_committed';
    $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('Small5 synthetic token unavailable');
    }
    $requests = [
        'getconfig' => ['/p/mealconfig/getconfig', []],
        'dishOptions' => ['/p/Ontoanalysis/FoodRecordDishOptions',
            ['page' => '1', 'page_size' => '10']],
        'staffInfo' => ['/p/Ontoanalysis/StaffInfo',
            ['staff_uuid' => (string)$staff['uuid']]],
        'station' => ['/p/role/station', []],
        'querylist' => ['/p/MetabolismPal/querylist',
            ['sport_uuid' => (string)$staff['sport_uuid'],
                'name' => (string)$staff['name'], 'page' => '1', 'pagesize' => '10']],
    ];
    foreach ($requests as $route => $request) {
        [$path, $form] = $request;
        $json = $http($path, $form, $token);
        if (($json['code'] ?? null) !== 0 || !is_array($json['data'] ?? null)) {
            throw new RuntimeException('Small5 positive business response failed');
        }
        $data = $json['data'];
        $rows = [];
        if ($route === 'getconfig') {
            if (($json['message'] ?? null) !== '获取成功') {
                throw new RuntimeException('Small5 getconfig message mismatch');
            }
            foreach ($keys as $key) {
                $value = $data[$key] ?? null;
                if (!is_string($value)
                    || $value !== date('Y-m-d H:i:s', strtotime($configMap[$key]))) {
                    throw new RuntimeException('Small5 getconfig source/type mismatch');
                }
            }
            $rowCount = count($keys);
            $rowTypes = $types($data);
        } elseif ($route === 'dishOptions') {
            if (($json['message'] ?? null) !== '查询成功'
                || !is_array($data['rows'] ?? null)
                || count($data['rows']) !== 10
                || (int)($data['page'] ?? 0) !== 1
                || (int)($data['page_size'] ?? 0) !== 10
                || ($data['has_more'] ?? null) !== true) {
                throw new RuntimeException('Small5 dish option pagination/type mismatch');
            }
            foreach (array_slice($dishes, 0, 10) as $i => $raw) {
                $row = $data['rows'][$i] ?? null;
                if (!is_array($row)
                    || !is_int($row['id'] ?? null)
                    || $row['id'] !== (int)$raw['ai_dishes_id']
                    || !is_int($row['pc_dishes_id'] ?? null)
                    || $row['pc_dishes_id'] !== (int)$raw['id']
                    || (string)($row['uuid'] ?? '') !== (string)$raw['uuid']
                    || (string)($row['name'] ?? '') !== (string)$raw['name']) {
                    throw new RuntimeException('Small5 dish option row mismatch');
                }
                foreach (['weight', 'energy', 'protein', 'zinc', 'magnesium'] as $metric) {
                    $expected = $expectedMetric($raw[$metric] ?? null);
                    $actual = $row[$metric] ?? null;
                    if ($expected === null ? $actual !== null
                        : (!is_numeric($actual) || abs((float)$actual - $expected) > 0.00001)) {
                        throw new RuntimeException('Small5 dish option metric mismatch');
                    }
                }
            }
            $rowCount = 10;
            $rowTypes = $types($data['rows'][0]);
        } elseif ($route === 'staffInfo') {
            if (($json['message'] ?? null) !== '获取信息成功') {
                throw new RuntimeException('Small5 staff info message mismatch');
            }
            $raw = $infoRows[0];
            foreach (['uuid', 'name', 'sex', 'height', 'birthday', 'weight',
                'd_uuid', 'd_name', 's_name', 'status', 'grade', 'sport_uuid'] as $key) {
                if ($key === 'height') {
                    if (!is_string($data[$key] ?? null)
                        || preg_match('/^-?\d+\.\d{2}$/D', $data[$key]) !== 1
                        || !is_numeric($raw[$key] ?? null)
                        || bccomp($data[$key], (string)$raw[$key], 2) !== 0) {
                        throw new RuntimeException('Small5 StaffInfo height two-decimal contract mismatch');
                    }
                    continue;
                }
                if ($key === 'weight' && is_numeric($data[$key] ?? null)
                    && is_numeric($raw[$key] ?? null)
                    && bccomp((string)$data[$key], (string)$raw[$key], 2) === 0) {
                    // Original A JSON emits an integer for a .00 source value.
                    // Keep value matching exact; compare JSON types across lanes.
                    continue;
                }
                if ((string)($data[$key] ?? '') !== (string)($raw[$key] ?? '')) {
                    throw new RuntimeException('Small5 staff info row mismatch');
                }
            }
            if (!$matchesImage($data['head_img'] ?? null, $raw['head_img'] ?? null)
                || !is_int($data['age'] ?? null)
                || (string)($data['y_name'] ?? '')
                    !== implode(',', $infoRelated['sport_names'])) {
                throw new RuntimeException('Small5 staff info image/age type mismatch');
            }
            foreach ($aStaffInfoTypes as $field => $expectedType) {
                if (!array_key_exists($field, $data)) {
                    $results['staffInfo_type_mismatch'] = ['field' => $field,
                        'actual' => 'missing', 'expected' => $expectedType];
                    throw new RuntimeException('Small5 StaffInfo A-contract key/type mismatch');
                }
                if ($field === 'weight') {
                    $integral = bccomp((string)$raw['weight'],
                        bcadd((string)$raw['weight'], '0', 0), 2) === 0;
                    $expectedType = $integral ? 'integer' : 'double';
                }
                if (gettype($data[$field]) !== $expectedType) {
                    $results['staffInfo_type_mismatch'] = ['field' => $field,
                        'actual' => gettype($data[$field]), 'expected' => $expectedType];
                    throw new RuntimeException('Small5 StaffInfo A-contract key/type mismatch');
                }
            }
            $rowCount = 1;
            $rowTypes = $types($data);
        } elseif ($route === 'station') {
            if (count($data) !== count($stations)) {
                throw new RuntimeException('Small5 station count mismatch');
            }
            $expected = [];
            foreach ($stations as $raw) $expected[(string)$raw['uuid']] = (string)$raw['name'];
            foreach ($data as $row) {
                if (!is_array($row) || !isset($expected[(string)($row['uuid'] ?? '')])
                    || (string)($row['name'] ?? '') !== $expected[(string)$row['uuid']]) {
                    throw new RuntimeException('Small5 station row mismatch');
                }
                unset($expected[(string)$row['uuid']]);
            }
            if ($expected) throw new RuntimeException('Small5 station set mismatch');
            $rowCount = count($data);
            $rowTypes = $types($data[0]);
        } else {
            if (($json['message'] ?? null) !== '查询成功'
                || !is_array($data) || count($data) !== $queryTotal
                || (int)($json['total'] ?? -1) !== $queryTotal) {
                throw new RuntimeException('Small5 querylist count/envelope mismatch');
            }
            $expected = [];
            foreach ($queryRows as $raw) $expected[(string)$raw['uuid']] = $raw;
            foreach ($data as $row) {
                if (!is_array($row) || !isset($expected[(string)($row['uuid'] ?? '')])) {
                    throw new RuntimeException('Small5 querylist UUID set mismatch');
                }
                $raw = $expected[(string)$row['uuid']];
                foreach (['uuid', 'sport_uuid', 'sport_event_uuid',
                    'personnel_umber', 'name', 'sex', 'pal_mtime', 'pal_by_name',
                    'l_name'] as $key) {
                    if ((string)($row[$key] ?? '') !== (string)($raw[$key] ?? '')) {
                        throw new RuntimeException('Small5 querylist core row mismatch');
                    }
                }
                $related = $queryRelated[(string)$row['uuid']];
                if (!is_string($row['b_name'] ?? null)
                    || $row['b_name'] !== implode(',', $related['department_names'])
                    || !is_string($row['x_name'] ?? null)
                    || $row['x_name'] !== implode(',', $related['sport_names'])) {
                    throw new RuntimeException('Small5 querylist related-name/source mismatch');
                }
                unset($expected[(string)$row['uuid']]);
            }
            if ($expected) throw new RuntimeException('Small5 querylist source set mismatch');
            $rowCount = count($data);
            $rowTypes = $types($data[0]);
        }
        $results[$route] = ['positive' => true, 'rows' => $rowCount,
            'top_types' => $types($json), 'data_types' => $types($data),
            'first_row_types' => $rowTypes,
            'response_sha' => hash('sha256', json_encode($json,
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))];
    }
    $fresh = $connect();
    if (!hash_equals($approvedDbSha, hash('sha256', $identity($fresh)))) {
        throw new RuntimeException('Small5 source recheck DB identity drift');
    }
    $afterConfig = $read($fresh, $configSql, $keys);
    $afterDishes = $read($fresh, $dishSql);
    $afterStation = $read($fresh,
        'SELECT UUID,NAME FROM ' . $p . 'station WHERE STATUS=1');
    $afterStaff = $read($fresh, $candidateSql, [$expectedStaffId]);
    $afterInfo = $read($fresh, $staffInfoSql, [(string)$staff['uuid']]);
    $afterQuery = $read($fresh, $querySql, $queryArgs);
    $afterInfoRelated = count($afterInfo) === 1
        ? $relatedNames($fresh, (string)$afterInfo[0]['uuid'],
            (string)($afterInfo[0]['sport_event_uuid'] ?? '')) : null;
    $afterQueryRelated = [];
    foreach ($afterQuery as $row) {
        $afterQueryRelated[(string)$row['uuid']] = $relatedNames($fresh,
            (string)$row['uuid'], (string)($row['sport_event_uuid'] ?? ''));
    }
    $afterSource = ['config' => $afterConfig, 'dishes' => $afterDishes,
        'stations' => $afterStation, 'staff' => $afterStaff[0] ?? null,
        'staff_info' => $afterInfo[0] ?? null,
        'staff_info_related' => $afterInfoRelated,
        'querylist' => $afterQuery, 'querylist_related' => $afterQueryRelated,
        'totals' => [$count($fresh, 'SELECT COUNT(*)' . $dishWhere),
            $count($fresh, 'SELECT COUNT(*) FROM ' . $p . 'staff WHERE IS_SHOW=1'),
            $count($fresh, 'SELECT COUNT(*)' . $queryWhere, $queryArgs)]];
    $sourceUnchanged = hash('sha256', serialize($afterSource)) === $sourceSha;
    foreach ($sourceTableCounts as $table => $beforeCount) {
        if ($count($fresh, 'SELECT COUNT(*) FROM ' . $p . $table) !== $beforeCount) {
            $sourceUnchanged = false;
        }
    }
    if (!$sourceUnchanged) throw new RuntimeException('Small5 natural business source changed');
} catch (Throwable $e) {
    $failure = get_class($e) . ':' . hash('sha256', $e->getMessage());
} finally {
    $workerStopped = !is_dir('/proc/' . $pid);
    $runtimeSafe = $workerStopped || $runtimeReady();
    $cleanupSafe = $runtimeSafe && ($workerStopped || !$httpSent || $httpConfirmed);
    if ($cleanupSafe) {
        try {
            $cleanupDb = $connect();
            if (!hash_equals($approvedDbSha, hash('sha256', $identity($cleanupDb)))) {
                throw new RuntimeException('Small5 cleanup DB identity drift');
            }
            $rows = $read($cleanupDb, 'SELECT ID,UUID,ACCOUNT,NAME,TYPE,CREATE_BY'
                . ' FROM ' . $p . 'user WHERE UUID=? OR ACCOUNT=?', [$marker, $marker]);
            if (count($rows) > 1) throw new RuntimeException('Small5 extra actor association');
            foreach ($rows as $row) {
                if ((string)$row['uuid'] !== $marker
                    || (string)$row['account'] !== $marker
                    || (string)$row['name'] !== $marker
                    || (int)$row['type'] !== 0
                    || (string)$row['create_by'] !== 'CODEX'
                    || ($actorId > 0 && (int)$row['id'] !== $actorId)) {
                    throw new RuntimeException('Small5 actor ownership drift');
                }
                $stmt = $cleanupDb->prepare('DELETE FROM ' . $p . 'user'
                    . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
                $stmt->execute([(int)$row['id'], $marker, $marker, 'CODEX']);
                if ($stmt->rowCount() !== 1) {
                    throw new RuntimeException('Small5 exact cleanup affected rows mismatch');
                }
            }
            $verify = $connect();
            if (!hash_equals($approvedDbSha, hash('sha256', $identity($verify)))
                || $count($verify, 'SELECT COUNT(*) FROM ' . $p . 'user'
                    . ' WHERE UUID=? OR ACCOUNT=?', [$marker, $marker]) !== 0) {
                throw new RuntimeException('Small5 independent actor zero failed');
            }
            $cleaned = true;
        } catch (Throwable $e) {
            $failure .= ($failure === '' ? '' : ';') . 'cleanup:'
                . get_class($e) . ':' . hash('sha256', $e->getMessage());
        }
    } else {
        $failure .= ($failure === '' ? '' : ';')
            . ($httpSent && !$httpConfirmed && !$workerStopped
                ? 'cleanup:uncertain_http_outcome' : 'cleanup:runtime_drift');
    }
    if ($cleaned) {
        if (is_file($journalPath)) @unlink($journalPath);
        if (is_file($privateDir . '/recovery.next')) @unlink($privateDir . '/recovery.next');
        $privateCleared = @rmdir($privateDir);
        if (!$privateCleared) $failure .= ';private_journal_not_removed';
    } else {
        $journal['state'] = 'requires_manual_recovery';
        try { $saveJournal(); } catch (Throwable $e) { /* retain old journal */ }
    }
}
$result = ['lane' => $lane, 'marker' => $marker,
    'results' => $results, 'source_unchanged' => $sourceUnchanged,
    'actor_independent_zero' => $cleaned,
    'private_journal_removed' => $privateCleared,
    'private_recovery_pointer' => $cleaned ? null : $privateDir,
    'failure' => $failure];
echo json_encode($result, JSON_UNESCAPED_SLASHES), PHP_EOL;
if ($failure !== '' || !$sourceUnchanged || !$cleaned || !$privateCleared
    || count($results) !== 5) exit(1);
