<?php
declare(strict_types=1);

// Current Store compatible code, three bounded read routes, B/C only.
$lane = (string)($argv[1] ?? '');
$mode = (string)($argv[2] ?? '');
if (!in_array($lane, ['B', 'C'], true)
    || !in_array($mode, ['--execute', '--audit'], true)
    || ($mode === '--execute' && ((string)getenv('CDX_ONTO3_APPROVAL') !== 'ROOT_APPROVED_ONTO3_BC_V1'
        || (string)getenv('CDX_ONTO3_SCRIPT_SHA') !== hash_file('sha256', __FILE__)))) {
    fwrite(STDERR, "ONTO3 default deny\n");
    exit(78);
}
$stage = 'entry';
$marker = (string)getenv('CDX_ONTO3_MARKER');
if ($mode === '--audit' && !preg_match('/^CDXONTO3[A-F0-9]{12}$/', $marker)) exit(78);
require __DIR__ . '/preflight_onto3.php'; // SELECT only; retains chosen identifiers in memory.
$expectedSource = [
    'application/p/controller/Ontoanalysis.php' => '6bca66c9b34668e730458924c6e62f106a73c51a17ef912d7fa19102e94d5dfd',
    'application/p/logic/Ontoanalysis.php' => 'fa80caf1c0f9975341181ed9dadcca583736125caf147c237ef698684afc60f6',
    'application/p/logic/DietRecord.php' => 'd513be7feb5fdf2f9a815dc5af09e12b1dca7c39182e614b1b18848fc41fe669',
];
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('Ontoanalysis source drift');
    }
}

$read = static function (PDO $db, string $sql, array $args = [], int $cap = 1000): 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('row cap');
        $clean = [];
        foreach (array_change_key_case($row, CASE_LOWER) as $key => $value) {
            $clean[$key] = is_resource($value) ? stream_get_contents($value) : $value;
        }
        ksort($clean);
        $out[] = $clean;
    }
    return $out;
};
$count = static function (PDO $db, string $sql, array $args = []) use ($read): int {
    $rows = $read($db, $sql, $args, 1);
    return (int)(reset($rows[0]) ?: 0);
};
$userTable = $st . 'user';
if ($mode === '--audit') {
    $n = $count($store, 'SELECT COUNT(*) FROM ' . $userTable
        . ' WHERE UUID=? OR ACCOUNT=? OR NAME=?', [$marker, $marker, $marker]);
    echo json_encode(['audit' => 'actor', 'lane' => $lane, 'marker' => $marker,
        'count' => $n, 'zero' => $n === 0]), PHP_EOL;
    exit($n === 0 ? 0 : 3);
}

$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;
};
$snapshot = static function () use ($read, $store, $ai, $st, $at, $staffUuid,
    $mealDate, $menuDate, $candidate): array {
    $aiUserId = (int)$candidate['u'];
    $queries = [
        'staff' => [$store, 'SELECT UUID,AI_USER_ID,NAME,PERSONNEL_UMBER,IS_SHOW FROM '
            . $st . 'staff WHERE UUID=?', [$staffUuid]],
        'staff_department' => [$store, 'SELECT STAFF_UUID,DEPARTMENT_UUID FROM '
            . $st . 'staff_department WHERE STAFF_UUID=? ORDER BY DEPARTMENT_UUID', [$staffUuid]],
        'recommend' => [$store, 'SELECT UUID,STAFF_UUID,MENU_DATE,TOTAL_POWER FROM '
            . $st . 'menu_recommend WHERE STAFF_UUID=? AND MENU_DATE=? ORDER BY UUID',
            [$staffUuid, $menuDate]],
        'date_menu' => [$store, 'SELECT DM.UUID,DM.RECOMMEND_UUID,DM.MENU_DATE,DM.MEAL_TIMES,DM.NUMBER,DM.DISHES_UUID'
            . ' FROM ' . $st . 'date_menu DM JOIN ' . $st . 'menu_recommend R ON R.UUID=DM.RECOMMEND_UUID'
            . ' WHERE R.STAFF_UUID=? AND R.MENU_DATE=? ORDER BY DM.UUID',
            [$staffUuid, $menuDate]],
        'ai_staff' => [$ai, 'SELECT ID,USER_ID,STORE_ID,IS_DELETE FROM '
            . $at . 'staff WHERE USER_ID=? AND STORE_ID=10001 AND IS_DELETE=0 ORDER BY ID', [$aiUserId]],
        'ai_meal' => [$ai, 'SELECT M.ID,M.STAFF_ID,M.MEAL_DATE,M.MEAL_TIMES,M.MEAL_TIME,M.UPDATE_TIME'
            . ' FROM ' . $at . 'meal M JOIN ' . $at . 'staff S ON S.ID=M.STAFF_ID'
            . ' WHERE S.USER_ID=? AND M.MEAL_DATE=? AND M.STORE_ID=10001 AND M.IS_DELETE=0 ORDER BY M.ID',
            [$aiUserId, $mealDate]],
        'ai_infos' => [$ai, 'SELECT I.ID,I.MEAL_ID,I.MEAL_FILE_ID,I.DISHES_ID,I.DISHES_WEIGHT,I.ENERGY,I.PROTEIN'
            . ' FROM ' . $at . 'meal_infos I JOIN ' . $at . 'meal M ON M.ID=I.MEAL_ID'
            . ' JOIN ' . $at . 'staff S ON S.ID=M.STAFF_ID'
            . ' WHERE S.USER_ID=? AND M.MEAL_DATE=? AND I.STORE_ID=10001 AND I.IS_DELETE=0 ORDER BY I.ID',
            [$aiUserId, $mealDate]],
    ];
    $result = [];
    foreach ($queries as $name => [$db, $sql, $args]) {
        $rows = $read($db, $sql, $args);
        $encoded = json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        if (!is_string($encoded)) throw new RuntimeException('source encode');
        $result[$name] = ['count' => count($rows), 'sha256' => hash('sha256', $encoded), 'rows' => $rows];
    }
    return $result;
};
$digest = static function (array $snapshot): array {
    $result = [];
    foreach ($snapshot as $name => $item) $result[$name] = [$item['count'], $item['sha256']];
    return $result;
};
$runtimeReady = static function () use ($pid, $root, $base): bool {
    $cmdline = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
    $children = trim((string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children'));
    $boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
    return strpos($cmdline, $root . '/public') !== false
        && strpos($cmdline, (string)parse_url($base, PHP_URL_PORT)) !== false
        && $children === '' && is_array($boundary)
        && !empty($boundary['external_stubs']) && !empty($boundary['redis_isolated'])
        && empty($boundary['real_queue']);
};
$post = static function (string $path, array $form, string $token) use ($base, $runtimeReady): array {
    if (!in_array($path, ['/p/Ontoanalysis/MonitorList',
        '/p/Ontoanalysis/FoodRecordUploads', '/p/Ontoanalysis/getMealsStatus'], 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, 1048577) : false;
    $meta = is_resource($stream) ? stream_get_meta_data($stream) : [];
    if (is_resource($stream)) fclose($stream);
    if (!is_string($body) || strlen($body) > 1048576 || !empty($meta['timed_out'])
        || strpos((string)($http_response_header[0] ?? ''), ' 200 ') === false) {
        throw new RuntimeException('HTTP outcome incomplete');
    }
    $json = json_decode($body, true);
    if (!is_array($json) || ($json['code'] ?? null) !== 0 || !isset($json['data'])) {
        throw new RuntimeException('HTTP business response');
    }
    return $json;
};

$stage = 'source-snapshot';
$before = $snapshot();
if ($before['staff']['count'] !== 1 || $before['ai_staff']['count'] !== 1
    || $before['recommend']['count'] < 1 || $before['date_menu']['count'] < 1
    || $before['ai_meal']['count'] < 1 || $before['ai_infos']['count'] < 1) {
    throw new RuntimeException('positive source absent');
}
$original = $digest($before);
$marker = 'CDXONTO3' . strtoupper(bin2hex(random_bytes(6)));
if ($count($store, 'SELECT COUNT(*) FROM ' . $userTable
    . ' WHERE UUID=? OR ACCOUNT=? OR NAME=?', [$marker, $marker, $marker]) !== 0) {
    throw new RuntimeException('actor collision');
}
$stage = 'actor';
$actorId = 0;
$failure = '';
$result = ['lane' => $lane, 'marker' => $marker, 'routes' => [],
    'source_counts' => array_map(static function ($x) { return $x['count']; }, $before),
    'source_unchanged' => false, 'actor_zero' => false];
try {
    if (!$runtimeReady()) throw new RuntimeException('worker drift');
    $stmt = $store->prepare('INSERT INTO ' . $userTable
        . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
        . ' VALUES (?,?,?,?,?,?,?,?,?)');
    $stmt->execute([$marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
        $marker, 0, date('Y-m-d H:i:s'), 'CODEX', 0]);
    if ($stmt->rowCount() !== 1) throw new RuntimeException('actor insert');
    $ids = $read($store, 'SELECT ID FROM ' . $userTable
        . ' WHERE UUID=? AND ACCOUNT=? AND NAME=? AND TYPE=0 AND CREATE_BY=?',
        [$marker, $marker, $marker, 'CODEX'], 2);
    if (count($ids) !== 1) throw new RuntimeException('actor identity');
    $actorId = (int)$ids[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');

    $stage = 'monitor-http';
    $staffSource = $before['staff']['rows'][0];
    $staffName = trim((string)$staffSource['name']);
    if ($staffName === '') throw new RuntimeException('monitor source name empty');
    $monitor = $post('/p/Ontoanalysis/MonitorList',
        ['department_uuid' => $department, 'time' => $menuDate,
            'keyword' => $staffName, 'page' => 1], $token);
    if (!is_array($monitor['data']) || !$monitor['data'] || (int)($monitor['total'] ?? 0) < 1) {
        throw new RuntimeException('monitor empty');
    }
    $monitorOwn = false;
    foreach ($monitor['data'] as $row) {
        if ((string)($row['uuid'] ?? '') === $staffUuid) {
            $monitorOwn = (string)($row['personnel_umber'] ?? '')
                === (string)$staffSource['personnel_umber'];
        }
    }
    if (!$monitorOwn) throw new RuntimeException('monitor own staff/source mismatch');
    $result['routes']['MonitorList'] = ['count' => count($monitor['data']),
        'own_staff_source_match' => true, 'envelope' => $shape($monitor),
        'row' => $shape($monitor['data'][0])];

    $stage = 'food-http';
    $food = $post('/p/Ontoanalysis/FoodRecordUploads',
        ['staff_uuid' => $staffUuid, 'start_date' => $mealDate, 'end_date' => $mealDate], $token);
    $foodRows = $food['data']['rows'] ?? null;
    if (!is_array($foodRows) || !$foodRows || (int)($food['data']['total'] ?? 0) !== count($foodRows)) {
        throw new RuntimeException('food rows empty/mismatch');
    }
    $mealIds = array_fill_keys(array_map(static function ($r) { return (int)$r['id']; },
        $before['ai_meal']['rows']), true);
    $infoIds = array_fill_keys(array_map(static function ($r) { return (int)$r['id']; },
        $before['ai_infos']['rows']), true);
    $items = 0;
    foreach ($foodRows as $foodRow) {
        if (!isset($mealIds[(int)($foodRow['meal_id'] ?? 0)])) {
            throw new RuntimeException('food meal source mismatch');
        }
        foreach ($foodRow['items'] ?? [] as $item) {
            if (!isset($infoIds[(int)($item['id'] ?? 0)])) {
                throw new RuntimeException('food item source mismatch');
            }
            $items++;
        }
    }
    if ($items < 1) throw new RuntimeException('food items empty');
    $result['routes']['FoodRecordUploads'] = ['count' => count($foodRows),
        'items' => $items, 'own_source_match' => true,
        'envelope' => $shape($food), 'data' => $shape($food['data']),
        'row' => $shape($foodRows[0]), 'item' => $shape($foodRows[0]['items'][0])];

    $stage = 'meals-status-http';
    $status = $post('/p/Ontoanalysis/getMealsStatus',
        ['staff_uuid' => $staffUuid, 'start_date' => $menuDate, 'end_date' => $menuDate], $token);
    // The nonempty business branch returns only the missing-meal date map.
    if (!is_array($status['data'])) {
        throw new RuntimeException('meals status shape');
    }
    $result['routes']['getMealsStatus'] = ['source_recommend' => $before['recommend']['count'],
        'source_menu' => $before['date_menu']['count'],
        'missing_day_entries' => count($status['data']),
        'envelope' => $shape($status), 'data' => $shape($status['data'])];

    $stage = 'source-after';
    $result['source_unchanged'] = $digest($snapshot()) === $original;
    if (!$result['source_unchanged']) throw new RuntimeException('source mutated');
} catch (Throwable $error) {
    $failure = $stage . ':' . get_class($error) . ':' . (string)$error->getCode()
        . ':' . substr(hash('sha256', $error->getMessage()), 0, 12);
} finally {
    if ($actorId > 0) {
        $row = $read($store, '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($row) !== 1) throw new RuntimeException('actor cleanup identity');
        $delete = $store->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($store, '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);
