<?php

declare(strict_types=1);

// REVIEW BUILD: no DML or HTTP can run while this compile-time gate is false.
// Changing it requires a new SHA, a fresh source review, and root approval.
const OM_DML_REVIEWED = false;
$mode = (string)($argv[1] ?? '');
if (!in_array($mode, ['--preflight', '--execute'], true)
    || ($mode === '--execute' && !OM_DML_REVIEWED)) {
    fwrite(STDERR, "Order-list fixture DML/HTTP disabled pending review\n");
    exit(78);
}
if ($mode === '--execute'
    && ((string)getenv('CDX_OM_APPROVAL') !== 'ROOT_APPROVED_EXACT_SHA_ORDERLIST_ONLY'
        || (string)getenv('CDX_OM_SHA') !== hash_file('sha256', __FILE__))) {
    fwrite(STDERR, "Order-list fixture explicit approval/SHA mismatch\n");
    exit(78);
}
set_exception_handler(static function (Throwable $error): void {
    fwrite(STDERR, 'Order-list fixture stopped: ' . get_class($error) . "\n");
    exit(1);
});
require __DIR__ . '/fixture_guard.php';
if (!function_exists('proc_open')
    || !is_file(__DIR__ . '/independent_cleanup_verify.php')) {
    throw new RuntimeException('Independent cleanup verification unavailable');
}
date_default_timezone_set('PRC');
[$aRoot, $aBase] = omRuntimeCheck('A');
[$bRoot, $bBase] = omRuntimeCheck('B');
$store = omConnectStore();
$ai = omConnectAi();
$schemaSha = omSchemaFingerprint($store);
$source = omSource($store, $ai);
$financialSha = hash('sha256', serialize([
    $source['user']['balance'], $source['user']['pay_money'],
    $source['ai_user'], $source['ledgers'],
]));
$markerCount = static function (PDO $db): array {
    $orderCount = (int)$db->query(
        "SELECT COUNT(*) FROM ydy_meal_order WHERE order_no LIKE 'CDXPAROM%'"
    )->fetchColumn();
    $pickupCount = (int)$db->query(
        "SELECT COUNT(*) FROM ydy_meal_order_pickup WHERE order_no LIKE 'CDXPAROM%'"
    )->fetchColumn();
    return [$orderCount, $pickupCount];
};
if ($markerCount($store) !== [0, 0]) {
    throw new RuntimeException('Another or orphan order-machine fixture exists');
}
$query = $store->prepare('SELECT COUNT(*) FROM ydy_meal_order_pickup WHERE restaurant_id=?'
    . ' AND meal_date=?');
$query->execute([OM_RESTAURANT_ID, date('Y-m-d')]);
$todayPickupCount = (int)$query->fetchColumn();
if ($todayPickupCount !== 0) {
    throw new RuntimeException('Candidate stall pickup baseline changed');
}
$runtimeInfo = [
    'mode' => $mode === '--preflight' ? 'read_only_preflight' : 'gated_execute',
    'store_schema_sha' => $schemaSha,
    'runtime_lanes' => ['A', 'B'],
    'synthetic_user_id' => (int)$source['user']['id'],
    'ai_user_id' => (int)$source['user']['ai_user_id'],
    'synthetic_account_sha' => hash('sha256', (string)$source['user']['account']),
    'device_free_type2_restaurant_id' => (int)$source['stall']['id'],
    'fixture_order_rows' => 0, 'fixture_pickup_rows' => 0,
    'today_candidate_pickup_rows' => 0,
    'financial_baseline_sha' => $financialSha,
    'business_dml' => false, 'business_http' => false,
];
if ($mode === '--preflight') {
    echo json_encode($runtimeInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
if ((int)date('H') >= 22) {
    throw new RuntimeException('Too close to midnight for a shared DB fixture');
}

// From here downward is deliberately unreachable in this review build.
$safeBoundary = static function (string $base): void {
    $ctx = stream_context_create(['http' => ['timeout' => 3,
        'ignore_errors' => true]]);
    $body = @file_get_contents($base . '/__poc_boundary', false, $ctx, 0, 4097);
    $decoded = is_string($body) ? json_decode($body, true) : null;
    if (!is_array($decoded) || empty($decoded['external_stubs'])
        || empty($decoded['redis_isolated']) || !empty($decoded['real_queue'])) {
        throw new RuntimeException('Isolated external/queue boundary not confirmed');
    }
};
$safeBoundary($aBase);
$safeBoundary($bBase);
$nonce = strtoupper(bin2hex(random_bytes(4)));
$orderNo = 'CDXPAROM' . $nonce . 'A';
if (!preg_match('/^CDXPAROM[A-F0-9]{8}A$/D', $orderNo)) {
    throw new RuntimeException('Generated marker invalid');
}
$pickupNo = '9' . str_pad((string)random_int(0, 9999999), 7, '0', STR_PAD_LEFT);
$now = date('Y-m-d H:i:s');
$today = substr($now, 0, 10);
$clock = substr($now, 11, 8);
$mealTimes = 4;
foreach ([1 => ['morning_start', 'morning_end'],
    2 => ['noon_start', 'noon_end'], 3 => ['night_start', 'night_end']] as $meal => $fields) {
    if ($clock >= (string)$source['stall'][$fields[0]] . ':00'
        && $clock <= (string)$source['stall'][$fields[1]] . ':59') {
        $mealTimes = $meal;
        break;
    }
}
$collision = $store->prepare('SELECT COUNT(*) FROM ydy_meal_order_pickup'
    . ' WHERE restaurant_id=? AND meal_date=? AND meal_times=? AND pickup_no=?');
$collision->execute([OM_RESTAURANT_ID, $today, $mealTimes, $pickupNo]);
if ((int)$collision->fetchColumn() !== 0) {
    throw new RuntimeException('Generated pickup number collision');
}
$journalDir = '/tmp/zhct-orderlist-ab-' . bin2hex(random_bytes(6));
if (!@mkdir($journalDir, 0700)) throw new RuntimeException('Private journal dir unavailable');
@chmod($journalDir, 0700);
$journalPath = $journalDir . '/journal.json';
$writeJournal = static function (array $payload) use ($journalPath): void {
    $tmp = $journalPath . '.tmp';
    $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    if (!is_string($json) || file_put_contents($tmp, $json, LOCK_EX) === false) {
        throw new RuntimeException('Private recovery journal write failed');
    }
    @chmod($tmp, 0600);
    if (!rename($tmp, $journalPath)) {
        throw new RuntimeException('Private recovery journal replace failed');
    }
};
$journal = [
    'phase' => 'prepared', 'order_no' => $orderNo,
    'user_id' => OM_USER_ID, 'ai_user_id' => (int)$source['user']['ai_user_id'],
    'restaurant_id' => OM_RESTAURANT_ID, 'pickup_no' => $pickupNo,
    'meal_date' => $today, 'meal_times' => $mealTimes,
    'created_at' => $now, 'order_id' => 0, 'pickup_id' => 0,
];
$writeJournal($journal); // Persist marker before the first insert/commit.
$orderId = 0;
$pickupId = 0;
$committed = false;
$cleaned = false;
$lockHeld = false;
$results = [];
try {
    if ((int)$store->query("SELECT GET_LOCK('CDXPAROM_ORDERLIST_AB',0)")->fetchColumn() !== 1) {
        throw new RuntimeException('A/B fixture exclusive DB lock unavailable');
    }
    $lockHeld = true;
    if ($markerCount($store) !== [0, 0]) {
        throw new RuntimeException('Fixture marker race before insert');
    }
    omRuntimeCheck('A');
    omRuntimeCheck('B');
    omSchemaFingerprint($store);
    $sourceRecheck = omSource($store, $ai);
    if ((string)$sourceRecheck['user']['account'] !== (string)$source['user']['account']
        || serialize($sourceRecheck['stall']) !== serialize($source['stall'])
        || !hash_equals($financialSha, hash('sha256', serialize([
        $sourceRecheck['user']['balance'], $sourceRecheck['user']['pay_money'],
        $sourceRecheck['ai_user'], $sourceRecheck['ledgers'],
    ])))) {
        throw new RuntimeException('Synthetic account changed before insert');
    }
    $query->execute([OM_RESTAURANT_ID, $today]);
    if ((int)$query->fetchColumn() !== 0) {
        throw new RuntimeException('Candidate pickup scope changed before insert');
    }
    $collision->execute([OM_RESTAURANT_ID, $today, $mealTimes, $pickupNo]);
    if ((int)$collision->fetchColumn() !== 0) {
        throw new RuntimeException('Pickup number collision before insert');
    }
    $store->beginTransaction();
    $orderFields = [
        'order_no' => $orderNo,
        'user_id' => OM_USER_ID,
        'restaurant_id' => OM_RESTAURANT_ID,
        'user_name' => (string)$source['user']['account'],
        'user_mobile' => '',
        'total_price' => '0.00', 'ori_total_price' => '0.00',
        'order_price' => '0.00', 'pay_price' => '0.00',
        'cash' => '0.00', 'subsidy' => '0.00',
        'pay_status' => 20, 'pay_time' => $now,
        'meal_date' => $today, 'meal_times' => $mealTimes,
        'meal_number' => 0, 'bind_time' => $now,
        'source' => 3, 'settled_status' => 0,
        'restaurant_method' => 2, 'order_status' => 30,
        'refund_status' => 0, 'print_status' => 0,
        'is_delete' => 0, 'msgid' => '',
        'create_time' => $now, 'update_time' => $now,
    ];
    $insertStatic = static function (PDO $db, string $table, array $fields): int {
        $columns = array_keys($fields); // Columns are literal constants above, never input.
        $sql = 'INSERT INTO ' . $table . ' (' . implode(',', $columns) . ') VALUES ('
            . implode(',', array_fill(0, count($columns), '?')) . ')';
        $stmt = $db->prepare($sql);
        $stmt->execute(array_values($fields));
        if ($stmt->rowCount() !== 1) throw new RuntimeException('One fixture insert expected');
        return (int)$db->lastInsertId();
    };
    $orderId = $insertStatic($store, 'ydy_meal_order', $orderFields);
    if ($orderId <= 0) throw new RuntimeException('Fixture order ID absent');
    $pickupFields = [
        'restaurant_id' => OM_RESTAURANT_ID,
        'meal_date' => $today, 'meal_times' => $mealTimes,
        'meal_order_id' => $orderId, 'order_no' => $orderNo,
        'pickup_no' => $pickupNo, 'prepare_status' => 30,
        'prepare_time' => $now, 'pickup_ready_time' => $now,
        'serve_equipment_code' => '', 'last_call_time' => $now,
        'call_count' => 0,
    ];
    $pickupId = $insertStatic($store, 'ydy_meal_order_pickup', $pickupFields);
    if ($pickupId <= 0) throw new RuntimeException('Fixture pickup ID absent');
    [$orders, $pickups] = omMarkerRows($store, $orderNo);
    if (count($orders) !== 1 || count($pickups) !== 1
        || (int)$orders[0]['id'] !== $orderId
        || (int)$orders[0]['user_id'] !== OM_USER_ID
        || (int)$orders[0]['restaurant_id'] !== OM_RESTAURANT_ID
        || (int)$orders[0]['pay_status'] !== 20
        || (int)$orders[0]['print_status'] !== 0
        || (float)$orders[0]['pay_price'] !== 0.0
        || (int)$pickups[0]['id'] !== $pickupId
        || (int)$pickups[0]['meal_order_id'] !== $orderId
        || (int)$pickups[0]['prepare_status'] !== 30
        || (string)$pickups[0]['pickup_no'] !== $pickupNo) {
        throw new RuntimeException('Fixture transaction self-read mismatch');
    }
    $journal['order_id'] = $orderId;
    $journal['pickup_id'] = $pickupId;
    $journal['phase'] = 'ready_to_commit';
    $writeJournal($journal); // Exact IDs durable before COMMIT.
    $store->commit();
    $committed = true;
    $journal['phase'] = 'committed';
    $writeJournal($journal);
    $visible = omConnectStore();
    [$visibleOrders, $visiblePickups] = omMarkerRows($visible, $orderNo);
    if (count($visibleOrders) !== 1 || count($visiblePickups) !== 1
        || (int)$visibleOrders[0]['id'] !== $orderId
        || (int)$visibleOrders[0]['print_status'] !== 0
        || (int)$visibleOrders[0]['pay_status'] !== 20
        || (int)$visiblePickups[0]['id'] !== $pickupId
        || (int)$visiblePickups[0]['meal_order_id'] !== $orderId
        || (int)$visiblePickups[0]['prepare_status'] !== 30) {
        throw new RuntimeException('Independent post-commit order/pickup readback failed');
    }
    $httpOrder = static function (string $lane, string $base) use (
        $orderNo, $orderId, $pickupNo, $source, $now): array {
        omRuntimeCheck($lane);
        $form = http_build_query(['keyword' => $orderNo], '', '&', PHP_QUERY_RFC3986);
        $ctx = stream_context_create(['http' => ['method' => 'POST',
            'header' => "Content-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n",
            'content' => $form, 'timeout' => 20, 'ignore_errors' => true]]);
        $stream = @fopen($base . '/api/orderMachine/orderList', 'rb', false, $ctx);
        if (!$stream) throw new RuntimeException('Order-list HTTP open failed');
        $headers = $http_response_header ?? [];
        $body = stream_get_contents($stream, 262145);
        fclose($stream);
        if (!is_string($body) || strlen($body) > 262144
            || strpos((string)($headers[0] ?? ''), ' 200 ') === false) {
            throw new RuntimeException('Order-list HTTP status/size mismatch');
        }
        $json = json_decode($body, true);
        $rows = is_array($json) ? ($json['data'] ?? null) : null;
        if (!is_array($json) || array_keys($json) !== ['code', 'message', 'data']
            || ($json['code'] ?? null) !== 0
            || ($json['message'] ?? null) !== '获取成功'
            || !is_array($rows) || count($rows) !== 1) {
            throw new RuntimeException('Order-list success envelope/count mismatch');
        }
        $row = $rows[0];
        if (!is_array($row)
            || array_keys($row) !== ['id', 'pickup_no', 'restaurant_name',
                'order_amount', 'pay_time', 'order_no', 'user_mobile',
                'user_name', 'order_status', 'order_status_value']
            || !is_int($row['id']) || $row['id'] !== $orderId
            || $row['pickup_no'] !== $pickupNo
            || $row['restaurant_name'] !== (string)$source['stall']['name']
            || $row['order_amount'] !== '0.00'
            || !is_string($row['pay_time'])
            || $row['pay_time'] !== $now
            || $row['order_no'] !== $orderNo
            || $row['user_mobile'] !== (string)$source['user']['mobile']
            || $row['user_name'] !== (string)$source['user']['name']
            || $row['order_status'] !== '已完成'
            || !is_int($row['order_status_value'])
            || $row['order_status_value'] !== 30) {
            throw new RuntimeException('Order-list per-field source/JSON mismatch');
        }
        return ['lane' => $lane, 'response_sha' => hash('sha256', $body),
            'response_bytes' => strlen($body), 'count' => count($rows)];
    };
    $results[] = $httpOrder('A', $aBase);
    $results[] = $httpOrder('B', $bBase);
    if (!hash_equals($results[0]['response_sha'], $results[1]['response_sha'])) {
        throw new RuntimeException('A/B order-list response differs for shared fixture');
    }
} finally {
    if ($store->inTransaction()) $store->rollBack();
    if (!$committed && $orderNo !== '') {
        // Resolve ambiguous COMMIT outcomes through a fresh approved connection.
        try {
            $rescue = omConnectStore();
            [$rescueOrders, $rescuePickups] = omMarkerRows($rescue, $orderNo);
            if (count($rescueOrders) === 1 && count($rescuePickups) === 1) {
                $orderId = (int)$rescueOrders[0]['id'];
                $pickupId = (int)$rescuePickups[0]['id'];
                $committed = true;
            } elseif (count($rescueOrders) === 0 && count($rescuePickups) === 0) {
                @unlink($journalPath);
                @rmdir($journalDir);
            }
        } catch (Throwable $ignored) {
            // The private marker journal remains for independent recovery audit.
        }
    }
    if ($committed) {
        $cleanup = omConnectStore();
        omSchemaFingerprint($cleanup);
        $aiAfter = omConnectAi();
        $afterSource = omSource($cleanup, $aiAfter);
        $afterFinanceSha = hash('sha256', serialize([
            $afterSource['user']['balance'], $afterSource['user']['pay_money'],
            $afterSource['ai_user'], $afterSource['ledgers'],
        ]));
        if (!hash_equals($financialSha, $afterFinanceSha)) {
            throw new RuntimeException('Synthetic user/AI financial source changed; manual review');
        }
        [$orders, $pickups] = omMarkerRows($cleanup, $orderNo);
        if (count($orders) !== 1 || count($pickups) !== 1
            || (int)$orders[0]['id'] !== $orderId
            || (int)$orders[0]['user_id'] !== OM_USER_ID
            || (int)$orders[0]['restaurant_id'] !== OM_RESTAURANT_ID
            || (int)$orders[0]['pay_status'] !== 20
            || (int)$orders[0]['print_status'] !== 0
            || (int)$orders[0]['order_status'] !== 30
            || (int)$orders[0]['source'] !== 3
            || (float)$orders[0]['pay_price'] !== 0.0
            || (int)$pickups[0]['id'] !== $pickupId
            || (int)$pickups[0]['meal_order_id'] !== $orderId
            || (int)$pickups[0]['prepare_status'] !== 30
            || (string)$pickups[0]['pickup_no'] !== $pickupNo
            || (string)$pickups[0]['meal_date'] !== $today
            || (int)$pickups[0]['meal_times'] !== $mealTimes) {
            throw new RuntimeException('Fixture ownership changed; do not delete');
        }
        foreach (['ydy_meal_order_refund' => 'meal_order_id',
            'ydy_initial_menu' => 'meal_order_id'] as $table => $field) {
            $check = $cleanup->prepare('SELECT COUNT(*) FROM ' . $table . ' WHERE ' . $field . '=?');
            $check->execute([$orderId]);
            if ((int)$check->fetchColumn() !== 0) {
                throw new RuntimeException('Unexpected linked business row; do not delete');
            }
        }
        $check = $cleanup->prepare('SELECT COUNT(*) FROM ydy_wanyisheng_sync_task'
            . " WHERE biz_type='order' AND biz_key=?");
        $check->execute([$orderNo]);
        if ((int)$check->fetchColumn() !== 0) {
            throw new RuntimeException('Unexpected external sync task; do not delete');
        }
        $cleanup->beginTransaction();
        try {
            $delete = $cleanup->prepare('DELETE FROM ydy_meal_order_pickup'
                . ' WHERE id=? AND order_no=? AND meal_order_id=? AND prepare_status=30');
            $delete->execute([$pickupId, $orderNo, $orderId]);
            if ($delete->rowCount() !== 1) {
                throw new RuntimeException('Exact pickup cleanup count mismatch');
            }
            $delete = $cleanup->prepare('DELETE FROM ydy_meal_order'
                . ' WHERE id=? AND order_no=? AND user_id=? AND restaurant_id=?'
                . ' AND pay_status=20 AND print_status=0 AND pay_price=0');
            $delete->execute([$orderId, $orderNo, OM_USER_ID, OM_RESTAURANT_ID]);
            if ($delete->rowCount() !== 1) {
                throw new RuntimeException('Exact order cleanup count mismatch');
            }
            $cleanup->commit();
            $cleaned = true;
        } catch (Throwable $e) {
            if ($cleanup->inTransaction()) $cleanup->rollBack();
            throw $e;
        }
        // A new process and connection, not the cleanup PDO, must verify zero.
        $cmd = escapeshellarg(PHP_BINARY) . ' '
            . escapeshellarg(__DIR__ . '/independent_cleanup_verify.php');
        $pipes = [];
        $process = proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'],
            2 => ['pipe', 'w']], $pipes);
        if (!is_resource($process)) {
            throw new RuntimeException('Independent cleanup verifier did not start');
        }
        fwrite($pipes[0], json_encode(['order_no' => $orderNo,
            'financial_sha' => $financialSha]));
        fclose($pipes[0]);
        $verification = stream_get_contents($pipes[1], 8192);
        fclose($pipes[1]);
        $verifierError = stream_get_contents($pipes[2], 4096);
        fclose($pipes[2]);
        $exitCode = proc_close($process);
        $verified = is_string($verification) ? json_decode($verification, true) : null;
        if ($exitCode !== 0 || !is_array($verified)
            || empty($verified['clean']) || ($verified['orders'] ?? -1) !== 0
            || ($verified['pickups'] ?? -1) !== 0) {
            throw new RuntimeException('Independent cleanup verifier failed; journal retained'
                . ($verifierError === '' ? '' : ' (redacted stderr available locally)'));
        }
        @unlink($journalPath);
        @rmdir($journalDir);
    }
    if ($lockHeld) {
        try { $store->query("SELECT RELEASE_LOCK('CDXPAROM_ORDERLIST_AB')"); }
        catch (Throwable $ignored) { /* Connection may already be gone. */ }
    }
}
echo json_encode(['mode' => 'executed_after_future_approval',
    'business_http' => count($results), 'a_b_response_sha' => $results[0]['response_sha'] ?? null,
    'cleanup' => $cleaned, 'journal_retained' => !$cleaned],
    JSON_UNESCAPED_SLASHES), PHP_EOL;
