<?php
declare(strict_types=1);

// Two bounded current-PC Store Bill reads. Never downloads image URLs.
$lane = (string)($argv[1] ?? '');
$mode = (string)($argv[2] ?? '');
if (!in_array($lane, ['B', 'C'], true)
    || !in_array($mode, ['--preflight', '--execute', '--audit', '--type1'], true)
    || (in_array($mode, ['--execute', '--type1'], true)
        && ((string)getenv('CDX_BILL2_APPROVAL') !== 'ROOT_APPROVED_BILL2_BC_V1'
        || (string)getenv('CDX_BILL2_SHA') !== hash_file('sha256', __FILE__)))) {
    fwrite(STDERR, "BILL2 default deny\n");
    exit(78);
}
$target = $lane === 'B'
    ? ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 10001,
        '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401']
    : ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', 112175,
        'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194'];
[$root, $base, $pid, $expectedIdentitySha] = $target;
$stage = 'boundary';
try {
    $cmdline = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
    $children = trim((string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children'));
    $boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
    if (strpos($cmdline, $root . '/public') === false
        || strpos($cmdline, (string)parse_url($base, PHP_URL_PORT)) === false
        || $children !== '' || !is_array($boundary)
        || empty($boundary['external_stubs']) || empty($boundary['redis_isolated'])
        || !empty($boundary['real_queue'])) throw new RuntimeException('worker boundary');
    $expectedSource = [
        'application/p/controller/Bill.php' => '8d99ea508e165d694a156d43420cb98f3a809a0fb9a120716e4f9dd90a4b9cf2',
        'application/p/logic/Bill.php' => '699e733b42157762d6af98e07c6366c3dc553bb2ff2d7676c0f758b7ff065daa',
        'application/p/logic/RecipeBatch.php' => '0313e936e55b97d13e97a2437ba755409b4341cc670ac83bf9282cb5658f6db4',
        'application/common.php' => '1ff0e49451950d671498ad5e1f7380f60448d2f7a39041d4b53370abd6e6d78f',
        'extend/oss/OSSServer.php' => '4ee3819dd1cd937a60c0cf776d88ccd2a53bdb8fce551e44d4b939c787a540b8',
    ];
    foreach ($expectedSource as $relative => $hash) {
        $file = $root . '/' . $relative;
        if (!is_file($file) || is_link($file) || !hash_equals($hash, hash_file('sha256', $file))) {
            throw new RuntimeException('business source drift');
        }
    }
    $stage = 'database-config';
    $config = require $root . '/application/database.php';
    if (!is_array($config) || ($config['prefix'] ?? '') !== 'ydy_') {
        throw new RuntimeException('database config');
    }
    $dialect = $lane === 'C' ? 'dm' : 'mysql';
    $dsn = $dialect === 'dm' ? 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP'
        : 'mysql:host=' . $config['hostname'] . ';port=' . ($config['hostport'] ?? 3306)
            . ';dbname=' . $config['database'] . ';charset=utf8mb4';
    $db = new PDO($dsn, (string)$config['username'], (string)$config['password'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
    $identity = $dialect === 'dm'
        ? 'ZHCT_APP|5237|' . (string)$db->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn()
        : (string)$db->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")->fetchColumn();
    if (!hash_equals($expectedIdentitySha, hash('sha256', $identity))) {
        throw new RuntimeException('database identity');
    }
    $prefix = $dialect === 'dm' ? 'YDY_' : 'ydy_';
    $userTable = $prefix . 'user';
    $rows = static function (string $sql, array $args = [], int $cap = 2000) use ($db): array {
        if (!preg_match('/^SELECT\b/i', $sql)) throw new RuntimeException('not SELECT');
        $stmt = $db->prepare($sql);
        $stmt->execute($args);
        $out = [];
        while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
            if (count($out) >= $cap) throw new RuntimeException('source row cap');
            $normalized = [];
            foreach (array_change_key_case($row, CASE_LOWER) as $k => $value) {
                $normalized[$k] = is_resource($value) ? stream_get_contents($value) : $value;
            }
            ksort($normalized);
            $out[] = $normalized;
        }
        return $out;
    };
    $count = static function (string $sql, array $args = []) use ($rows): int {
        $result = $rows($sql, $args, 1);
        return (int)(reset($result[0]) ?: 0);
    };
    if ($mode === '--audit') {
        $marker = (string)getenv('CDX_BILL2_MARKER');
        if (!preg_match('/^CDXBILL2[A-F0-9]{12}$/', $marker)) exit(78);
        $n = $count('SELECT COUNT(*) FROM ' . $userTable
            . ' WHERE UUID=? OR ACCOUNT=? OR NAME=?', [$marker, $marker, $marker]);
        echo json_encode(['lane' => $lane, 'audit' => 'actor', 'marker' => $marker,
            'count' => $n, 'zero' => $n === 0]), PHP_EOL;
        exit($n === 0 ? 0 : 3);
    }
    $stage = 'natural-source';
    $groups = $rows('SELECT I.DISHES_UUID AS U,MAX(I.ID) AS LAST_ID,COUNT(*) AS N'
        . ' FROM ' . $prefix . 'dishes_recognize_img I JOIN ' . $prefix . 'dishes D'
        . ' ON D.UUID=I.DISHES_UUID WHERE I.IS_DELETE=0 AND D.IS_DEL=1'
        . ' GROUP BY I.DISHES_UUID ORDER BY LAST_ID DESC');
    if (!$groups || count($groups) > 1000) throw new RuntimeException('image source absent');
    $dishUuid = (string)$groups[0]['u'];
    $batches = $rows('SELECT B.ID,B.BATCH_NO FROM ' . $prefix . 'recipe_batch B'
        . ' JOIN ' . $prefix . 'recipe_batch_detail D ON D.BATCH_ID=B.ID'
        . ' WHERE B.STORE_ID=10001 AND B.IS_DELETE=0'
        . ($mode === '--type1' ? ' AND B.BATCH_TYPE=1' : '')
        . ' AND D.STORE_ID=10001 AND D.IS_DELETE=0 GROUP BY B.ID,B.BATCH_NO ORDER BY B.ID DESC');
    if (!$batches || count($batches) > 1000) throw new RuntimeException('batch source absent');
    $batchId = (int)$batches[0]['id'];
    $batchNo = (string)$batches[0]['batch_no'];
    $snapshot = static function () use ($rows, $prefix, $dishUuid, $batchId): array {
        $queries = [
            'images' => ['SELECT ID,DISHES_UUID,IMG_URL,FEATURE,EQUIPMENT_CODE,CREATE_TIME'
                . ' FROM ' . $prefix . 'dishes_recognize_img WHERE DISHES_UUID=? AND IS_DELETE=0 ORDER BY ID DESC', [$dishUuid]],
            'dish' => ['SELECT UUID,NAME,PRICE,IS_DEL,STATUS FROM ' . $prefix
                . 'dishes WHERE UUID=?', [$dishUuid]],
            'batch' => ['SELECT * FROM ' . $prefix . 'recipe_batch WHERE ID=?', [$batchId]],
            'details' => ['SELECT * FROM ' . $prefix . 'recipe_batch_detail'
                . ' WHERE BATCH_ID=? AND STORE_ID=10001 AND IS_DELETE=0 ORDER BY ID ASC', [$batchId]],
        ];
        $out = [];
        foreach ($queries as $key => [$sql, $args]) {
            $source = $rows($sql, $args);
            $encoded = json_encode($source, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
            if (!is_string($encoded)) throw new RuntimeException('source encode');
            $out[$key] = ['count' => count($source), 'sha256' => hash('sha256', $encoded),
                'rows' => $source];
        }
        return $out;
    };
    $digest = static function (array $source): array {
        $out = [];
        foreach ($source as $key => $item) $out[$key] = [$item['count'], $item['sha256']];
        return $out;
    };
    $sourceBefore = $snapshot();
    if ($sourceBefore['images']['count'] < 1 || $sourceBefore['images']['count'] > 50
        || $sourceBefore['dish']['count'] !== 1 || $sourceBefore['batch']['count'] !== 1
        || $sourceBefore['details']['count'] < 1 || $sourceBefore['details']['count'] > 1000) {
        throw new RuntimeException('bounded source absent');
    }
    $original = $digest($sourceBefore);
    $sourceCounts = array_map(static function ($item) { return $item['count']; }, $sourceBefore);
    $preflight = ['lane' => $lane, 'status' => 'preflight-pass',
        'image_groups' => count($groups), 'batch_candidates' => count($batches),
        'source_counts' => $sourceCounts,
        'dish_sha12' => substr(hash('sha256', $dishUuid), 0, 12),
        'batch_sha12' => substr(hash('sha256', $batchNo), 0, 12)];
    if ($mode === '--preflight') {
        echo json_encode($preflight), PHP_EOL;
        exit(0);
    }
    echo json_encode($preflight), PHP_EOL;
    $marker = 'CDXBILL2' . strtoupper(bin2hex(random_bytes(6)));
    if ($count('SELECT COUNT(*) FROM ' . $userTable
        . ' WHERE UUID=? OR ACCOUNT=? OR NAME=?', [$marker, $marker, $marker]) !== 0) {
        throw new RuntimeException('actor collision');
    }
    $shape = static function ($value): array {
        if (!is_array($value)) return ['_type' => gettype($value)];
        $out = [];
        foreach ($value as $key => $item) $out[(string)$key] = gettype($item);
        ksort($out);
        return $out;
    };
    $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'));
        $b = 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($b) && !empty($b['external_stubs'])
            && !empty($b['redis_isolated']) && empty($b['real_queue']);
    };
    $post = static function (string $path, array $form, string $token)
        use ($base, $runtimeReady): array {
        if (!in_array($path, ['/p/bill/dishesRecognizeImgList', '/p/bill/recipeBatchDetail'], 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 outcome incomplete');
        }
        $json = json_decode($body, true);
        if (!is_array($json) || ($json['code'] ?? null) !== 0 || !isset($json['data'])) {
            throw new RuntimeException('HTTP business response');
        }
        return $json;
    };
    $actorId = 0;
    $failure = '';
    $result = ['lane' => $lane, 'marker' => $marker, 'routes' => [],
        'source_counts' => $sourceCounts, 'source_unchanged' => false, 'actor_zero' => false];
    try {
        if (!$runtimeReady()) throw new RuntimeException('worker drift');
        $insert = $db->prepare('INSERT INTO ' . $userTable
            . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
            . ' VALUES (?,?,?,?,?,?,?,?,?)');
        $insert->execute([$marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
            $marker, 0, date('Y-m-d H:i:s'), 'CODEX', 0]);
        if ($insert->rowCount() !== 1) throw new RuntimeException('actor insert');
        $actorRows = $rows('SELECT ID FROM ' . $userTable
            . ' WHERE UUID=? AND ACCOUNT=? AND NAME=? AND TYPE=0 AND CREATE_BY=?',
            [$marker, $marker, $marker, 'CODEX'], 2);
        if (count($actorRows) !== 1) throw new RuntimeException('actor identity');
        $actorId = (int)$actorRows[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('actor token');

        if ($mode !== '--type1') {
            $stage = 'image-http';
            $image = $post('/p/bill/dishesRecognizeImgList',
                ['page' => 1, 'page_size' => 1, 'keyword' => ''], $token);
            $payload = $image['data'];
            $imageRows = $payload['data'] ?? null;
            if (!is_array($payload) || !is_array($imageRows) || count($imageRows) !== 1
                || (int)($payload['total'] ?? 0) !== count($groups)
                || (int)($payload['page'] ?? 0) !== 1
                || (int)($payload['page_size'] ?? 0) !== 1) {
                throw new RuntimeException('image list pagination');
            }
            $dish = $imageRows[0];
            if ((string)($dish['dishes_uuid'] ?? '') !== $dishUuid
                || (string)($dish['dishes_name'] ?? '') !== (string)$sourceBefore['dish']['rows'][0]['name']
                || (int)($dish['image_count'] ?? 0) !== $sourceBefore['images']['count']
                || !is_array($dish['images'] ?? null)) {
                throw new RuntimeException('image source group');
            }
            $sourceImageIds = array_fill_keys(array_map(static function ($r) { return (int)$r['id']; },
                $sourceBefore['images']['rows']), true);
            if (count($dish['images']) !== count($sourceImageIds)) {
                throw new RuntimeException('image source count');
            }
            foreach ($dish['images'] as $item) {
                if (!isset($sourceImageIds[(int)($item['id'] ?? 0)])) {
                    throw new RuntimeException('image source ID');
                }
            }
            $result['routes']['dishesRecognizeImgList'] = [
                'group_total' => (int)$payload['total'], 'images_in_first_group' => count($dish['images']),
                'source_match' => true, 'envelope' => $shape($image),
                'data' => $shape($payload), 'dish' => $shape($dish),
                'image' => $shape($dish['images'][0])];
        }

        $stage = 'batch-http';
        $detail = $post('/p/bill/recipeBatchDetail',
            ['batch_no' => $batchNo, 'page' => 1, 'page_size' => 10], $token);
        $data = $detail['data'];
        $expectedDetails = array_slice($sourceBefore['details']['rows'], 0, 10);
        if (!is_array($data) || !is_array($data['batch'] ?? null)
            || !is_array($data['details'] ?? null) || !$data['details']
            || (string)($data['batch']['batch_no'] ?? '') !== $batchNo
            || (int)($data['batch']['id'] ?? 0) !== $batchId
            || (int)($data['pagination']['total'] ?? -1) !== $sourceBefore['details']['count']
            || count($data['details']) !== count($expectedDetails)) {
            throw new RuntimeException('batch detail pagination');
        }
        foreach ($data['details'] as $index => $item) {
            if ((int)($item['id'] ?? 0) !== (int)$expectedDetails[$index]['id']
                || (int)($item['batch_id'] ?? 0) !== $batchId) {
                throw new RuntimeException('batch source ID');
            }
        }
        $result['routes']['recipeBatchDetail'] = [
            'total' => $sourceBefore['details']['count'],
            'page_rows' => count($data['details']), 'source_match' => true,
            'envelope' => $shape($detail), 'data' => $shape($data),
            'batch' => $shape($data['batch']),
            'detail' => $shape($data['details'][0]),
            'pagination' => $shape($data['pagination'])];
        $stage = 'source-after';
        $result['source_unchanged'] = $digest($snapshot()) === $original;
        if (!$result['source_unchanged']) throw new RuntimeException('source changed');
    } catch (Throwable $error) {
        $failure = $stage . ':' . get_class($error) . ':' . (string)$error->getCode()
            . ':' . substr(hash('sha256', $error->getMessage()), 0, 12);
    } finally {
        if ($actorId > 0) {
            $actor = $rows('SELECT ID FROM ' . $userTable
                . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND NAME=? AND TYPE=0 AND CREATE_BY=?',
                [$actorId, $marker, $marker, $marker, 'CODEX'], 2);
            if (count($actor) !== 1) throw new RuntimeException('actor cleanup identity');
            $delete = $db->prepare('DELETE FROM ' . $userTable
                . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND NAME=? AND TYPE=0 AND CREATE_BY=?');
            $delete->execute([$actorId, $marker, $marker, $marker, 'CODEX']);
            if ($delete->rowCount() !== 1) throw new RuntimeException('actor cleanup affected');
        }
        $result['actor_zero'] = $count('SELECT COUNT(*) FROM ' . $userTable
            . ' WHERE UUID=? OR ACCOUNT=? OR NAME=?', [$marker, $marker, $marker]) === 0;
        if (!$result['source_unchanged']) {
            try { $result['source_unchanged'] = $digest($snapshot()) === $original; }
            catch (Throwable $sourceError) {
                $failure .= '|source-after:' . get_class($sourceError) . ':'
                    . substr(hash('sha256', $sourceError->getMessage()), 0, 12);
            }
        }
    }
    $result['failure'] = $failure;
    echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
    exit($failure === '' && $result['source_unchanged'] && $result['actor_zero'] ? 0 : 1);
} catch (Throwable $error) {
    echo json_encode(['lane' => $lane, 'status' => 'blocked-before-http',
        'stage' => $stage, 'class' => get_class($error),
        'code' => (string)$error->getCode(),
        'message_sha12' => substr(hash('sha256', $error->getMessage()), 0, 12)]), PHP_EOL;
    exit(2);
}
