<?php
declare(strict_types=1);

// Hidden own staff + own training experience only. No file upload or annex DML.
$lane = (string)($argv[1] ?? '');
$mode = (string)($argv[2] ?? '');
if (!in_array($lane, ['B', 'C'], true)
    || !in_array($mode, ['--preflight', '--execute', '--audit'], true)
    || ($mode === '--execute' && ((string)getenv('CDX_TRAIN_APPROVAL') !== 'ROOT_APPROVED_TRAIN_BC_V1'
        || (string)getenv('CDX_TRAIN_SHA') !== hash_file('sha256', __FILE__)))) {
    fwrite(STDERR, "TRAIN default deny\n");
    exit(78);
}
$target = $lane === 'B'
    ? ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 10001,
        '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401']
    : ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', 112175,
        'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194'];
[$root, $base, $pid, $identitySha] = $target;
$stage = 'boundary';
$marker = (string)getenv('CDX_TRAIN_MARKER');
try {
    $runtimeReady = static function () use ($pid, $root, $base): bool {
        $cmdline = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
        $children = trim((string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children'));
        $boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
        return strpos($cmdline, $root . '/public') !== false
            && strpos($cmdline, (string)parse_url($base, PHP_URL_PORT)) !== false
            && $children === '' && is_array($boundary)
            && !empty($boundary['external_stubs']) && !empty($boundary['redis_isolated'])
            && empty($boundary['real_queue']);
    };
    if (!$runtimeReady()) throw new RuntimeException('worker boundary');
    $sources = [
        'application/p/controller/TrainExperience.php' => 'e6d4003a76f6b3fe29dacb63db265546e7038f346998a7ccdc9eed1328b45a45',
        'application/p/logic/TrainExperience.php' => '12e9e1987b1ea13ddd3ef7c6cb8461886ffb1bfa5010b067f64a6b64f9263bbb',
        'application/validate/TrainExperience.php' => '39be40a2ced5aa3e4f9dd8fd4c960c7629693a8ecf4a4148373962b6dadb1bd7',
        'application/model/TrainExperience.php' => '59edfc62ec3364ac67702aa7b43f1d2ecc78557c9158ae6747d4fc5c68528ccf',
        'application/model/Annex.php' => 'c5390dc2f5e9068f6f5817a5f5909ca6bb160866d6f862438f541464fb6bda95',
    ];
    foreach ($sources as $relative => $sha) {
        $path = $root . '/' . $relative;
        if (!is_file($path) || is_link($path) || !hash_equals($sha, hash_file('sha256', $path))) {
            throw new RuntimeException('business source drift');
        }
    }
    $stage = 'database-config';
    $cfg = require $root . '/application/database.php';
    if (!is_array($cfg) || ($cfg['prefix'] ?? '') !== 'ydy_') {
        throw new RuntimeException('database config');
    }
    $dialect = $lane === 'C' ? 'dm' : 'mysql';
    $dsn = $dialect === 'dm' ? 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP'
        : 'mysql:host=' . $cfg['hostname'] . ';port=' . ($cfg['hostport'] ?? 3306)
            . ';dbname=' . $cfg['database'] . ';charset=utf8mb4';
    $db = new PDO($dsn, (string)$cfg['username'], (string)$cfg['password'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
    $identity = $dialect === 'dm'
        ? 'ZHCT_APP|5237|' . (string)$db->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn()
        : (string)$db->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")->fetchColumn();
    if (!hash_equals($identitySha, hash('sha256', $identity))) {
        throw new RuntimeException('database identity');
    }
    $prefix = $dialect === 'dm' ? 'YDY_' : 'ydy_';
    $staffTable = $prefix . 'staff';
    $userTable = $prefix . 'user';
    $trainTable = $prefix . 'train_experience';
    $annexTable = $prefix . 'annex';
    $rows = static function (string $sql, array $args = [], int $cap = 200)
        use ($db): array {
        if (!preg_match('/^SELECT\b/i', $sql)) throw new RuntimeException('not SELECT');
        $stmt = $db->prepare($sql);
        $stmt->execute($args);
        $result = [];
        while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
            if (count($result) >= $cap) throw new RuntimeException('row cap');
            $clean = [];
            foreach (array_change_key_case($row, CASE_LOWER) as $key => $value) {
                $clean[$key] = is_resource($value) ? stream_get_contents($value) : $value;
            }
            $result[] = $clean;
        }
        return $result;
    };
    $count = static function (string $sql, array $args = []) use ($rows): int {
        $r = $rows($sql, $args, 1);
        return (int)(reset($r[0]) ?: 0);
    };
    $tablesWithStaffUuid = [];
    if ($dialect === 'mysql') {
        $tablesWithStaffUuid = $db->query("SELECT TABLE_NAME FROM information_schema.COLUMNS"
            . " WHERE TABLE_SCHEMA=DATABASE() AND COLUMN_NAME='staff_uuid'")
            ->fetchAll(PDO::FETCH_COLUMN);
    } else {
        $tablesWithStaffUuid = $db->query("SELECT TABLE_NAME FROM USER_TAB_COLUMNS WHERE COLUMN_NAME='STAFF_UUID'")
            ->fetchAll(PDO::FETCH_COLUMN);
    }
    if (!$tablesWithStaffUuid || count($tablesWithStaffUuid) > 200) {
        throw new RuntimeException('staff reference inventory');
    }
    foreach ($tablesWithStaffUuid as $table) {
        if (!preg_match('/^' . $prefix . '[A-Z0-9_]+$/i', (string)$table)) {
            throw new RuntimeException('staff reference table name');
        }
    }
    $references = static function (string $staffUuid) use ($count, $tablesWithStaffUuid): array {
        $result = [];
        foreach ($tablesWithStaffUuid as $table) {
            $n = $count('SELECT COUNT(*) FROM ' . $table . ' WHERE STAFF_UUID=?', [$staffUuid]);
            if ($n !== 0) $result[$table] = $n;
        }
        return $result;
    };
    if ($mode === '--audit') {
        if (!preg_match('/^CDXTREXP[A-F0-9]{12}$/', $marker)) exit(78);
        $staffUuid = $marker . 'S';
        $actorUuid = $marker . 'A';
        $out = [
            'actor' => $count('SELECT COUNT(*) FROM ' . $userTable
                . ' WHERE UUID=? OR ACCOUNT=?', [$actorUuid, $actorUuid]),
            'staff' => $count('SELECT COUNT(*) FROM ' . $staffTable
                . ' WHERE UUID=? OR PERSONNEL_UMBER=?', [$staffUuid, $marker]),
            'experience' => $count('SELECT COUNT(*) FROM ' . $trainTable
                . ' WHERE STAFF_UUID=? OR TRAIN_COURSE_NAME=?', [$staffUuid, $marker]),
            'annex' => $count('SELECT COUNT(*) FROM ' . $annexTable
                . ' WHERE STAFF_UUID=? OR MASTER_UUID=?', [$staffUuid, $marker]),
            'other_refs' => $references($staffUuid),
        ];
        $zero = array_sum(array_slice($out, 0, 4)) === 0 && !$out['other_refs'];
        echo json_encode(['lane' => $lane, 'audit' => $out, 'zero' => $zero]), PHP_EOL;
        exit($zero ? 0 : 3);
    }
    $stage = 'schema-gate';
    $gate = [];
    foreach ([$staffTable, $trainTable, $annexTable] as $table) {
        if ($dialect === 'mysql') {
            $t = $db->prepare('SELECT COUNT(*) FROM information_schema.TRIGGERS'
                . ' WHERE TRIGGER_SCHEMA=DATABASE() AND EVENT_OBJECT_TABLE=?');
            $t->execute([$table]);
            $triggers = (int)$t->fetchColumn();
            $f = $db->prepare('SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE'
                . ' WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND REFERENCED_TABLE_NAME IS NOT NULL');
            $f->execute([$table]);
            $outbound = (int)$f->fetchColumn();
            $f = $db->prepare('SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE'
                . ' WHERE REFERENCED_TABLE_SCHEMA=DATABASE() AND REFERENCED_TABLE_NAME=?');
            $f->execute([$table]);
            $inbound = (int)$f->fetchColumn();
        } else {
            $t = $db->prepare('SELECT COUNT(*) FROM USER_TRIGGERS WHERE TABLE_NAME=?');
            $t->execute([$table]);
            $triggers = (int)$t->fetchColumn();
            $f = $db->prepare("SELECT COUNT(*) FROM USER_CONSTRAINTS WHERE TABLE_NAME=? AND CONSTRAINT_TYPE='R'");
            $f->execute([$table]);
            $outbound = (int)$f->fetchColumn();
            $f = $db->prepare("SELECT COUNT(*) FROM USER_CONSTRAINTS WHERE CONSTRAINT_TYPE='R'"
                . ' AND R_CONSTRAINT_NAME IN (SELECT CONSTRAINT_NAME FROM USER_CONSTRAINTS WHERE TABLE_NAME=?)');
            $f->execute([$table]);
            $inbound = (int)$f->fetchColumn();
        }
        $gate[$table] = [$triggers, $outbound, $inbound];
        if ($triggers || $outbound || $inbound) throw new RuntimeException('target FK/trigger present');
    }
    $prior = [
        'staff_count' => $count('SELECT COUNT(*) FROM ' . $staffTable),
        'experience_count' => $count('SELECT COUNT(*) FROM ' . $trainTable),
        'annex_count' => $count('SELECT COUNT(*) FROM ' . $annexTable),
    ];
    if ($mode === '--preflight') {
        echo json_encode(['lane' => $lane, 'status' => 'preflight-pass',
            'staff_ref_tables' => count($tablesWithStaffUuid),
            'target_gate_all_zero' => true, 'prior_counts' => $prior]), PHP_EOL;
        exit(0);
    }
    $marker = 'CDXTREXP' . strtoupper(bin2hex(random_bytes(6)));
    $staffUuid = $marker . 'S';
    $actorUuid = $marker . 'A';
    if ($count('SELECT COUNT(*) FROM ' . $staffTable
        . ' WHERE UUID=? OR PERSONNEL_UMBER=?', [$staffUuid, $marker]) !== 0
        || $count('SELECT COUNT(*) FROM ' . $userTable
            . ' WHERE UUID=? OR ACCOUNT=?', [$actorUuid, $actorUuid]) !== 0
        || $count('SELECT COUNT(*) FROM ' . $trainTable
            . ' WHERE STAFF_UUID=? OR TRAIN_COURSE_NAME=?', [$staffUuid, $marker]) !== 0
        || $references($staffUuid)) throw new RuntimeException('marker collision');

    $privateDir = sys_get_temp_dir() . '/cdx-train-' . $marker;
    if (file_exists($privateDir) || !mkdir($privateDir, 0700)) {
        throw new RuntimeException('recovery directory');
    }
    $journalPath = $privateDir . '/recovery.json';
    $journal = ['lane' => $lane, 'marker' => $marker, 'state' => 'prepared',
        'prior_counts' => $prior];
    $saveJournal = static function () use (&$journal, $journalPath): void {
        if (file_put_contents($journalPath, json_encode($journal)) === false) {
            throw new RuntimeException('recovery journal');
        }
        chmod($journalPath, 0600);
    };
    $saveJournal();
    $shape = static function ($value): array {
        if (!is_array($value)) return ['_type' => gettype($value)];
        $out = [];
        foreach ($value as $key => $item) $out[(string)$key] = gettype($item);
        ksort($out);
        return $out;
    };
    $httpUncertain = false;
    $post = static function (string $path, array $form, string $token)
        use ($base, $runtimeReady, &$httpUncertain): array {
        if (!in_array($path, ['/p/TrainExperience/add', '/p/TrainExperience/lists',
            '/p/TrainExperience/info', '/p/TrainExperience/update',
            '/p/TrainExperience/delete', '/p/TrainExperience/annexLists'], true)
            || !$runtimeReady()) throw new RuntimeException('HTTP boundary');
        $context = stream_context_create(['http' => ['method' => 'POST',
            'header' => "Content-Type: application/x-www-form-urlencoded\r\ntoken: " . $token . "\r\n",
            'content' => http_build_query($form, '', '&', PHP_QUERY_RFC3986),
            'timeout' => 20, 'ignore_errors' => true]]);
        $httpUncertain = true;
        $stream = @fopen($base . $path, 'rb', false, $context);
        $body = is_resource($stream) ? stream_get_contents($stream, 1048577) : false;
        $meta = is_resource($stream) ? stream_get_meta_data($stream) : [];
        if (is_resource($stream)) fclose($stream);
        if (!is_string($body) || strlen($body) > 1048576 || !empty($meta['timed_out'])
            || strpos((string)($http_response_header[0] ?? ''), ' 200 ') === false) {
            throw new RuntimeException('HTTP outcome incomplete');
        }
        $json = json_decode($body, true);
        if (!is_array($json) || !array_key_exists('code', $json)) {
            throw new RuntimeException('HTTP JSON unknown');
        }
        $httpUncertain = false;
        return $json;
    };
    $actorId = 0;
    $staffId = 0;
    $experienceUuid = '';
    $failure = '';
    $result = ['lane' => $lane, 'marker' => $marker, 'routes' => [],
        'prior_counts' => $prior, 'source_hidden' => false,
        'cleanup_zero' => false, 'journal_removed' => false];
    try {
        $stage = 'fixture-transaction';
        $db->beginTransaction();
        $insert = $db->prepare('INSERT INTO ' . $userTable
            . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
            . ' VALUES (?,?,?,?,?,?,?,?,?)');
        $insert->execute([$actorUuid, $actorUuid, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
            $marker, 0, date('Y-m-d H:i:s'), $marker, 0]);
        if ($insert->rowCount() !== 1) throw new RuntimeException('actor insert');
        $columns = ['UUID','STATION_UUID','NAME','BIRTHDAY','SPORT_UUID',
            'SPORT_EVENT_UUID','CREATE_TIME','CREATE_BY','PERSONNEL_UMBER',
            'PERMANENT_ADDRESS','COACH_NAME','SPORT_YEAR','IS_SHOW','STATUS','JOB_STATUS'];
        $values = [$staffUuid,$marker . 'T',$marker,'2000-01-01',$marker . 'P',
            '[]',date('Y-m-d H:i:s'),$marker,$marker,$marker,$marker,'0',0,0,0];
        if ($dialect === 'mysql') { $columns[] = 'IN_TIME'; $values[] = '2020-01-01'; }
        $insert = $db->prepare('INSERT INTO ' . $staffTable
            . ' (' . implode(',', $columns) . ') VALUES ('
            . implode(',', array_fill(0, count($columns), '?')) . ')');
        $insert->execute($values);
        if ($insert->rowCount() !== 1) throw new RuntimeException('hidden staff insert');
        $actorRows = $rows('SELECT ID FROM ' . $userTable
            . ' WHERE UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?',
            [$actorUuid, $actorUuid, $marker], 2);
        $staffRows = $rows('SELECT ID,IS_SHOW,STATUS,JOB_STATUS FROM ' . $staffTable
            . ' WHERE UUID=? AND PERSONNEL_UMBER=? AND CREATE_BY=?',
            [$staffUuid, $marker, $marker], 2);
        if (count($actorRows) !== 1 || count($staffRows) !== 1
            || (int)$staffRows[0]['is_show'] !== 0
            || (int)$staffRows[0]['status'] !== 0
            || (int)$staffRows[0]['job_status'] !== 0) {
            throw new RuntimeException('fixture identity/hidden');
        }
        $actorId = (int)$actorRows[0]['id'];
        $staffId = (int)$staffRows[0]['id'];
        $db->commit();
        $journal['actor_id'] = $actorId;
        $journal['staff_id'] = $staffId;
        $journal['state'] = 'fixture_committed';
        $saveJournal();
        $result['source_hidden'] = true;
        require_once $root . '/application/base/Security.php';
        $token = \app\base\Security::encrypt(implode("\t",
            [(string)$actorId, $actorUuid, $marker, '0', '', '', $actorUuid]));
        if (!is_string($token) || $token === '') throw new RuntimeException('actor token');
        $form = ['staff_uuid' => $staffUuid, 'start_date' => '2001-01-02',
            'end_date' => '2001-01-03', 'train_course_name' => $marker,
            'organizer' => $marker, 'contractor' => $marker,
            'content' => $marker . '-before', 'file' => '[]'];

        $stage = 'reject-missing-name';
        $badForm = $form;
        unset($badForm['train_course_name']);
        $bad = $post('/p/TrainExperience/add', $badForm, $token);
        if (($bad['code'] ?? 0) === 0
            || $count('SELECT COUNT(*) FROM ' . $trainTable . ' WHERE STAFF_UUID=?', [$staffUuid]) !== 0) {
            throw new RuntimeException('invalid add accepted');
        }
        $result['routes']['add_missing_required'] = ['rejected' => true,
            'envelope' => $shape($bad)];

        $stage = 'add';
        $saved = $post('/p/TrainExperience/add', $form, $token);
        if (($saved['code'] ?? null) !== 0) throw new RuntimeException('valid add rejected');
        $experience = $rows('SELECT * FROM ' . $trainTable
            . ' WHERE STAFF_UUID=? AND TRAIN_COURSE_NAME=? AND CREATE_BY=?',
            [$staffUuid, $marker, $actorUuid], 2);
        if (count($experience) !== 1
            || (string)($experience[0]['content'] ?? '') !== $marker . '-before') {
            throw new RuntimeException('add source readback');
        }
        $experienceUuid = (string)$experience[0]['uuid'];
        $journal['experience_uuid'] = $experienceUuid;
        $journal['state'] = 'experience_committed';
        $saveJournal();
        $result['routes']['add'] = ['source_rows' => 1, 'envelope' => $shape($saved)];

        $stage = 'lists';
        $list = $post('/p/TrainExperience/lists',
            ['staff_uuid' => $staffUuid, 'page' => 1], $token);
        if (($list['code'] ?? null) !== 0 || (int)($list['data']['total'] ?? -1) !== 1
            || count($list['data']['rows'] ?? []) !== 1
            || (string)($list['data']['rows'][0]['uuid'] ?? '') !== $experienceUuid) {
            throw new RuntimeException('lists source mismatch');
        }
        $result['routes']['lists'] = ['source_rows' => 1,
            'envelope' => $shape($list), 'data' => $shape($list['data']),
            'row' => $shape($list['data']['rows'][0])];

        $stage = 'info';
        $info = $post('/p/TrainExperience/info', ['uuid' => $experienceUuid], $token);
        if (($info['code'] ?? null) !== 0 || !is_array($info['data'] ?? null)
            || (string)($info['data']['uuid'] ?? '') !== $experienceUuid
            || (string)($info['data']['content'] ?? '') !== $marker . '-before'
            || !is_array($info['data']['annex'] ?? null)
            || $info['data']['annex'] !== []) throw new RuntimeException('info source mismatch');
        $result['routes']['info'] = ['source_match' => true,
            'envelope' => $shape($info), 'row' => $shape($info['data'])];

        $stage = 'annex-lists';
        $annex = $post('/p/TrainExperience/annexLists',
            ['uuid' => $experienceUuid], $token);
        if (($annex['code'] ?? null) !== 0 || (int)($annex['data']['total'] ?? -1) !== 0
            || ($annex['data']['rows'] ?? null) !== []) {
            throw new RuntimeException('annex empty source mismatch');
        }
        $result['routes']['annexLists'] = ['source_rows' => 0,
            'envelope' => $shape($annex), 'data' => $shape($annex['data'])];

        $stage = 'update';
        $update = $form;
        $update['uuid'] = $experienceUuid;
        $update['content'] = $marker . '-after';
        unset($update['file']); // DB-only route; frontend's file=[] variant tested next.
        $updated = $post('/p/TrainExperience/update', $update, $token);
        if (($updated['code'] ?? null) !== 0) throw new RuntimeException('valid update rejected');
        $after = $rows('SELECT CONTENT FROM ' . $trainTable . ' WHERE UUID=? AND STAFF_UUID=?',
            [$experienceUuid, $staffUuid], 2);
        if (count($after) !== 1 || (string)$after[0]['content'] !== $marker . '-after') {
            throw new RuntimeException('update source readback');
        }
        $result['routes']['update'] = ['source_match' => true,
            'envelope' => $shape($updated)];
        $stage = 'update-pc-file-empty';
        $update['file'] = '[]';
        $update['content'] = $marker . '-pc';
        $pcUpdate = $post('/p/TrainExperience/update', $update, $token);
        $pcAfter = $rows('SELECT CONTENT FROM ' . $trainTable
            . ' WHERE UUID=? AND STAFF_UUID=?', [$experienceUuid, $staffUuid], 2);
        if (count($pcAfter) !== 1) throw new RuntimeException('PC update source lost');
        $pcCode = (int)($pcUpdate['code'] ?? -1);
        $expectedContent = $pcCode === 0 ? $marker . '-pc' : $marker . '-after';
        if ((string)$pcAfter[0]['content'] !== $expectedContent) {
            throw new RuntimeException('PC update code/source mismatch');
        }
        $result['routes']['update_pc_file_empty'] = [
            'code' => $pcCode, 'source_matches_code' => true,
            'envelope' => $shape($pcUpdate)];
        if ($pcCode !== 0) throw new RuntimeException('PC empty-file update still rejected');

        $stage = 'delete';
        $deleted = $post('/p/TrainExperience/delete', ['uuid' => $experienceUuid], $token);
        if (($deleted['code'] ?? null) !== 0
            || $count('SELECT COUNT(*) FROM ' . $trainTable . ' WHERE UUID=?', [$experienceUuid]) !== 0
            || $count('SELECT COUNT(*) FROM ' . $annexTable . ' WHERE MASTER_UUID=?', [$experienceUuid]) !== 0) {
            throw new RuntimeException('delete source readback');
        }
        $result['routes']['delete'] = ['source_zero' => true,
            'envelope' => $shape($deleted)];
    } catch (Throwable $error) {
        $failure = $stage . ':' . get_class($error) . ':' . (string)$error->getCode()
            . ':' . substr(hash('sha256', $error->getMessage()), 0, 12);
        if ($db->inTransaction()) $db->rollBack();
    }
    $result['failure'] = $failure;
    $result['http_uncertain'] = $httpUncertain;
    $journal['state'] = $httpUncertain ? 'unknown_http' : 'business_completed_or_failed';
    $journal['failure'] = $failure;
    $saveJournal();
    if ($httpUncertain) {
        $result['status'] = 'preserved-for-recovery';
        $result['journal'] = $journalPath;
        echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
        exit(3);
    }
    $unexpected = $references($staffUuid);
    unset($unexpected[$trainTable]);
    if ($unexpected) {
        $result['status'] = 'unexpected-staff-references';
        $result['reference_tables'] = array_keys($unexpected);
        $result['journal'] = $journalPath;
        echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
        exit(4);
    }
    $stage = 'exact-cleanup';
    $db->beginTransaction();
    try {
        if ($experienceUuid !== '') {
            $remaining = $rows('SELECT UUID FROM ' . $trainTable
                . ' WHERE UUID=? AND STAFF_UUID=? AND CREATE_BY=?',
                [$experienceUuid, $staffUuid, $actorUuid], 2);
            if (count($remaining) > 1) throw new RuntimeException('experience cleanup ambiguous');
            if ($remaining) {
                $delete = $db->prepare('DELETE FROM ' . $trainTable
                    . ' WHERE UUID=? AND STAFF_UUID=? AND CREATE_BY=?');
                $delete->execute([$experienceUuid, $staffUuid, $actorUuid]);
                if ($delete->rowCount() !== 1) throw new RuntimeException('experience cleanup affected');
            }
        }
        if ($count('SELECT COUNT(*) FROM ' . $annexTable
            . ' WHERE STAFF_UUID=? OR MASTER_UUID=?', [$staffUuid, $experienceUuid ?: $marker]) !== 0
            || $references($staffUuid)) throw new RuntimeException('references before staff cleanup');
        $staffOwned = $rows('SELECT ID FROM ' . $staffTable
            . ' WHERE UUID=? AND PERSONNEL_UMBER=? AND CREATE_BY=? AND IS_SHOW=0 AND STATUS=0',
            [$staffUuid, $marker, $marker], 2);
        if (count($staffOwned) > 1 || ($staffOwned && (int)$staffOwned[0]['id'] !== $staffId)) {
            throw new RuntimeException('staff cleanup identity');
        }
        if ($staffOwned) {
            $delete = $db->prepare('DELETE FROM ' . $staffTable
                . ' WHERE ID=? AND UUID=? AND PERSONNEL_UMBER=? AND CREATE_BY=? AND IS_SHOW=0 AND STATUS=0');
            $delete->execute([$staffId, $staffUuid, $marker, $marker]);
            if ($delete->rowCount() !== 1) throw new RuntimeException('staff cleanup affected');
        }
        $actorOwned = $rows('SELECT ID FROM ' . $userTable
            . ' WHERE UUID=? AND ACCOUNT=? AND CREATE_BY=? AND TYPE=0',
            [$actorUuid, $actorUuid, $marker], 2);
        if (count($actorOwned) > 1 || ($actorOwned && (int)$actorOwned[0]['id'] !== $actorId)) {
            throw new RuntimeException('actor cleanup identity');
        }
        if ($actorOwned) {
            $delete = $db->prepare('DELETE FROM ' . $userTable
                . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND CREATE_BY=? AND TYPE=0');
            $delete->execute([$actorId, $actorUuid, $actorUuid, $marker]);
            if ($delete->rowCount() !== 1) throw new RuntimeException('actor cleanup affected');
        }
        $db->commit();
        $result['cleanup_zero'] = $count('SELECT COUNT(*) FROM ' . $staffTable
            . ' WHERE UUID=?', [$staffUuid]) === 0
            && $count('SELECT COUNT(*) FROM ' . $userTable . ' WHERE UUID=?', [$actorUuid]) === 0
            && $count('SELECT COUNT(*) FROM ' . $trainTable . ' WHERE STAFF_UUID=?', [$staffUuid]) === 0
            && !$references($staffUuid);
    } catch (Throwable $cleanupError) {
        if ($db->inTransaction()) $db->rollBack();
        $result['status'] = 'cleanup-blocked';
        $result['cleanup_error'] = get_class($cleanupError) . ':'
            . substr(hash('sha256', $cleanupError->getMessage()), 0, 12);
        $result['journal'] = $journalPath;
        echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
        exit(5);
    }
    if (!$result['cleanup_zero']) {
        $result['status'] = 'cleanup-not-zero';
        $result['journal'] = $journalPath;
        echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
        exit(6);
    }
    $journal['state'] = 'cleaned';
    $saveJournal();
    unlink($journalPath);
    rmdir($privateDir);
    $result['journal_removed'] = !file_exists($privateDir);
    $result['status'] = $failure === '' ? 'pass' : 'business-failed-cleaned';
    echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
    exit($failure === '' && $result['journal_removed'] ? 0 : 1);
} catch (Throwable $error) {
    echo json_encode(['lane' => $lane, 'status' => 'blocked', 'stage' => $stage,
        'class' => get_class($error), 'code' => (string)$error->getCode(),
        'message_sha12' => substr(hash('sha256', $error->getMessage()), 0, 12),
        'marker' => preg_match('/^CDXTREXP[A-F0-9]{12}$/', $marker) ? $marker : 'none']), PHP_EOL;
    exit(2);
}
