<?php
declare(strict_types=1);

// Local-development MySQL only. Read-only by default; execute owns one
// temporary disabled operator, calls previewData once, then deletes it.
$mode = (string)($argv[1] ?? '');
$sha = (string)hash_file('sha256', __FILE__);
if (!in_array($mode, ['--preflight', '--execute', '--audit'], true)
    || count($argv) !== 2
    || ($mode === '--execute' && ((string)getenv('CDX_EM_B_APPROVE')
        !== 'ROOT_LUNCH_RIGHT_PREVIEW_V1'
        || !hash_equals($sha, (string)getenv('CDX_EM_B_SHA'))))) {
    echo json_encode(['status' => 'default_deny', 'http_sent' => false,
        'dml_sent' => false]), PHP_EOL;
    exit(78);
}
$stage = 'source_runtime';
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'redacted_stop',
        'stage' => $stage, 'error_class' => get_class($error),
        'code' => (string)$error->getCode()]), PHP_EOL;
    exit(2);
});
$root = '/tmp/zhct-http-store-mysql.dQTTBP';
$base = 'http://127.0.0.1:18081';
$private = '/tmp/cdx-em-b-right-lunch-20261005';
$journal = $private . '/recovery.json';
if (PHP_VERSION_ID < 70300 || PHP_VERSION_ID >= 70400
    || is_link($root)
    || hash_file('sha256', $root . '/application/common/service/ElectronicMenu.php')
        !== '179869c96cfdaf5e9de1003ace06949dc654bc4876efe89ec53455a7721e47cd'
    || hash_file('sha256', $root . '/application/p/controller/ElectronicMenu.php')
        !== 'a93de90caced9884bb40374b9f3da7cbb04997930023e6cb229a7ca7ffefc2d7'
    || hash_file('sha256', $root . '/application/common/Common.php')
        !== '49e2eab5fdc17e514f9420418b4cc281b92edb560b99f7cc62935011138dfeb4') {
    throw new RuntimeException('B source/runtime drift');
}
$stage = 'isolated_listener';
$cmd = (string)@file_get_contents('/proc/10001/cmdline');
$children = trim((string)@file_get_contents('/proc/10001/task/10001/children'));
if (strpos($cmd, '127.0.0.1:18081') === false
    || strpos($cmd, $root . '/public') === false || $children !== '') {
    throw new RuntimeException('B isolated listener drift');
}
$boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
if (!is_array($boundary) || ($boundary['external_stubs'] ?? null) !== true
    || ($boundary['redis_isolated'] ?? null) !== true
    || ($boundary['real_queue'] ?? true) !== false) {
    throw new RuntimeException('B external/queue boundary drift');
}
$stage = 'local_database_identity';
$cfg = require $root . '/application/database.php';
$config = $cfg;
$local = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
unset($config);
foreach (['hostname', 'hostport', 'database', 'username', 'password', 'type'] as $key) {
    $stage = 'local_database_identity_' . $key;
    if (($cfg[$key] ?? null) !== ($local[$key] ?? null)) {
        throw new RuntimeException('B database not code local');
    }
}
$stage = 'local_database_connect';
$db = new PDO('mysql:host=' . $cfg['hostname'] . ';port=' . (int)$cfg['hostport']
    . ';dbname=' . $cfg['database'] . ';charset=' . ($cfg['charset'] ?? 'utf8'),
    (string)$cfg['username'], (string)$cfg['password'],
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
$stage = 'local_database_server_identity';
$identity = (string)$db->query(
    "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
    ->fetchColumn();
if (hash('sha256', $identity)
    !== '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401') {
    throw new RuntimeException('B development MySQL identity drift');
}
date_default_timezone_set('PRC');
$read = static function (string $day) use ($db): array {
    $menus = $db->query('SELECT ID,LEFT_RESTAURANT_ID,RIGHT_RESTAURANT_ID'
        . ' FROM ydy_electronic_menu WHERE STORE_ID=10001 AND STATUS=1'
        . ' AND IS_DELETE=0 AND DISPLAY_MODE=2 AND RIGHT_RESTAURANT_ID>0'
        . ' ORDER BY ID')->fetchAll(PDO::FETCH_ASSOC);
    if (count($menus) < 1 || count($menus) > 7) {
        throw new RuntimeException('B natural dual-menu scope drift');
    }
    $chosen = null;
    $rightDishes = [];
    $leftDishes = [];
    $restaurants = [];
    $meal = 0;
    $clock = date('H:i:s');
    $r = $db->prepare('SELECT ID,RESTAURANT_SHOP_ID,STATUS,IS_DELETE,'
        . ' MORNING_START,MORNING_END,NOON_START,NOON_END,NIGHT_START,NIGHT_END'
        . ' FROM ydy_restaurants WHERE ID IN (?,?) ORDER BY ID');
    $q = $db->prepare('SELECT rec.ID RECIPE_ID,d.ID DISH_ID,d.UUID,d.NAME,d.PRICE,'
        . ' d.FILE_PATH,d.CARBOHYDRATE,d.PROTEIN,d.FAT'
        . ' FROM ydy_recipes rec JOIN ydy_dishes d ON d.UUID=rec.DISHES_UUID'
        . ' WHERE rec.RESTAURANT_ID=? AND rec.MENU_DATE=? AND rec.MEAL_TIMES=?'
        . ' AND d.STATUS=1 AND d.IS_DEL=1 ORDER BY rec.ID');
    foreach ($menus as $menu) {
        $oldLeft = (int)$menu['left_restaurant_id'];
        $oldRight = (int)$menu['right_restaurant_id'];
        if ($oldLeft <= 0 || $oldRight <= 0 || $oldLeft === $oldRight) continue;
        $r->execute([$oldLeft, $oldRight]);
        $rs = $r->fetchAll(PDO::FETCH_ASSOC);
        if (count($rs) !== 2) continue;
        $byId = [];
        foreach ($rs as $row) $byId[(int)$row['id']] = $row;
        if (!isset($byId[$oldLeft], $byId[$oldRight])
            || (int)$byId[$oldLeft]['status'] !== 1
            || (int)$byId[$oldRight]['status'] !== 1
            || (int)$byId[$oldLeft]['is_delete'] !== 2
            || (int)$byId[$oldRight]['is_delete'] !== 2
            || (int)$byId[$oldLeft]['restaurant_shop_id']
                !== (int)$byId[$oldRight]['restaurant_shop_id']) continue;
        // The requested left is oldRight after swapping; its schedule
        // determines preview's meal period for both sides.
        $candidateMeal = 4;
        foreach (['morning' => 1, 'noon' => 2, 'night' => 3] as $name => $number) {
            $from = substr((string)$byId[$oldRight][$name . '_start'], 0, 5) . ':00';
            $to = substr((string)$byId[$oldRight][$name . '_end'], 0, 5) . ':59';
            if ($clock >= $from && $clock <= $to) { $candidateMeal = $number; break; }
        }
        if ($candidateMeal !== 2) continue;
        $q->execute([$oldLeft, $day, $candidateMeal]);
        $right = $q->fetchAll(PDO::FETCH_ASSOC);
        $q->execute([$oldRight, $day, $candidateMeal]);
        $left = $q->fetchAll(PDO::FETCH_ASSOC);
        if (count($right) < 1 || count($right) > 100 || $left !== []) continue;
        $chosen = $menu; $rightDishes = $right; $leftDishes = $left;
        $restaurants = $rs; $meal = $candidateMeal;
        break;
    }
    if ($chosen === null) throw new RuntimeException('B lunch right-positive source missing');
    $ids = array_values(array_unique(array_map(static function (array $row): int {
        return (int)$row['dish_id'];
    }, $rightDishes)));
    $tags = [];
    $t = $db->prepare('SELECT DISHES_ID,NAME FROM ydy_dishes_tag WHERE IS_DELETE=0'
        . ' AND DISHES_ID IN (' . implode(',', array_fill(0, count($ids), '?')) . ')'
        . ' ORDER BY ID');
    $t->execute($ids);
    foreach ($t->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $tags[(int)$row['dishes_id']][] = $row['name'];
    }
    return ['day' => $day, 'clock' => $clock, 'meal' => $meal,
        'old_left' => (int)$chosen['left_restaurant_id'],
        'old_right' => (int)$chosen['right_restaurant_id'],
        'right' => $rightDishes, 'left' => $leftDishes, 'tags' => $tags,
        'digest' => hash('sha256', serialize([$chosen, $restaurants,
            $rightDishes, $leftDishes, $tags]))];
};
$stage = 'natural_lunch_source';
$today = date('Y-m-d');
$source = $read($today);
$markerPrefix = 'CDXEMBRP';
$actorCount = (int)$db->query("SELECT COUNT(*) FROM ydy_user"
    . " WHERE ACCOUNT LIKE 'CDXEMBRP%'")->fetchColumn();
if ($mode === '--preflight') {
    if (is_dir($private) || $actorCount !== 0) {
        throw new RuntimeException('B previous actor/journal requires review');
    }
    echo json_encode(['status' => 'select_only_ready', 'lane' => 'B',
        'day' => $today, 'meal' => $source['meal'],
        'natural_right_source_count' => count($source['right']),
        'natural_left_source_count' => count($source['left']),
        'actor_count' => 0, 'http_sent' => false, 'dml_sent' => false]), PHP_EOL;
    exit(0);
}
if ($mode === '--audit') {
    if (!is_file($journal)) throw new RuntimeException('B recovery journal missing');
    $saved = json_decode((string)file_get_contents($journal), true);
    if (!is_array($saved) || !preg_match('/^CDXEMBRP[A-F0-9]{12}$/D',
        (string)($saved['marker'] ?? '')) || ($saved['day'] ?? null) !== $today
        || ($saved['source_sha'] ?? null) !== $source['digest']
        || $actorCount !== 0) {
        throw new RuntimeException('B independent audit mismatch');
    }
    $actor = $db->prepare('SELECT COUNT(*) FROM ydy_user WHERE ID=?'
        . ' AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
    $actor->execute([(int)$saved['actor_id'], $saved['marker'],
        $saved['marker'], 'CODEX']);
    if ((int)$actor->fetchColumn() !== 0) {
        throw new RuntimeException('B exact actor remains');
    }
    unlink($journal);
    rmdir($private);
    echo json_encode(['status' => 'independent_zero_pass', 'lane' => 'B',
        'source_unchanged' => true, 'actor_count' => 0,
        'http_sent' => false, 'dml_sent' => false]), PHP_EOL;
    exit(0);
}
if (is_dir($private) || $actorCount !== 0 || !mkdir($private, 0700)) {
    throw new RuntimeException('B exclusive private journal unavailable');
}
chmod($private, 0700);
$marker = $markerPrefix . strtoupper(bin2hex(random_bytes(6)));
$saved = ['marker' => $marker, 'actor_id' => 0, 'day' => $today,
    'source_sha' => $source['digest'], 'phase' => 'before_insert'];
$save = static function () use ($journal, &$saved): void {
    if (file_put_contents($journal, json_encode($saved), LOCK_EX) === false
        || !chmod($journal, 0600)) throw new RuntimeException('B journal write failed');
};
$save();
$result = ['status' => 'FAIL', 'lane' => 'B',
    'right_source_count' => count($source['right'])];
$failure = null;
try {
    $db->prepare('INSERT INTO ydy_user'
        . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
        . ' VALUES (?,?,?,?,?,0,?,?,0)')
        ->execute([$marker, $marker, 'DISABLED',
            strtoupper(bin2hex(random_bytes(8))), $marker,
            date('Y-m-d H:i:s'), 'CODEX']);
    $actor = $db->prepare('SELECT ID FROM ydy_user WHERE UUID=? AND ACCOUNT=?'
        . ' AND TYPE=0 AND CREATE_BY=?');
    $actor->execute([$marker, $marker, 'CODEX']);
    $ids = $actor->fetchAll(PDO::FETCH_COLUMN);
    if (count($ids) !== 1 || (int)$ids[0] <= 0) {
        throw new RuntimeException('B actor readback ambiguous');
    }
    $saved['actor_id'] = (int)$ids[0];
    $saved['phase'] = 'actor_inserted'; $save();
    require_once $root . '/application/base/Security.php';
    $token = \app\base\Security::encrypt(implode("\t", [
        (string)$saved['actor_id'], $marker, $marker, '0', '', '', $marker,
    ]));
    $saved['phase'] = 'http_started'; $save();
    $raw = @file_get_contents($base . '/p/electronicMenu/previewData', false,
        stream_context_create(['http' => [
            'method' => 'POST', 'timeout' => 12, 'ignore_errors' => true,
            'header' => "Content-Type: application/x-www-form-urlencoded\r\ntoken: "
                . $token . "\r\n",
            'content' => http_build_query([
                'display_mode' => 2,
                'left_restaurant_id' => $source['old_right'],
                'right_restaurant_id' => $source['old_left'],
                'carousel_interval' => 8,
            ], '', '&', PHP_QUERY_RFC3986),
        ]]));
    $headers = $http_response_header ?? [];
    preg_match('/HTTP\/\S+\s+(\d+)/', (string)($headers[0] ?? ''), $match);
    $response = is_string($raw) ? json_decode($raw, true) : null;
    if ((int)($match[1] ?? 0) !== 200 || !is_array($response)
        || ($response['code'] ?? null) !== 0 || !is_array($response['data'] ?? null)) {
        throw new RuntimeException('B right preview HTTP failed');
    }
    $data = $response['data'];
    $stalls = $data['stalls'] ?? null;
    if (!is_array($stalls) || count($stalls) !== 2
        || ($data['config']['display_mode'] ?? null) !== 2
        || ($data['config']['carousel_interval'] ?? null) !== 8
        || ($data['context']['business_date'] ?? null) !== $today
        || ($data['context']['meal_times'] ?? null) !== $source['meal']
        || ($stalls[0]['position'] ?? null) !== 'left'
        || ($stalls[1]['position'] ?? null) !== 'right'
        || ($stalls[0]['restaurant_id'] ?? null) !== $source['old_right']
        || ($stalls[1]['restaurant_id'] ?? null) !== $source['old_left']
        || ($stalls[0]['dishes'] ?? null) !== []
        || !is_array($stalls[1]['dishes'] ?? null)
        || count($stalls[1]['dishes']) !== count($source['right'])) {
        throw new RuntimeException('B right preview shape/source mismatch');
    }
    foreach ($source['right'] as $index => $dbRow) {
        $dish = $stalls[1]['dishes'][$index] ?? null;
        if (!is_array($dish) || ($dish['uuid'] ?? null) !== $dbRow['uuid']
            || ($dish['name'] ?? null) !== $dbRow['name']
            || ($dish['price'] ?? null) !== number_format((float)$dbRow['price'], 2, '.', '')
            || !is_string($dish['image_url'] ?? null)
            || ($dish['health_tags'] ?? null) !== ($source['tags'][(int)$dbRow['dish_id']] ?? [])) {
            throw new RuntimeException('B right preview row mismatch');
        }
        foreach (['carbohydrate', 'protein', 'fat'] as $field) {
            if (($dish[$field] ?? null) !== number_format((float)$dbRow[$field], 2, '.', '')) {
                throw new RuntimeException('B right preview nutrient mismatch');
            }
        }
        $path = str_replace('\\', '/', (string)$dbRow['file_path']);
        if (($path === '' && $dish['image_url'] !== '')
            || ($path !== '' && substr($dish['image_url'], -strlen($path)) !== $path)) {
            throw new RuntimeException('B right preview image-path mismatch');
        }
    }
    if ($read($today)['digest'] !== $source['digest']) {
        throw new RuntimeException('B source changed during preview');
    }
    $result = ['status' => 'HTTP_PASS_PENDING_AUDIT', 'lane' => 'B',
        'http_code' => 200, 'business_code' => 0,
        'right_source_count' => count($source['right']),
        'right_json_count' => count($stalls[1]['dishes']),
        'left_empty' => true, 'right_values_match' => true,
        'source_unchanged' => true,
        'response_type_sha' => hash('sha256', serialize([
            gettype($data['config']['display_mode']),
            gettype($data['context']['meal_times']),
            array_map('gettype', $stalls[1]['dishes'][0]),
        ]))];
} catch (Throwable $error) {
    $failure = get_class($error) . ':' . (string)$error->getCode();
} finally {
    $delete = $db->prepare('DELETE FROM ydy_user WHERE ID=? AND UUID=?'
        . ' AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
    $delete->execute([(int)$saved['actor_id'], $marker, $marker, 'CODEX']);
    if ($saved['actor_id'] > 0 && $delete->rowCount() !== 1) {
        throw new RuntimeException('B actor exact cleanup failed; journal retained');
    }
    $saved['phase'] = 'actor_deleted'; $save();
}
if ($failure !== null) $result['failure'] = $failure;
echo json_encode($result, JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($failure === null ? 0 : 2);
