<?php
declare(strict_types=1);

// Default-deny B/C Store billing write flow; only 2099-02 and one marker owner.
$lane = (string)($argv[1] ?? '');
$mode = (string)($argv[2] ?? '');
$marker = (string)getenv('CDX_BW3_MARKER');
if (!in_array($lane, ['B', 'C'], true)
    || !in_array($mode, ['--preflight', '--preflight-cleanup', '--execute', '--cleanup', '--audit'], true)
    || !preg_match('/^CDXBW3[A-F0-9]{12}$/', $marker)
    || (in_array($mode, ['--execute', '--cleanup'], true)
        && ((string)getenv('CDX_BW3_APPROVE') !== 'ROOT_APPROVED_BILLING_WRITE3_V1'
            || !hash_equals(hash_file('sha256', __FILE__),
                (string)getenv('CDX_BW3_SHA'))))) {
    fwrite(STDERR, "BW3 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;
$month = '2099-02';
$journalPath = '/tmp/cdx-billing-write3-' . $lane . '-' . $marker . '.journal.json';
$sourceHashes = [
    '/application/route.php' => '081b0186b7f40f0bdd237cecb3e2d678fc9344c656932c0d3b3052404e4f1510',
    '/application/p/controller/BillingRecord.php' => 'ef2e8f2a835f21ad92d6dfff7b49e71a79a9cbd2d2dfefc204f452ea4a2e1f1d',
    '/application/p/logic/BillingRecord.php' => '42bf383374c80a8774c014048c189f5c0af135ed30ef71715c05b0c97719f8ca',
    '/application/common/service/database/AdvisoryLock.php' => '347a4524f9b9fbd171ea8e8a8bf85657b01a300a2442383fb18c96fc9f3aa482',
    '/application/base/AdminController.php' => '7565b36fa4dda2c21cc4fa3b37753dbffb7c3a464bb8c11ea0f17ca00bececa5',
    '/application/common.php' => '1ff0e49451950d671498ad5e1f7380f60448d2f7a39041d4b53370abd6e6d78f',
];
$stage = 'boundary';
try {
    $runtimeReady = static function () use ($pid, $root, $base): bool {
        $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);
        return 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;
    };
    if (PHP_VERSION_ID < 70300 || PHP_VERSION_ID >= 70400
        || is_link($root) || !$runtimeReady()) {
        throw new RuntimeException('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_') {
        throw new RuntimeException('database config');
    }
    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)) {
                throw new RuntimeException('not code-local MySQL');
            }
        }
        $dsn = 'mysql:host=' . $cfg['hostname'] . ';port=' . ($cfg['hostport'] ?? 3306)
            . ';dbname=' . $cfg['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('DM worker config');
            }
        }
        if ((string)getenv('POC_HOST') !== '127.0.0.1'
            || (string)getenv('POC_PORT') !== '5237'
            || strtoupper((string)getenv('POC_SCHEMA')) !== 'ZHCT_APP') {
            throw new RuntimeException('not approved DM');
        }
        $dsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
    }
    $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 = $lane === 'B'
        ? (string)$db->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")->fetchColumn()
        : 'ZHCT_APP|5237|' . (string)$db->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn();
    if (hash('sha256', $identity) !== ($lane === 'B'
            ? '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401'
            : 'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194')) {
        throw new RuntimeException('DB identity');
    }
    $p = $lane === 'B' ? 'ydy_' : 'YDY_';
    $read = static function (string $sql, array $args = [], int $cap = 100) use ($db): array {
        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;
    };
    $count = static function (string $sql, array $args = []) use ($read): int {
        $rows = $read($sql, $args, 1);
        return $rows ? (int)reset($rows[0]) : 0;
    };
    $write = static function (string $sql, array $args) use ($db): 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();
    };
    $sourceCounts = static function () use ($count, $p, $month): array {
        return [
            'meal_orders' => $count('SELECT COUNT(*) FROM ' . $p . 'meal_order'
                . ' WHERE BIND_TIME>=? AND BIND_TIME<?',
                ['2099-02-01 00:00:00', '2099-03-01 00:00:00']),
            'bills' => $count('SELECT COUNT(*) FROM ' . $p . 'billing_record'
                . ' WHERE BILL_MONTH=?', [$month]),
            'bill_order_snapshots' => $count('SELECT COUNT(*) FROM ' . $p
                . 'billing_record_order WHERE BILL_MONTH=?', [$month]),
            'bill_no_prefix' => $count('SELECT COUNT(*) FROM ' . $p
                . 'billing_record WHERE BILL_NO LIKE ?', ['BILL9902%']),
        ];
    };
    $own = static function () use ($read, $p, $marker, $month): array {
        $actor = $read('SELECT ID,UUID,ACCOUNT,NAME,TYPE,CREATE_BY,STAFF_UUID FROM '
            . $p . 'user WHERE UUID=? OR ACCOUNT=?', [$marker, $marker], 3);
        $staff = $read('SELECT ID,UUID,NAME,CREATE_BY,PERSONNEL_UMBER FROM '
            . $p . 'staff WHERE UUID=?', [$marker], 3);
        $links = $read('SELECT ID,STAFF_UUID,DEPARTMENT_UUID,CREATE_BY FROM '
            . $p . 'staff_department WHERE STAFF_UUID=?', [$marker], 3);
        $orders = $read('SELECT ID,ORDER_NO,USER_ID,BIND_TIME,MEAL_TIMES,ORDER_STATUS,'
            . ' PAY_PRICE,REFUND_AMOUNT,IS_DELETE FROM ' . $p . 'meal_order WHERE ORDER_NO=?',
            ['M' . $marker], 3);
        $bills = $read('SELECT ID,BILL_NO,BILL_MONTH,USER_ID,STAFF_UUID,AMOUNT,'
            . ' ORIGINAL_AMOUNT,STATUS,PARENT_MOBILE,EXPIRE_TIME,STORE_ID,IS_DELETE FROM '
            . $p . 'billing_record WHERE STAFF_UUID=? AND BILL_MONTH=?',
            [$marker, $month], 3);
        $billId = count($bills) === 1 ? (int)$bills[0]['id'] : 0;
        $details = $billId > 0 ? $read('SELECT ID,BILL_ID,MEAL_TYPE,ORIGINAL_COUNT,'
            . ' ADJUSTED_COUNT,ORIGINAL_AMOUNT,ADJUSTED_AMOUNT,STORE_ID,IS_DELETE FROM '
            . $p . 'billing_record_detail WHERE BILL_ID=? ORDER BY MEAL_TYPE',
            [$billId], 5) : [];
        $snapshots = $billId > 0 ? $read('SELECT ID,BILL_ID,MEAL_ORDER_ID,ORDER_NO,'
            . ' AMOUNT,STORE_ID,IS_DELETE FROM ' . $p . 'billing_record_order WHERE BILL_ID=?',
            [$billId], 4) : [];
        $logs = $read('SELECT ID,BILL_ID,ACTION,STATUS_BEFORE,STATUS_AFTER,OPERATOR,STORE_ID FROM '
            . $p . 'billing_record_log WHERE OPERATOR=?', [$marker], 5);
        return compact('actor', 'staff', 'links', 'orders', 'bills',
            'details', 'snapshots', 'logs');
    };
    $journal = ['lane' => $lane, 'marker' => $marker, 'month' => $month,
        'identity_sha256' => hash('sha256', $identity),
        'phase' => 'before-fixture', 'events' => []];
    $saveJournal = static function (array $record) use ($journalPath): void {
        if (!is_file($journalPath) || is_link($journalPath)) {
            throw new RuntimeException('journal missing/symlink');
        }
        $json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        if (!is_string($json)) throw new RuntimeException('journal encode');
        $temporary = $journalPath . '.tmp.' . bin2hex(random_bytes(4));
        $handle = fopen($temporary, 'x');
        if (!is_resource($handle)) throw new RuntimeException('journal temp');
        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, $month, $identity): array {
        if (!is_file($journalPath) || is_link($journalPath)) {
            throw new RuntimeException('journal missing/symlink');
        }
        $j = json_decode((string)file_get_contents($journalPath), true);
        if (!is_array($j) || ($j['lane'] ?? null) !== $lane
            || ($j['marker'] ?? null) !== $marker || ($j['month'] ?? null) !== $month
            || ($j['identity_sha256'] ?? null) !== hash('sha256', $identity)) {
            throw new RuntimeException('journal identity');
        }
        return $j;
    };
    $allZero = static function (array $state): bool {
        foreach ($state as $rows) if (count($rows) !== 0) return false;
        return true;
    };
    $assertOwned = static function (array $s) use ($marker, $month, &$journal): void {
        foreach (['actor' => 1, 'staff' => 1, 'links' => 1, 'orders' => 1,
            'bills' => 1, 'details' => 3, 'snapshots' => 1, 'logs' => 3] as $key => $max) {
            if (count($s[$key]) > $max) throw new RuntimeException('own row cap ' . $key);
        }
        $userId = count($s['actor']) === 1 ? (int)$s['actor'][0]['id'] : 0;
        $orderId = count($s['orders']) === 1 ? (int)$s['orders'][0]['id'] : 0;
        $billId = count($s['bills']) === 1 ? (int)$s['bills'][0]['id'] : 0;
        foreach ($s['actor'] as $r) {
            if ($r['uuid'] !== $marker || $r['account'] !== $marker
                || $r['name'] !== $marker || $r['staff_uuid'] !== $marker
                || (int)$r['type'] !== 0 || $r['create_by'] !== 'CODEX') {
                throw new RuntimeException('actor owner');
            }
        }
        foreach ($s['staff'] as $r) {
            if ($r['uuid'] !== $marker || $r['name'] !== $marker
                || $r['personnel_umber'] !== $marker
                || $r['create_by'] !== $marker) {
                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['orders'] as $r) {
            if ($r['order_no'] !== 'M' . $marker
                || (int)$r['user_id'] !== $userId
                || !preg_match('/^2099-02-01 08:00:00(?:\.0{1,6})?$/',
                    (string)$r['bind_time'])
                || (int)$r['meal_times'] !== 1
                || (int)$r['order_status'] !== 30
                || (int)$r['is_delete'] !== 0
                || number_format((float)$r['pay_price'], 2, '.', '') !== '1.20'
                || number_format((float)$r['refund_amount'], 2, '.', '') !== '0.20') {
                throw new RuntimeException('meal order owner');
            }
        }
        foreach ($s['bills'] as $r) {
            if ((int)$r['user_id'] !== $userId || $r['staff_uuid'] !== $marker
                || $r['bill_month'] !== $month
                || strpos((string)$r['bill_no'], 'BILL9902') !== 0
                || (int)$r['store_id'] !== 10001 || (int)$r['is_delete'] !== 0
                || (int)$r['status'] !== 10 && (int)$r['status'] !== 20
                || $r['parent_mobile'] !== '00000000000'
                || number_format((float)$r['original_amount'], 2, '.', '') !== '1.00'
                || !in_array(number_format((float)$r['amount'], 2, '.', ''),
                    ['1.00', '0.90'], true)) {
                throw new RuntimeException('bill owner/amount');
            }
        }
        $types = [];
        foreach ($s['details'] as $r) {
            $type = (int)$r['meal_type'];
            if ((int)$r['bill_id'] !== $billId || (int)$r['store_id'] !== 10001
                || (int)$r['is_delete'] !== 0 || !in_array($type, [1, 2, 3], true)
                || isset($types[$type])) {
                throw new RuntimeException('detail owner');
            }
            $types[$type] = true;
        }
        foreach ($s['snapshots'] as $r) {
            if ((int)$r['bill_id'] !== $billId
                || (int)$r['meal_order_id'] !== $orderId
                || $r['order_no'] !== 'M' . $marker
                || (int)$r['store_id'] !== 10001
                || (int)$r['is_delete'] !== 0
                || number_format((float)$r['amount'], 2, '.', '') !== '1.00') {
                throw new RuntimeException('snapshot owner');
            }
        }
        foreach ($s['logs'] as $r) {
            if ((int)$r['bill_id'] !== $billId
                || $r['operator'] !== $marker
                || (int)$r['store_id'] !== 10001
                || !in_array($r['action'],
                    ['generate', 'adjust_amount', 'issue_payment'], true)) {
                throw new RuntimeException('bill log owner');
            }
        }
    };
    $assertNoExternalReferences = static function (array $s) use ($db, $lane,
        $cfg, $p, $marker, $read, $count): array {
        $userId = count($s['actor']) === 1 ? (int)$s['actor'][0]['id'] : 0;
        $orderId = count($s['orders']) === 1 ? (int)$s['orders'][0]['id'] : 0;
        $billId = count($s['bills']) === 1 ? (int)$s['bills'][0]['id'] : 0;
        $billNo = count($s['bills']) === 1 ? (string)$s['bills'][0]['bill_no'] : '';
        if ($lane === 'C') {
            $stmt = $db->prepare("SELECT TABLE_NAME,COLUMN_NAME,DATA_TYPE FROM ALL_TAB_COLUMNS"
                . " WHERE OWNER=? AND COLUMN_NAME IN"
                . " ('USER_ID','STAFF_UUID','MEAL_ORDER_ID','BILL_ID','ORDER_NO','BILL_NO')"
                . " ORDER BY TABLE_NAME,COLUMN_NAME");
            $stmt->execute(['ZHCT_APP']);
        } else {
            $stmt = $db->prepare("SELECT TABLE_NAME,COLUMN_NAME,DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS"
                . " WHERE TABLE_SCHEMA=? AND UPPER(COLUMN_NAME) IN"
                . " ('USER_ID','STAFF_UUID','MEAL_ORDER_ID','BILL_ID','ORDER_NO','BILL_NO')"
                . " ORDER BY TABLE_NAME,COLUMN_NAME");
            $stmt->execute([(string)$cfg['database']]);
        }
        $catalog = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (count($catalog) > 1000) throw new RuntimeException('reference catalog cap');
        $owned = [
            'USER_ID' => ['YDY_MEAL_ORDER', 'YDY_BILLING_RECORD', 'YDY_BILLING_RECORD_ORDER'],
            'STAFF_UUID' => ['YDY_USER', 'YDY_STAFF', 'YDY_STAFF_DEPARTMENT', 'YDY_BILLING_RECORD'],
            'MEAL_ORDER_ID' => ['YDY_BILLING_RECORD_ORDER'],
            'BILL_ID' => ['YDY_BILLING_RECORD_DETAIL', 'YDY_BILLING_RECORD_ORDER', 'YDY_BILLING_RECORD_LOG'],
            'ORDER_NO' => ['YDY_MEAL_ORDER', 'YDY_BILLING_RECORD_ORDER'],
            'BILL_NO' => ['YDY_BILLING_RECORD'],
        ];
        $excludedNumericSortColumns = [];
        $scanned = 0;
        foreach ($catalog as $ref) {
            $ref = array_change_key_case($ref, CASE_LOWER);
            $physical = (string)$ref['table_name'];
            $table = strtoupper($physical);
            $column = strtoupper((string)$ref['column_name']);
            $dataType = strtolower((string)$ref['data_type']);
            if (!preg_match('/^YDY_[A-Z0-9_]+$/', $table)
                || !isset($owned[$column])) {
                throw new RuntimeException('reference catalog name');
            }
            if (in_array($column, ['ORDER_NO', 'BILL_NO'], true)
                && !in_array($dataType, ['char', 'varchar', 'varchar2',
                    'text', 'tinytext', 'mediumtext', 'longtext', 'clob'], true)) {
                // Many unrelated tables use numeric ORDER_NO for display sorting.
                $excludedNumericSortColumns[] = $table . '.' . $column . ':' . $dataType;
                continue;
            }
            if (in_array($table, $owned[$column], true)) continue;
            $scanned++;
            $id = $column === 'USER_ID' ? $userId
                : ($column === 'STAFF_UUID' ? $marker
                    : ($column === 'MEAL_ORDER_ID' ? $orderId
                        : ($column === 'BILL_ID' ? $billId
                            : ($column === 'ORDER_NO' ? 'M' . $marker : $billNo))));
            if ($id === 0 || $id === '') {
                $count('SELECT COUNT(*) FROM ' . $physical . ' WHERE 1=0');
            } elseif (in_array($column, ['ORDER_NO', 'BILL_NO'], true)) {
                $matches = $read('SELECT ' . $column . ' AS V FROM ' . $physical
                    . ' WHERE ' . $column . '=?', [(string)$id], 100);
                foreach ($matches as $match) {
                    if (hash_equals((string)$id, (string)$match['v'])) {
                        throw new RuntimeException('external string reference');
                    }
                }
            } elseif ($count('SELECT COUNT(*) FROM ' . $physical
                . ' WHERE ' . $column . '=?', [$id]) !== 0) {
                throw new RuntimeException('external ID/UUID reference');
            }
        }
        if ($orderId > 0 && $count('SELECT COUNT(*) FROM ' . $p
            . 'billing_record_order WHERE MEAL_ORDER_ID=? AND BILL_ID<>?',
            [$orderId, $billId]) !== 0) {
            throw new RuntimeException('foreign order snapshot');
        }
        if ($userId > 0 && ($count('SELECT COUNT(*) FROM ' . $p
                . 'meal_order WHERE USER_ID=? AND ORDER_NO<>?',
                [$userId, 'M' . $marker]) !== 0
            || $count('SELECT COUNT(*) FROM ' . $p
                . 'billing_record WHERE USER_ID=? AND STAFF_UUID<>?',
                [$userId, $marker]) !== 0)) {
            throw new RuntimeException('foreign same-user order/bill');
        }
        if ($billId > 0 && $count('SELECT COUNT(*) FROM ' . $p
            . 'billing_record_log WHERE BILL_ID=? AND OPERATOR<>?',
            [$billId, $marker]) !== 0) {
            throw new RuntimeException('foreign bill log');
        }
        return ['catalog_columns' => count($catalog), 'scanned_columns' => $scanned,
            'excluded_numeric_sort_columns' => $excludedNumericSortColumns];
    };
    $cleanup = static function (array &$record) use ($db, $own, $assertOwned,
        $assertNoExternalReferences, $saveJournal, $write, $p, $marker,
        $month, $allZero): void {
        $s = $own();
        $assertOwned($s);
        $assertNoExternalReferences($s);
        $record['phase'] = 'cleanup-start';
        $record['events'][] = ['phase' => 'cleanup-start',
            'counts' => array_map('count', $s)];
        $saveJournal($record);
        if (!$db->beginTransaction()) throw new RuntimeException('cleanup transaction begin');
        try {
        foreach ($s['logs'] as $r) {
            if ($write('DELETE FROM ' . $p . 'billing_record_log'
                . ' WHERE ID=? AND BILL_ID=? AND OPERATOR=?',
                [(int)$r['id'], (int)$r['bill_id'], $marker]) !== 1) {
                throw new RuntimeException('log cleanup affected');
            }
        }
        foreach ($s['snapshots'] as $r) {
            if ($write('DELETE FROM ' . $p . 'billing_record_order'
                . ' WHERE ID=? AND BILL_ID=? AND MEAL_ORDER_ID=? AND ORDER_NO=?',
                [(int)$r['id'], (int)$r['bill_id'], (int)$r['meal_order_id'],
                    'M' . $marker]) !== 1) {
                throw new RuntimeException('snapshot cleanup affected');
            }
        }
        foreach ($s['details'] as $r) {
            if ($write('DELETE FROM ' . $p . 'billing_record_detail'
                . ' WHERE ID=? AND BILL_ID=? AND MEAL_TYPE=?',
                [(int)$r['id'], (int)$r['bill_id'], (int)$r['meal_type']]) !== 1) {
                throw new RuntimeException('detail cleanup affected');
            }
        }
        foreach ($s['bills'] as $r) {
            if ($write('DELETE FROM ' . $p . 'billing_record'
                . ' WHERE ID=? AND STAFF_UUID=? AND USER_ID=? AND BILL_MONTH=?',
                [(int)$r['id'], $marker, (int)$r['user_id'], $month]) !== 1) {
                throw new RuntimeException('bill cleanup affected');
            }
        }
        foreach ($s['orders'] as $r) {
            if ($write('DELETE FROM ' . $p . 'meal_order'
                . ' WHERE ID=? AND ORDER_NO=? AND USER_ID=? AND BIND_TIME=?',
                [(int)$r['id'], 'M' . $marker, (int)$r['user_id'],
                    '2099-02-01 08:00:00']) !== 1) {
                throw new RuntimeException('meal order cleanup affected');
            }
        }
        foreach ($s['links'] as $r) {
            if ($write('DELETE FROM ' . $p . '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('DELETE FROM ' . $p . '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('DELETE FROM ' . $p . 'user'
                . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND CREATE_BY=?',
                [(int)$r['id'], $marker, $marker, 'CODEX']) !== 1) {
                throw new RuntimeException('actor cleanup affected');
            }
        }
        $after = $own();
        if (!$allZero($after)) throw new RuntimeException('cleanup not zero');
        if (!$db->commit()) throw new RuntimeException('cleanup transaction commit');
        } catch (Throwable $cleanupError) {
            $rolledBack = false;
            try {
                if ($db->inTransaction()) {
                    $rolledBack = $db->rollBack();
                }
            } catch (Throwable $rollbackError) {
                $record['rollback_error_sha12'] = substr(hash('sha256',
                    $rollbackError->getMessage()), 0, 12);
            }
            $record['phase'] = $rolledBack ? 'cleanup-rolled-back' : 'cleanup-uncertain';
            $record['events'][] = ['phase' => $record['phase'],
                'failure_sha12' => substr(hash('sha256',
                    $cleanupError->getMessage()), 0, 12)];
            $saveJournal($record);
            throw $cleanupError;
        }
        $record['phase'] = 'verified-clean';
        $record['events'][] = ['phase' => $record['phase'],
            'counts' => array_map('count', $own())];
        $saveJournal($record);
    };
    $state = $own();
    if ($mode === '--audit') {
        $j = $loadJournal();
        $zero = $allZero($state);
        $futureCounts = $sourceCounts();
        $monthZero = array_sum($futureCounts) === 0;
        $verifiedClean = ($j['phase'] ?? null) === 'verified-clean';
        echo json_encode(['lane' => $lane, 'marker' => $marker,
            'phase' => $j['phase'] ?? '', 'all_own_rows_zero' => $zero,
            'future_month_zero' => $monthZero,
            'future_month_counts' => $futureCounts,
            'counts' => array_map('count', $state)]), PHP_EOL;
        exit($zero && $monthZero && $verifiedClean ? 0 : 3);
    }
    if ($mode === '--preflight-cleanup') {
        $journal = $loadJournal();
        if (($journal['phase'] ?? '') !== 'failure-readback-fixture-meal-order'
            || count($journal['events'] ?? []) !== 5
            || array_map('count', $state) !== [
                'actor' => 1, 'staff' => 1, 'links' => 1, 'orders' => 1,
                'bills' => 0, 'details' => 0, 'snapshots' => 0, 'logs' => 0,
            ] || $sourceCounts() !== [
                'meal_orders' => 1, 'bills' => 0,
                'bill_order_snapshots' => 0, 'bill_no_prefix' => 0,
            ]) {
            throw new RuntimeException('retained fixture boundary');
        }
        $assertOwned($state);
        $references = $assertNoExternalReferences($state);
        $department = $read('SELECT UUID,NAME,STATUS,IS_SHOW FROM '
            . $p . 'department WHERE UUID=?', [$journal['department_uuid']], 2);
        if (count($department) !== 1
            || hash('sha256', json_encode($department,
                JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE))
                !== ($journal['department_sha256'] ?? '')) {
            throw new RuntimeException('source department drift');
        }
        echo json_encode(['lane' => $lane, 'marker' => $marker,
            'ready_for_exact_cleanup' => true,
            'own_counts' => array_map('count', $state),
            'reference_scan' => $references,
            'department_unchanged' => true,
            'business_http' => 0, 'dml' => 0]), PHP_EOL;
        exit(0);
    }
    $stage = 'preflight';
    if ($mode === '--preflight' || $mode === '--execute') {
        if (file_exists($journalPath) || is_link($journalPath) || !$allZero($state)) {
            throw new RuntimeException('marker/journal collision');
        }
        $referenceScan = $assertNoExternalReferences($state);
        $source = $sourceCounts();
        if (array_sum($source) !== 0) throw new RuntimeException('future month not empty');
        $departments = $read('SELECT D.UUID,D.NAME FROM ' . $p
            . 'department D WHERE D.STATUS=1 AND D.IS_SHOW=1 AND D.NAME<>?'
            . ' AND NOT EXISTS (SELECT 1 FROM ' . $p . '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('leaf department absent');
        $department = $departments[0];
        if ($count('SELECT COUNT(*) FROM ' . $p . 'department WHERE UUID=?',
            [$department['uuid']]) !== 1) {
            throw new RuntimeException('department UUID not unique');
        }
        if ($lane === 'B') {
            $holder = $read('SELECT IS_USED_LOCK(?) AS HOLDER',
                ['billing_record_10001_209902'], 1);
            if (($holder[0]['holder'] ?? null) !== null) {
                throw new RuntimeException('month lock occupied');
            }
        }
        $preflight = ['lane' => $lane, 'ready' => true, 'marker' => $marker,
            'month' => $month, 'source_counts' => $source,
            'own_counts' => array_map('count', $state),
            'leaf_department_sha12' => substr(hash('sha256',
                (string)$department['uuid']), 0, 12),
            'leaf_count_bounded' => count($departments),
            'journal_absent' => true,
            'lock_free' => $lane === 'B' ? true : 'checked-at-acquire',
            'reference_scan' => $referenceScan];
        if ($mode === '--preflight') {
            echo json_encode($preflight), PHP_EOL;
            exit(0);
        }
    }
    if ($mode === '--cleanup') {
        $journal = $loadJournal();
        $cleanup($journal);
        echo json_encode(['lane' => $lane, 'marker' => $marker,
            'phase' => $journal['phase'], 'counts' => array_map('count', $own()),
            'future_month_counts' => $sourceCounts()]), PHP_EOL;
        exit(0);
    }

    $departmentBefore = $read('SELECT UUID,NAME,STATUS,IS_SHOW FROM ' . $p
        . 'department WHERE UUID=?', [$department['uuid']], 2);
    if (count($departmentBefore) !== 1) throw new RuntimeException('department row');
    $departmentSha = hash('sha256', json_encode($departmentBefore,
        JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE));
    $journal['department_uuid'] = $department['uuid'];
    $journal['department_sha256'] = $departmentSha;
    $journal['future_month_before'] = $source;
    $initial = json_encode($journal, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
    if (!is_string($initial)) throw new RuntimeException('journal encode');
    $handle = fopen($journalPath, 'x');
    if (!is_resource($handle)) throw new RuntimeException('journal create');
    chmod($journalPath, 0600);
    $written = fwrite($handle, $initial);
    fflush($handle);
    fclose($handle);
    if ($written !== strlen($initial)) throw new RuntimeException('journal initialize');
    $capture = static function (string $label) use (&$journal, $own,
        $saveJournal, $sourceCounts): array {
        $s = $own();
        $journal['phase'] = $label;
        $journal['events'][] = ['phase' => $label,
            'counts' => array_map('count', $s),
            'future_month_counts' => $sourceCounts(),
            'bills' => array_map(static function (array $r): array {
                return ['id' => (int)$r['id'], 'status' => (int)$r['status'],
                    'amount' => number_format((float)$r['amount'], 2, '.', '')];
            }, $s['bills']),
            'log_actions' => array_column($s['logs'], 'action')];
        $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, $runtimeReady): array {
        if (!in_array($path, ['/p/billingRecord/generate',
            '/p/billingrecord/adjustamount',
            '/p/billingrecord/issuepayment'], 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]]);
        $stream = @fopen($base . $path, 'rb', false, $context);
        $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 JSON contract');
        }
        return $json;
    };
    $outcome = ['lane' => $lane, 'marker' => $marker,
        'routes' => [], 'status' => 'started', 'cleaned' => false,
        'department_unchanged' => false, 'fixture_source_unchanged' => false,
        'journal' => $journalPath];
    $failure = '';
    try {
        $stage = 'fixture-user';
        if ($write('INSERT INTO ' . $p . 'user'
            . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,STAFF_UUID,MOBILE,REGISTER_MOBILE,'
            . ' CREATE_TIME,CREATE_BY,IS_DELETE) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)',
            [$marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
                $marker, 0, $marker, '00000000000', '00000000000',
                date('Y-m-d H:i:s'), 'CODEX', 0]) !== 1) {
            throw new RuntimeException('user 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-staff';
        if ($write('INSERT INTO ' . $p . '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,MOBILE,REGISTER_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, '00000000000', '00000000000']) !== 1) {
            throw new RuntimeException('staff insert');
        }
        $s = $capture($stage);

        $stage = 'fixture-department-link';
        if ($write('INSERT INTO ' . $p . '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 department insert');
        }
        $s = $capture($stage);

        $stage = 'fixture-meal-order';
        if ($write('INSERT INTO ' . $p . 'meal_order'
            . ' (ORDER_NO,TOTAL_PRICE,ORDER_PRICE,PAY_PRICE,REFUND_AMOUNT,'
            . ' PAY_STATUS,PAY_TIME,MEAL_DATE,MEAL_TIMES,BIND_TIME,SOURCE,'
            . ' ORDER_STATUS,USER_ID,IS_DELETE,USER_MOBILE,USER_NAME)'
            . ' VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
            ['M' . $marker, '1.20', '1.20', '1.20', '0.20', 20,
                '2099-02-01 08:00:00', '2099-02-01', 1, '2099-02-01 08:00:00',
                3, 30, $actorId, 0, '00000000000', $marker]) !== 1) {
            throw new RuntimeException('meal order insert');
        }
        $s = $capture($stage);
        $assertOwned($s);
        $counts = $sourceCounts();
        if ($counts !== ['meal_orders' => 1, 'bills' => 0,
            'bill_order_snapshots' => 0, 'bill_no_prefix' => 0]) {
            throw new RuntimeException('future month unexpected source');
        }
        $fixtureRows = [
            'user' => $read('SELECT * FROM ' . $p . 'user WHERE ID=?', [$actorId], 2),
            'staff' => $read('SELECT * FROM ' . $p . 'staff WHERE UUID=?', [$marker], 2),
            'order' => $read('SELECT * FROM ' . $p . 'meal_order WHERE ORDER_NO=?',
                ['M' . $marker], 2),
        ];
        $fixtureSha = hash('sha256', json_encode($fixtureRows,
            JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE));
        $journal['fixture_source_sha256'] = $fixtureSha;
        $saveJournal($journal);

        $stage = 'http-generate';
        $generated = $post('/p/billingRecord/generate', ['bill_month' => $month], $token);
        $s = $capture($stage);
        $assertOwned($s);
        if ((int)$generated['code'] !== 0
            || (int)($generated['data']['created'] ?? -1) !== 1
            || count($s['bills']) !== 1 || count($s['details']) !== 3
            || count($s['snapshots']) !== 1 || count($s['logs']) !== 1
            || $s['logs'][0]['action'] !== 'generate'
            || (int)$s['bills'][0]['status'] !== 10
            || number_format((float)$s['bills'][0]['amount'], 2, '.', '') !== '1.00'
            || (int)$s['details'][0]['original_count'] !== 1
            || number_format((float)$s['details'][0]['original_amount'], 2, '.', '') !== '1.00'
            || $sourceCounts() !== ['meal_orders' => 1, 'bills' => 1,
                'bill_order_snapshots' => 1, 'bill_no_prefix' => 1]) {
            throw new RuntimeException('generate HTTP/source mismatch');
        }
        $billId = (int)$s['bills'][0]['id'];
        $outcome['routes']['generate'] = $tree($generated);

        $stage = 'http-adjustamount';
        $meals = [
            ['meal_type' => 1, 'count' => 1, 'amount' => '0.90'],
            ['meal_type' => 2, 'count' => 0, 'amount' => '0.00'],
            ['meal_type' => 3, 'count' => 0, 'amount' => '0.00'],
        ];
        $adjusted = $post('/p/billingrecord/adjustamount',
            ['bill_id' => $billId,
                'meals' => json_encode($meals, JSON_UNESCAPED_UNICODE),
                'reason' => $marker], $token);
        $s = $capture($stage);
        $assertOwned($s);
        if ((int)$adjusted['code'] !== 0
            || (int)($adjusted['data']['id'] ?? 0) !== $billId
            || count($s['details']) !== 3 || count($s['logs']) !== 2
            || number_format((float)$s['bills'][0]['amount'], 2, '.', '') !== '0.90'
            || number_format((float)$s['details'][0]['adjusted_amount'], 2, '.', '') !== '0.90'
            || number_format((float)$s['details'][1]['adjusted_amount'], 2, '.', '') !== '0.00'
            || number_format((float)$s['details'][2]['adjusted_amount'], 2, '.', '') !== '0.00'
            || count(array_filter($s['logs'], static function (array $r): bool {
                return $r['action'] === 'adjust_amount';
            })) !== 1
            || (int)$s['bills'][0]['status'] !== 10) {
            throw new RuntimeException('adjust HTTP/source mismatch');
        }
        $outcome['routes']['adjustamount'] = $tree($adjusted);

        $stage = 'http-issuepayment';
        $issued = $post('/p/billingrecord/issuepayment',
            ['bill_ids' => json_encode([$billId]), 'expire_date' => '2099-03-10'], $token);
        $s = $capture($stage);
        $assertOwned($s);
        if ((int)$issued['code'] !== 0
            || (int)($issued['data']['success'] ?? -1) !== 1
            || !is_array($issued['data']['failed'] ?? null)
            || count($issued['data']['failed']) !== 0
            || count($s['logs']) !== 3
            || count(array_filter($s['logs'], static function (array $r): bool {
                return $r['action'] === 'issue_payment';
            })) !== 1
            || (int)$s['bills'][0]['status'] !== 20
            || !preg_match('/^2099-03-10 23:59:59(?:\.0{1,6})?$/',
                (string)$s['bills'][0]['expire_time'])
            || number_format((float)$s['bills'][0]['amount'], 2, '.', '') !== '0.90') {
            throw new RuntimeException('issue HTTP/source mismatch');
        }
        $outcome['routes']['issuepayment'] = $tree($issued);
        $fixtureAfter = [
            'user' => $read('SELECT * FROM ' . $p . 'user WHERE ID=?', [$actorId], 2),
            'staff' => $read('SELECT * FROM ' . $p . 'staff WHERE UUID=?', [$marker], 2),
            'order' => $read('SELECT * FROM ' . $p . 'meal_order WHERE ORDER_NO=?',
                ['M' . $marker], 2),
        ];
        $outcome['fixture_source_unchanged'] = $fixtureSha === hash('sha256',
            json_encode($fixtureAfter, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE));
        if (!$outcome['fixture_source_unchanged']) {
            throw new RuntimeException('fixture source changed');
        }
        $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 = $read('SELECT UUID,NAME,STATUS,IS_SHOW FROM '
            . $p . 'department WHERE UUID=?', [$department['uuid']], 2);
        $outcome['department_unchanged'] = $departmentSha === hash('sha256',
            json_encode($departmentAfter,
                JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE));
    } catch (Throwable $departmentError) {
        $outcome['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['department_unchanged']
        && $outcome['fixture_source_unchanged'] ? 0 : 1);
} catch (Throwable $error) {
    echo json_encode(['lane' => $lane, 'mode' => $mode,
        'status' => 'blocked', 'stage' => $stage,
        'class' => get_class($error), 'code' => (string)$error->getCode(),
        'message_sha12' => substr(hash('sha256', $error->getMessage()), 0, 12)]), PHP_EOL;
    exit(2);
}
