<?php
declare(strict_types=1);

// Deliberately default-deny: only isolated, self-owned cash-refund fixtures.
$lane = (string)($argv[1] ?? '');
$mode = (string)($argv[2] ?? '');
$marker = (string)getenv('CDX_CRW3_MARKER');
if (!in_array($lane, ['B', 'C'], true)
    || !in_array($mode, ['--preflight', '--execute', '--cleanup', '--audit'], true)
    || !preg_match('/^CDXCRW3[A-F0-9]{12}$/', $marker)
    || (in_array($mode, ['--execute', '--cleanup'], true)
        && ((string)getenv('CDX_CRW3_APPROVE') !== 'ROOT_APPROVED_CASHREFUND_WRITE3_V1'
            || !hash_equals(hash_file('sha256', __FILE__), (string)getenv('CDX_CRW3_SHA'))))) {
    fwrite(STDERR, "CRW3 default deny\n");
    exit(78);
}
$root = $lane === 'B' ? '/tmp/zhct-http-store-mysql.dQTTBP'
    : '/tmp/zhct-dm-poc-20260928.fcpiiN/store';
$base = $lane === 'B' ? 'http://127.0.0.1:18081' : 'http://127.0.0.1:18080';
$pid = $lane === 'B' ? 10001 : 112175;
$journalPath = '/tmp/cdx-cashrefund-write3-' . $lane . '-' . $marker . '.journal.json';
$sourceHashes = [
    '/application/route.php' => '081b0186b7f40f0bdd237cecb3e2d678fc9344c656932c0d3b3052404e4f1510',
    '/application/p/controller/CashRefund.php' => 'cd24ac67287603d536510ef7405b7fa3998c6bc99bc5a29399601cad0ea01172',
    '/application/p/logic/CashRefund.php' => 'b0bd6f2d9a956116538ae4326cd35ab8d1b68a4576a2cb265372bd23cefd7c3d',
    '/application/common/service/SecurityAuditService.php' => 'a57fc430f52f541d1619ae87fe29651dca3ab408013fe6f38a80f5aa6db62cf1',
    '/application/common/service/Order.php' => 'e38805a0f3e6a540e041aa0cdbac29a398ca35d5f63f61d8e128854e8bd599a1',
    '/application/base/AdminController.php' => '7565b36fa4dda2c21cc4fa3b37753dbffb7c3a464bb8c11ea0f17ca00bececa5',
    '/application/common.php' => '1ff0e49451950d671498ad5e1f7380f60448d2f7a39041d4b53370abd6e6d78f',
];
$stage = 'boundary';
try {
    $cmd = (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);
    if (PHP_VERSION_ID < 70300 || PHP_VERSION_ID >= 70400 || is_link($root)
        || strpos($cmd, $root . '/public') === false
        || strpos($cmd, (string)parse_url($base, PHP_URL_PORT)) === false
        || $children !== '' || !is_array($boundary)
        || ($boundary['external_stubs'] ?? null) !== true
        || ($boundary['redis_isolated'] ?? null) !== true
        || ($boundary['real_queue'] ?? true) !== false) {
        throw new RuntimeException('isolated worker boundary');
    }
    foreach ($sourceHashes as $file => $sha) {
        if (!is_file($root . $file) || is_link($root . $file)
            || !hash_equals($sha, hash_file('sha256', $root . $file))) {
            throw new RuntimeException('business source drift');
        }
    }
    $stage = 'database';
    $cfg = require $root . '/application/database.php';
    if (!is_array($cfg) || ($cfg['prefix'] ?? '') !== 'ydy_'
        || ($cfg['ai_zhct']['prefix'] ?? '') !== 'yoshop_') {
        throw new RuntimeException('database config');
    }
    $ac = $cfg['ai_zhct'];
    if ($lane === 'B') {
        $local = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
        foreach (['hostname', 'database', 'username', 'password'] as $key) {
            if (($cfg[$key] ?? null) !== ($local[$key] ?? null)
                || ($ac[$key] ?? null) !== ($local['ai_zhct'][$key] ?? null)) {
                throw new RuntimeException('not code-local MySQL');
            }
        }
        $storeDsn = 'mysql:host=' . $cfg['hostname'] . ';port=' . ($cfg['hostport'] ?? 3306)
            . ';dbname=' . $cfg['database'] . ';charset=utf8mb4';
        $aiDsn = 'mysql:host=' . $ac['hostname'] . ';port=' . ($ac['hostport'] ?? 3306)
            . ';dbname=' . $ac['database'] . ';charset=utf8mb4';
    } else {
        foreach (['hostname' => 'POC_HOST', 'hostport' => 'POC_PORT',
            'database' => 'POC_SCHEMA', 'username' => 'POC_USER',
            'password' => 'POC_PASSWORD'] as $key => $env) {
            if ((string)($cfg[$key] ?? '') !== (string)getenv($env)) {
                throw new RuntimeException('Store DM config');
            }
        }
        foreach (['hostname' => 'POC_AI_HOST', 'hostport' => 'POC_AI_PORT',
            'database' => 'POC_AI_SCHEMA', 'username' => 'POC_AI_USER',
            'password' => 'POC_AI_PASSWORD'] as $key => $env) {
            if ((string)($ac[$key] ?? '') !== (string)getenv($env)) {
                throw new RuntimeException('AI DM config');
            }
        }
        if ((string)getenv('POC_HOST') !== '127.0.0.1'
            || (string)getenv('POC_PORT') !== '5237'
            || strtoupper((string)getenv('POC_SCHEMA')) !== 'ZHCT_APP'
            || (string)getenv('POC_AI_HOST') !== '127.0.0.1'
            || (string)getenv('POC_AI_PORT') !== '5238'
            || strtoupper((string)getenv('POC_AI_SCHEMA')) !== 'AIZHCT_APP') {
            throw new RuntimeException('not approved DM');
        }
        $storeDsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
        $aiDsn = 'dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP';
    }
    $options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5];
    $store = new PDO($storeDsn, (string)$cfg['username'], (string)$cfg['password'], $options);
    $ai = new PDO($aiDsn, (string)$ac['username'], (string)$ac['password'], $options);
    $storeIdentity = $lane === 'B'
        ? (string)$store->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")->fetchColumn()
        : 'ZHCT_APP|5237|' . (string)$store->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn();
    $aiIdentity = $lane === 'B'
        ? (string)$ai->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")->fetchColumn()
        : 'AIZHCT_APP|5238|' . (string)$ai->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn();
    if (hash('sha256', $storeIdentity) !== ($lane === 'B'
            ? '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401'
            : 'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194')
        || ($lane === 'B' && substr(hash('sha256', $aiIdentity), 0, 12) !== '210f925e093f')
        || ($lane === 'C' && $aiIdentity !== 'AIZHCT_APP|5238|AIZHCT_APP')) {
        throw new RuntimeException('DB identity');
    }
    $sp = $lane === 'B' ? 'ydy_' : 'YDY_';
    $ap = $lane === 'B' ? 'yoshop_' : 'YOSHOP_';
    $read = static function (PDO $db, string $sql, array $args = [], int $cap = 100) {
        if (!preg_match('/^SELECT\b/i', $sql)) throw new RuntimeException('not SELECT');
        $stmt = $db->prepare($sql);
        $stmt->execute($args);
        $rows = [];
        while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
            if (count($rows) >= $cap) throw new RuntimeException('read cap');
            $row = array_change_key_case($row, CASE_LOWER);
            foreach ($row as $key => $value) {
                if (is_resource($value)) $row[$key] = stream_get_contents($value);
            }
            ksort($row);
            $rows[] = $row;
        }
        return $rows;
    };
    $one = static function (PDO $db, string $sql, array $args = []) use ($read): ?array {
        $rows = $read($db, $sql, $args, 2);
        if (count($rows) > 1) throw new RuntimeException('non-unique own row');
        return $rows[0] ?? null;
    };
    $count = static function (PDO $db, string $sql, array $args = []) use ($one): int {
        $row = $one($db, $sql, $args);
        return $row === null ? 0 : (int)reset($row);
    };
    $write = static function (PDO $db, string $sql, array $args): int {
        if (!preg_match('/^(INSERT|UPDATE|DELETE)\b/i', $sql)) {
            throw new RuntimeException('DML only');
        }
        $stmt = $db->prepare($sql);
        $stmt->execute($args);
        return $stmt->rowCount();
    };
    $own = static function () use ($read, $store, $ai, $sp, $ap, $marker): array {
        $mobile = '000' . substr($marker, -8);
        $actor = $read($store, 'SELECT ID,UUID,ACCOUNT,NAME,TYPE,CREATE_BY FROM '
            . $sp . 'user WHERE UUID=? OR ACCOUNT=?', [$marker, $marker], 3);
        $staff = $read($store, 'SELECT ID,UUID,NAME,AI_USER_ID,CREATE_BY FROM '
            . $sp . 'staff WHERE UUID=?', [$marker], 3);
        $links = $read($store, 'SELECT ID,STAFF_UUID,DEPARTMENT_UUID,CREATE_BY FROM '
            . $sp . 'staff_department WHERE STAFF_UUID=?', [$marker], 3);
        $users = $read($ai, 'SELECT USER_ID,NICK_NAME,MOBILE,BALANCE,STORE_ID FROM '
            . $ap . 'user WHERE NICK_NAME=? OR MOBILE=?', [$marker, $mobile], 3);
        $orders = $read($ai, 'SELECT ORDER_ID,ORDER_NO,USER_ID,REFUNDABLE_MONEY,REFUND_MONEY,PAY_STATUS FROM '
            . $ap . 'recharge_order WHERE ORDER_NO=?', ['R' . $marker], 3);
        $refunds = $read($ai, 'SELECT ID,REFUND_NO,USER_ID,STAFF_UUID,REASON,STATUS,'
            . ' REFUND_AMOUNT,BALANCE_LOG_ID,CARD_ID FROM ' . $ap
            . 'cash_refund_order WHERE STAFF_UUID=?', [$marker], 4);
        $refundLogs = $read($ai, 'SELECT ID,REFUND_ID,REFUND_NO,ACTION,OPERATOR FROM '
            . $ap . 'cash_refund_order_log WHERE OPERATOR=?', [$marker], 8);
        $userId = count($users) === 1 ? (int)$users[0]['user_id'] : 0;
        $balanceLogs = $userId > 0 ? $read($ai, 'SELECT LOG_ID,USER_ID,ORDER_ID,SCENE,MONEY,BALANCE_AFTER FROM '
            . $ap . 'user_balance_log WHERE USER_ID=?', [$userId], 5) : [];
        $audits = $read($store, 'SELECT ID,USER_UUID,EVENT_CODE,TARGET_ID,MODULE FROM '
            . $sp . 'security_audit_log WHERE USER_UUID=?', [$marker], 8);
        return compact('actor', 'staff', 'links', 'users', 'orders', 'refunds',
            'refundLogs', 'balanceLogs', 'audits');
    };
    $journal = ['lane' => $lane, 'marker' => $marker,
        'store_identity_sha256' => hash('sha256', $storeIdentity),
        'ai_identity_sha256' => hash('sha256', $aiIdentity),
        'phase' => 'before-fixture', 'events' => []];
    $saveJournal = static function (array $record) use ($journalPath): void {
        if (is_link($journalPath) || !is_file($journalPath)) {
            throw new RuntimeException('journal missing/symlink');
        }
        $json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        $temporary = $journalPath . '.tmp.' . bin2hex(random_bytes(4));
        if (!is_string($json) || file_exists($temporary) || is_link($temporary)) {
            throw new RuntimeException('journal temp collision');
        }
        $handle = fopen($temporary, 'x');
        if (!is_resource($handle)) throw new RuntimeException('journal open');
        chmod($temporary, 0600);
        $written = fwrite($handle, $json);
        fflush($handle);
        fclose($handle);
        if ($written !== strlen($json) || !rename($temporary, $journalPath)) {
            throw new RuntimeException('journal write/rename');
        }
    };
    $loadJournal = static function () use ($journalPath, $lane, $marker,
        $storeIdentity, $aiIdentity): array {
        if (is_link($journalPath) || !is_file($journalPath)) {
            throw new RuntimeException('journal missing/symlink');
        }
        $v = json_decode((string)file_get_contents($journalPath), true);
        if (!is_array($v) || ($v['lane'] ?? null) !== $lane
            || ($v['marker'] ?? null) !== $marker
            || ($v['store_identity_sha256'] ?? null) !== hash('sha256', $storeIdentity)
            || ($v['ai_identity_sha256'] ?? null) !== hash('sha256', $aiIdentity)) {
            throw new RuntimeException('journal identity');
        }
        return $v;
    };
    $allZero = static function (array $rows): bool {
        foreach ($rows as $items) if (count($items) !== 0) return false;
        return true;
    };
    $assertOwned = static function (array $s) use ($marker, &$journal): void {
        foreach (['actor' => 1, 'staff' => 1, 'links' => 1, 'users' => 1,
            'orders' => 1, 'refunds' => 2, 'refundLogs' => 4,
            'balanceLogs' => 1, 'audits' => 4] as $key => $max) {
            if (count($s[$key]) > $max) throw new RuntimeException('own row cap ' . $key);
        }
        foreach ($s['actor'] as $r) {
            if ($r['uuid'] !== $marker || $r['account'] !== $marker
                || $r['name'] !== $marker || (int)$r['type'] !== 0
                || $r['create_by'] !== 'CODEX') throw new RuntimeException('actor owner');
        }
        $userId = count($s['users']) === 1 ? (int)$s['users'][0]['user_id'] : 0;
        foreach ($s['staff'] as $r) {
            if ($r['uuid'] !== $marker || $r['name'] !== $marker
                || $r['create_by'] !== $marker
                || ($userId > 0 && (int)$r['ai_user_id'] !== $userId)) {
                throw new RuntimeException('staff owner');
            }
        }
        foreach ($s['links'] as $r) {
            if ($r['staff_uuid'] !== $marker || $r['create_by'] !== $marker
                || (isset($journal['department_uuid'])
                    && $r['department_uuid'] !== $journal['department_uuid'])) {
                throw new RuntimeException('staff-department owner');
            }
        }
        foreach ($s['users'] as $r) {
            if ($r['nick_name'] !== $marker
                || $r['mobile'] !== '000' . substr($marker, -8)
                || (int)$r['store_id'] !== 10001
                || (float)$r['balance'] < 0 || (float)$r['balance'] > 1) {
                throw new RuntimeException('AI user owner/balance');
            }
        }
        foreach ($s['orders'] as $r) {
            if ($r['order_no'] !== 'R' . $marker
                || (int)$r['user_id'] !== $userId
                || (int)$r['pay_status'] !== 20
                || (float)$r['refundable_money'] < 0
                || (float)$r['refund_money'] < 0
                || abs((float)$r['refundable_money'] + (float)$r['refund_money'] - 1.0) > 0.001) {
                throw new RuntimeException('recharge owner/money');
            }
        }
        $refundIds = [];
        foreach ($s['refunds'] as $r) {
            if ($r['staff_uuid'] !== $marker || (int)$r['user_id'] !== $userId
                || !in_array($r['reason'], [$marker . '-agree', $marker . '-reject'], true)
                || !in_array((int)$r['status'], [10, 20, 30], true)
                || !in_array(number_format((float)$r['refund_amount'], 2, '.', ''), ['0.40', '0.20'], true)
                || (string)$r['card_id'] !== '') {
                throw new RuntimeException('refund owner/card');
            }
            $refundIds[(int)$r['id']] = true;
        }
        foreach ($s['refundLogs'] as $r) {
            if ($r['operator'] !== $marker || !isset($refundIds[(int)$r['refund_id']])
                || !in_array($r['action'], ['create', 'agree', 'reject'], true)) {
                throw new RuntimeException('refund log owner');
            }
        }
        foreach ($s['balanceLogs'] as $r) {
            if ((int)$r['user_id'] !== $userId
                || !isset($refundIds[(int)$r['order_id']])
                || (int)$r['scene'] !== 60
                || number_format((float)$r['money'], 2, '.', '') !== '-0.40') {
                throw new RuntimeException('balance log owner');
            }
        }
        foreach ($s['audits'] as $r) {
            if ($r['user_uuid'] !== $marker || $r['module'] !== 'cash_refund'
                || !isset($refundIds[(int)$r['target_id']])
                || !in_array($r['event_code'],
                    ['cash_refund_create', 'cash_refund_agree', 'cash_refund_reject'], true)) {
                throw new RuntimeException('Store audit owner');
            }
        }
    };
    $assertNoExternalReferences = static function (array $s) use ($store, $ai,
        $lane, $cfg, $ac, $count, $read, $sp, $ap, $marker): void {
        $userId = count($s['users']) === 1 ? (int)$s['users'][0]['user_id'] : 0;
        $rechargeId = count($s['orders']) === 1 ? (int)$s['orders'][0]['order_id'] : 0;
        $dm = $lane === 'C';
        $catalog = static function (PDO $db, string $schema, array $columns) use ($dm): array {
            $names = array_map('strtoupper', $columns);
            $quoted = "'" . implode("','", $names) . "'";
            $sql = $dm
                ? 'SELECT TABLE_NAME,COLUMN_NAME FROM ALL_TAB_COLUMNS WHERE OWNER=?'
                    . ' AND COLUMN_NAME IN (' . $quoted . ') ORDER BY TABLE_NAME,COLUMN_NAME'
                : 'SELECT TABLE_NAME,COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS'
                    . ' WHERE TABLE_SCHEMA=? AND UPPER(COLUMN_NAME) IN (' . $quoted . ')'
                    . ' ORDER BY TABLE_NAME,COLUMN_NAME';
            $stmt = $db->prepare($sql);
            $stmt->execute([$schema]);
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        };
        if ($userId > 0 || $rechargeId > 0) {
            $ownedAi = ['YOSHOP_USER' => true, 'YOSHOP_RECHARGE_ORDER' => true,
                'YOSHOP_CASH_REFUND_ORDER' => true, 'YOSHOP_USER_BALANCE_LOG' => true];
            foreach ($catalog($ai, $dm ? 'AIZHCT_APP' : (string)$ac['database'],
                ['USER_ID', 'RECHARGE_ORDER_ID']) as $ref) {
                $ref = array_change_key_case($ref, CASE_LOWER);
                $physical = (string)$ref['table_name'];
                $table = strtoupper($physical);
                $column = strtoupper((string)$ref['column_name']);
                if (!preg_match('/^YOSHOP_[A-Z0-9_]+$/', $table)
                    || !in_array($column, ['USER_ID', 'RECHARGE_ORDER_ID'], true)) {
                    throw new RuntimeException('AI ref catalog');
                }
                if ($column === 'USER_ID' && isset($ownedAi[$table])) continue;
                $id = $column === 'USER_ID' ? $userId : $rechargeId;
                if ($id > 0 && $count($ai,
                    'SELECT COUNT(*) FROM ' . $physical . ' WHERE ' . $column . '=?', [$id]) !== 0) {
                    throw new RuntimeException('AI external user/recharge reference');
                }
            }
        }
        if (count($s['staff']) > 0) {
            foreach ($catalog($store, $dm ? 'ZHCT_APP' : (string)$cfg['database'],
                ['STAFF_UUID']) as $ref) {
                $ref = array_change_key_case($ref, CASE_LOWER);
                $physical = (string)$ref['table_name'];
                $table = strtoupper($physical);
                if (!preg_match('/^YDY_[A-Z0-9_]+$/', $table)) {
                    throw new RuntimeException('Store ref catalog');
                }
                if (in_array($table, ['YDY_STAFF', 'YDY_STAFF_DEPARTMENT'], true)) continue;
                if ($count($store, 'SELECT COUNT(*) FROM ' . $physical
                    . ' WHERE STAFF_UUID=?', [$marker]) !== 0) {
                    throw new RuntimeException('Store external staff reference');
                }
            }
        }
        foreach ($s['refunds'] as $refund) {
            $refundId = (int)$refund['id'];
            foreach ($read($store, 'SELECT ID,USER_UUID FROM ' . $sp
                . 'security_audit_log WHERE MODULE=? AND TARGET_ID=?',
                ['cash_refund', (string)$refundId], 10) as $audit) {
                if ((string)$audit['user_uuid'] !== $marker) {
                    throw new RuntimeException('foreign Store audit reference');
                }
            }
            foreach ($read($ai, 'SELECT ID,OPERATOR FROM ' . $ap
                . 'cash_refund_order_log WHERE REFUND_ID=?', [$refundId], 10) as $log) {
                if ((string)$log['operator'] !== $marker) {
                    throw new RuntimeException('foreign refund log reference');
                }
            }
            foreach ($read($ai, 'SELECT LOG_ID,USER_ID FROM ' . $ap
                . 'user_balance_log WHERE ORDER_ID=? AND SCENE=60',
                [$refundId], 10) as $log) {
                if ((int)$log['user_id'] !== $userId) {
                    throw new RuntimeException('foreign balance log reference');
                }
            }
        }
    };
    $cleanup = static function (array &$record) use ($own, $assertOwned,
        $assertNoExternalReferences, $saveJournal, $write, $store, $ai, $sp,
        $ap, $marker, $allZero): void {
        $s = $own();
        $assertOwned($s);
        $assertNoExternalReferences($s);
        $record['phase'] = 'cleanup-start';
        $record['events'][] = ['phase' => 'cleanup-start',
            'counts' => array_map('count', $s)];
        $saveJournal($record);
        foreach ($s['audits'] as $r) {
            if ($write($store, 'DELETE FROM ' . $sp . 'security_audit_log'
                . ' WHERE ID=? AND USER_UUID=? AND EVENT_CODE=? AND TARGET_ID=?',
                [(int)$r['id'], $marker, $r['event_code'], $r['target_id']]) !== 1) {
                throw new RuntimeException('audit cleanup affected');
            }
        }
        foreach ($s['balanceLogs'] as $r) {
            if ($write($ai, 'DELETE FROM ' . $ap . 'user_balance_log'
                . ' WHERE LOG_ID=? AND USER_ID=? AND ORDER_ID=? AND SCENE=60',
                [(int)$r['log_id'], (int)$r['user_id'], (int)$r['order_id']]) !== 1) {
                throw new RuntimeException('balance log cleanup affected');
            }
        }
        foreach ($s['refundLogs'] as $r) {
            if ($write($ai, 'DELETE FROM ' . $ap . 'cash_refund_order_log'
                . ' WHERE ID=? AND REFUND_ID=? AND OPERATOR=?',
                [(int)$r['id'], (int)$r['refund_id'], $marker]) !== 1) {
                throw new RuntimeException('refund log cleanup affected');
            }
        }
        foreach ($s['refunds'] as $r) {
            if ($write($ai, 'DELETE FROM ' . $ap . 'cash_refund_order'
                . ' WHERE ID=? AND STAFF_UUID=? AND USER_ID=? AND REASON=?',
                [(int)$r['id'], $marker, (int)$r['user_id'], $r['reason']]) !== 1) {
                throw new RuntimeException('refund cleanup affected');
            }
        }
        foreach ($s['orders'] as $r) {
            if ($write($ai, 'DELETE FROM ' . $ap . 'recharge_order'
                . ' WHERE ORDER_ID=? AND ORDER_NO=? AND USER_ID=?',
                [(int)$r['order_id'], 'R' . $marker, (int)$r['user_id']]) !== 1) {
                throw new RuntimeException('recharge cleanup affected');
            }
        }
        foreach ($s['users'] as $r) {
            if ($write($ai, 'DELETE FROM ' . $ap . 'user'
                . ' WHERE USER_ID=? AND NICK_NAME=? AND MOBILE=?',
                [(int)$r['user_id'], $marker, '000' . substr($marker, -8)]) !== 1) {
                throw new RuntimeException('AI user cleanup affected');
            }
        }
        foreach ($s['links'] as $r) {
            if ($write($store, 'DELETE FROM ' . $sp . 'staff_department'
                . ' WHERE ID=? AND STAFF_UUID=? AND CREATE_BY=?',
                [(int)$r['id'], $marker, $marker]) !== 1) {
                throw new RuntimeException('staff link cleanup affected');
            }
        }
        foreach ($s['staff'] as $r) {
            if ($write($store, 'DELETE FROM ' . $sp . 'staff'
                . ' WHERE ID=? AND UUID=? AND NAME=? AND CREATE_BY=?',
                [(int)$r['id'], $marker, $marker, $marker]) !== 1) {
                throw new RuntimeException('staff cleanup affected');
            }
        }
        foreach ($s['actor'] as $r) {
            if ($write($store, 'DELETE FROM ' . $sp . 'user'
                . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?',
                [(int)$r['id'], $marker, $marker, 'CODEX']) !== 1) {
                throw new RuntimeException('actor cleanup affected');
            }
        }
        $after = $own();
        $record['phase'] = $allZero($after) ? 'verified-clean' : 'cleanup-incomplete';
        $record['events'][] = ['phase' => $record['phase'],
            'counts' => array_map('count', $after)];
        $saveJournal($record);
        if (!$allZero($after)) throw new RuntimeException('cleanup not zero');
    };
    $state = $own();
    if ($mode === '--audit') {
        $previous = $loadJournal();
        $zero = $allZero($state);
        echo json_encode(['lane' => $lane, 'marker' => $marker,
            'journal_phase' => $previous['phase'] ?? '',
            'all_own_rows_zero' => $zero,
            'counts' => array_map('count', $state)]), PHP_EOL;
        exit($zero ? 0 : 3);
    }
    if ($mode === '--preflight') {
        if (file_exists($journalPath) || is_link($journalPath) || !$allZero($state)) {
            throw new RuntimeException('journal or marker collision');
        }
        $departments = $read($store, 'SELECT D.UUID,D.NAME FROM ' . $sp
            . 'department D WHERE D.STATUS=1 AND D.IS_SHOW=1 AND D.NAME<>?'
            . ' AND NOT EXISTS (SELECT 1 FROM ' . $sp . 'department C'
            . ' WHERE C.PUUID=D.UUID AND C.STATUS=1 AND C.IS_SHOW=1)'
            . ' ORDER BY D.ID ASC', [''], 10000);
        if (!$departments) throw new RuntimeException('active leaf department absent');
        $d = $departments[0];
        if ($count($store, 'SELECT COUNT(*) FROM ' . $sp
            . 'department WHERE UUID=?', [$d['uuid']]) !== 1) {
            throw new RuntimeException('department UUID not unique');
        }
        echo json_encode(['lane' => $lane, 'ready' => true,
            'marker' => $marker, 'own_counts' => array_map('count', $state),
            'leaf_department_sha12' => substr(hash('sha256', (string)$d['uuid']), 0, 12),
            'leaf_count_bounded' => count($departments),
            'no_card' => $count($store, 'SELECT COUNT(*) FROM ' . $sp
                . 'staff_card WHERE STAFF_UUID=? AND CARD_STATUS=1 AND IS_DELETE=0', [$marker]) === 0,
            'journal_absent' => true]), PHP_EOL;
        exit(0);
    }
    if ($mode === '--cleanup') {
        $journal = $loadJournal();
        $cleanup($journal);
        echo json_encode(['lane' => $lane, 'marker' => $marker,
            'phase' => $journal['phase'], 'own_counts' => array_map('count', $own())]), PHP_EOL;
        exit(0);
    }

    if (file_exists($journalPath) || is_link($journalPath) || !$allZero($state)) {
        throw new RuntimeException('journal or marker collision');
    }
    $departments = $read($store, 'SELECT D.UUID,D.NAME,D.STATUS,D.IS_SHOW FROM '
        . $sp . 'department D WHERE D.STATUS=1 AND D.IS_SHOW=1 AND D.NAME<>?'
        . ' AND NOT EXISTS (SELECT 1 FROM ' . $sp . 'department C'
        . ' WHERE C.PUUID=D.UUID AND C.STATUS=1 AND C.IS_SHOW=1)'
        . ' ORDER BY D.ID ASC', [''], 10000);
    if (!$departments) throw new RuntimeException('active leaf department absent');
    $department = $departments[0];
    if ($count($store, 'SELECT COUNT(*) FROM ' . $sp
        . 'department WHERE UUID=?', [$department['uuid']]) !== 1) {
        throw new RuntimeException('department UUID not unique');
    }
    $departmentSha = hash('sha256', json_encode($department,
        JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE));
    if ($count($store, 'SELECT COUNT(*) FROM ' . $sp . 'staff_card'
        . ' WHERE STAFF_UUID=? AND CARD_STATUS=1 AND IS_DELETE=0', [$marker]) !== 0) {
        throw new RuntimeException('synthetic staff has active card');
    }
    $journal['department_uuid'] = $department['uuid'];
    $journal['department_source_sha256'] = $departmentSha;
    $handle = fopen($journalPath, 'x');
    if (!is_resource($handle)) throw new RuntimeException('journal create');
    chmod($journalPath, 0600);
    $initial = json_encode($journal, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
    if (!is_string($initial) || fwrite($handle, $initial) !== strlen($initial)) {
        fclose($handle);
        throw new RuntimeException('journal initialize');
    }
    fflush($handle);
    fclose($handle);
    $capture = static function (string $label) use (&$journal, $own, $saveJournal,
        $marker): array {
        $s = $own();
        $journal['phase'] = $label;
        $journal['events'][] = ['phase' => $label,
            'counts' => array_map('count', $s),
            'refunds' => array_map(static function (array $r): array {
                return ['id' => (int)$r['id'], 'reason' => (string)$r['reason'],
                    'status' => (int)$r['status'],
                    'balance_log_id' => (int)$r['balance_log_id']];
            }, $s['refunds']),
            'audits' => array_map(static function (array $r): array {
                return ['id' => (int)$r['id'], 'event_code' => (string)$r['event_code'],
                    'target_id' => (string)$r['target_id']];
            }, $s['audits']),
            'user_balance' => count($s['users']) === 1
                ? number_format((float)$s['users'][0]['balance'], 2, '.', '') : null,
            'order_refundable' => count($s['orders']) === 1
                ? number_format((float)$s['orders'][0]['refundable_money'], 2, '.', '') : null];
        $saveJournal($journal);
        return $s;
    };
    $tree = static function ($value) use (&$tree) {
        if (is_int($value) || is_float($value)) return 'number';
        if (!is_array($value)) return gettype($value);
        if ($value === []) return [];
        if (array_keys($value) === range(0, count($value) - 1)) return [$tree($value[0])];
        $out = [];
        foreach ($value as $key => $item) $out[(string)$key] = $tree($item);
        ksort($out);
        return $out;
    };
    $post = static function (string $path, array $form, string $token) use ($base): array {
        if (!in_array($path, ['/p/cashRefund/add', '/p/cashRefund/agree',
            '/p/cashRefund/reject'], true)) throw new RuntimeException('HTTP path');
        $ctx = 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]]);
        $stream = @fopen($base . $path, 'rb', false, $ctx);
        $body = is_resource($stream) ? stream_get_contents($stream, 2097153) : false;
        $meta = is_resource($stream) ? stream_get_meta_data($stream) : [];
        if (is_resource($stream)) fclose($stream);
        if (!is_string($body) || strlen($body) > 2097152 || !empty($meta['timed_out'])
            || strpos((string)($http_response_header[0] ?? ''), ' 200 ') === false) {
            throw new RuntimeException('HTTP incomplete');
        }
        $json = json_decode($body, true);
        if (!is_array($json) || !array_key_exists('code', $json)
            || !array_key_exists('data', $json)) {
            throw new RuntimeException('HTTP invalid JSON contract');
        }
        return $json;
    };
    $findRefund = static function (array $s, string $reason) use ($marker): ?array {
        $matches = array_values(array_filter($s['refunds'],
            static function (array $r) use ($reason): bool {
                return (string)$r['reason'] === $reason;
            }));
        if (count($matches) > 1) throw new RuntimeException('duplicate own refund');
        return $matches[0] ?? null;
    };
    $auditMatches = static function (array $s, string $code, int $refundId): int {
        return count(array_filter($s['audits'], static function (array $r) use ($code, $refundId): bool {
            return (string)$r['event_code'] === $code
                && (int)$r['target_id'] === $refundId;
        }));
    };
    $outcome = ['lane' => $lane, 'marker' => $marker, 'routes' => [],
        'status' => 'started', 'journal' => $journalPath,
        'source_department_unchanged' => false, 'cleaned' => false];
    $failure = '';
    try {
        $stage = 'fixture-actor';
        if ($write($store, 'INSERT INTO ' . $sp . 'user'
            . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
            . ' VALUES (?,?,?,?,?,?,?,?,?)',
            [$marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
                $marker, 0, date('Y-m-d H:i:s'), 'CODEX', 0]) !== 1) {
            throw new RuntimeException('actor insert');
        }
        $s = $capture($stage);
        $actorId = (int)$s['actor'][0]['id'];
        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('token');

        $stage = 'fixture-ai-user';
        if ($write($ai, 'INSERT INTO ' . $ap . 'user'
            . ' (MOBILE,NICK_NAME,BALANCE,STORE_ID,IS_DELETE) VALUES (?,?,?,?,?)',
            ['000' . substr($marker, -8), $marker, '1.00', 10001, 0]) !== 1) {
            throw new RuntimeException('AI user insert');
        }
        $s = $capture($stage);
        $userId = (int)$s['users'][0]['user_id'];

        $stage = 'fixture-staff';
        if ($write($store, 'INSERT INTO ' . $sp . 'staff'
            . ' (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,STATUS,AI_USER_ID,MOBILE)'
            . ' VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
            [$marker, $marker, $marker, '2000-01-01', $marker, '[]', '2000-01-01',
                date('Y-m-d H:i:s'), $marker, $marker, $marker, $marker, '1',
                1, 1, $userId, '000' . substr($marker, -8)]) !== 1) {
            throw new RuntimeException('staff insert');
        }
        $s = $capture($stage);
        $stage = 'fixture-staff-link';
        if ($write($store, 'INSERT INTO ' . $sp . 'staff_department'
            . ' (DEPARTMENT_UUID,STAFF_UUID,CREATE_TIME,CREATE_BY) VALUES (?,?,?,?)',
            [$department['uuid'], $marker, date('Y-m-d H:i:s'), $marker]) !== 1) {
            throw new RuntimeException('staff link insert');
        }
        $s = $capture($stage);

        $stage = 'fixture-recharge';
        $now = time();
        if ($write($ai, 'INSERT INTO ' . $ap . 'recharge_order'
            . ' (ORDER_NO,USER_ID,PAY_PRICE,ACTUAL_MONEY,PAY_METHOD,PAY_STATUS,'
            . ' PAY_TIME,REFUND_MONEY,REFUNDABLE_MONEY,RECHARGE_DATE,STORE_ID,'
            . ' IS_DELETE,CREATE_TIME,UPDATE_TIME)'
            . ' VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
            ['R' . $marker, $userId, '1.00', '1.00', 'test', 20, $now,
                '0.00', '1.00', date('Y-m-d'), 10001, 0, $now, $now]) !== 1) {
            throw new RuntimeException('recharge insert');
        }
        $s = $capture($stage);
        $assertOwned($s);
        if ($count($store, 'SELECT COUNT(*) FROM ' . $sp . 'staff_card'
            . ' WHERE STAFF_UUID=? AND CARD_STATUS=1 AND IS_DELETE=0', [$marker]) !== 0) {
            throw new RuntimeException('active card before HTTP');
        }

        $stage = 'http-add-agree-target';
        $first = $post('/p/cashRefund/add', ['department_uuid' => $department['uuid'],
            'staff_uuid' => $marker, 'refund_amount' => '0.40',
            'refund_method' => 'cash', 'reason' => $marker . '-agree',
            'remark' => $marker], $token);
        $s = $capture($stage);
        $assertOwned($s);
        $firstRefund = $findRefund($s, $marker . '-agree');
        if ((int)$first['code'] !== 0 || !$firstRefund
            || (int)($first['data']['refund_id'] ?? 0) !== (int)$firstRefund['id']
            || (int)$firstRefund['status'] !== 10
            || count($s['refundLogs']) !== 1
            || $auditMatches($s, 'cash_refund_create', (int)$firstRefund['id']) !== 1
            || number_format((float)$s['users'][0]['balance'], 2, '.', '') !== '1.00'
            || number_format((float)$s['orders'][0]['refundable_money'], 2, '.', '') !== '1.00') {
            throw new RuntimeException('add1 AI/Store/response mismatch');
        }
        $outcome['routes']['add_first'] = $tree($first);

        $stage = 'http-agree';
        $agree = $post('/p/cashRefund/agree',
            ['refund_id' => (int)$firstRefund['id']], $token);
        $s = $capture($stage);
        $assertOwned($s);
        $firstRefund = $findRefund($s, $marker . '-agree');
        if ((int)$agree['code'] !== 0 || !$firstRefund
            || (int)($agree['data']['refund_id'] ?? 0) !== (int)$firstRefund['id']
            || (int)$firstRefund['status'] !== 20
            || count($s['balanceLogs']) !== 1
            || (int)$firstRefund['balance_log_id'] !== (int)$s['balanceLogs'][0]['log_id']
            || count($s['refundLogs']) !== 2
            || $auditMatches($s, 'cash_refund_agree', (int)$firstRefund['id']) !== 1
            || number_format((float)$s['users'][0]['balance'], 2, '.', '') !== '0.60'
            || number_format((float)$s['orders'][0]['refundable_money'], 2, '.', '') !== '0.60'
            || number_format((float)$s['orders'][0]['refund_money'], 2, '.', '') !== '0.40') {
            throw new RuntimeException('agree AI/Store/response mismatch');
        }
        $outcome['routes']['agree'] = $tree($agree);

        $stage = 'http-add-reject-target';
        $second = $post('/p/cashRefund/add', ['department_uuid' => $department['uuid'],
            'staff_uuid' => $marker, 'refund_amount' => '0.20',
            'refund_method' => 'cash', 'reason' => $marker . '-reject',
            'remark' => $marker], $token);
        $s = $capture($stage);
        $assertOwned($s);
        $secondRefund = $findRefund($s, $marker . '-reject');
        if ((int)$second['code'] !== 0 || !$secondRefund
            || (int)($second['data']['refund_id'] ?? 0) !== (int)$secondRefund['id']
            || (int)$secondRefund['status'] !== 10
            || count($s['refundLogs']) !== 3
            || $auditMatches($s, 'cash_refund_create', (int)$secondRefund['id']) !== 1
            || number_format((float)$s['users'][0]['balance'], 2, '.', '') !== '0.60') {
            throw new RuntimeException('add2 AI/Store/response mismatch');
        }
        $outcome['routes']['add_second'] = $tree($second);

        $stage = 'http-reject';
        $reject = $post('/p/cashRefund/reject',
            ['refund_id' => (int)$secondRefund['id'],
                'reject_reason' => $marker . '-reason'], $token);
        $s = $capture($stage);
        $assertOwned($s);
        $secondRefund = $findRefund($s, $marker . '-reject');
        if ((int)$reject['code'] !== 0 || !$secondRefund
            || (int)($reject['data']['refund_id'] ?? 0) !== (int)$secondRefund['id']
            || (int)$secondRefund['status'] !== 30
            || count($s['refundLogs']) !== 4
            || count($s['balanceLogs']) !== 1
            || $auditMatches($s, 'cash_refund_reject', (int)$secondRefund['id']) !== 1
            || number_format((float)$s['users'][0]['balance'], 2, '.', '') !== '0.60'
            || number_format((float)$s['orders'][0]['refundable_money'], 2, '.', '') !== '0.60') {
            throw new RuntimeException('reject AI/Store/response mismatch');
        }
        $outcome['routes']['reject'] = $tree($reject);
        $outcome['status'] = 'business-pass-before-cleanup';
    } catch (Throwable $error) {
        $failure = $stage . ':' . get_class($error) . ':' . (string)$error->getCode()
            . ':' . substr(hash('sha256', $error->getMessage()), 0, 12);
        $outcome['status'] = 'business-fail';
        $outcome['failure'] = $failure;
        try { $capture('failure-readback-' . $stage); }
        catch (Throwable $readbackError) {
            $outcome['failure_readback_sha12'] = substr(hash('sha256',
                $readbackError->getMessage()), 0, 12);
        }
    }
    try {
        $departmentAfter = $one($store, 'SELECT UUID,NAME,STATUS,IS_SHOW FROM '
            . $sp . 'department WHERE UUID=?', [$department['uuid']]);
        $outcome['source_department_unchanged'] = is_array($departmentAfter)
            && hash('sha256', json_encode($departmentAfter,
                JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE)) === $departmentSha;
    } catch (Throwable $departmentError) {
        $outcome['source_department_error_sha12'] = substr(hash('sha256',
            $departmentError->getMessage()), 0, 12);
    }
    try {
        $cleanup($journal);
        $outcome['cleaned'] = true;
    } catch (Throwable $cleanupError) {
        $outcome['cleanup_failure_sha12'] = substr(hash('sha256',
            $cleanupError->getMessage()), 0, 12);
        $outcome['status'] = 'cleanup-needs-attention';
    }
    echo json_encode($outcome, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
    exit($failure === '' && $outcome['status'] === 'business-pass-before-cleanup'
        && $outcome['cleaned'] && $outcome['source_department_unchanged'] ? 0 : 1);
} catch (Throwable $error) {
    echo json_encode(['lane' => $lane, 'mode' => $mode, 'stage' => $stage,
        'status' => 'blocked', 'class' => get_class($error),
        'message_sha12' => substr(hash('sha256', $error->getMessage()), 0, 12)]), PHP_EOL;
    exit(2);
}
