<?php
declare(strict_types=1);

// C only. Default is SELECT-only; --execute requires a pinned self SHA and
// explicit parent release. No existing business row is ever updated/deleted.
$mode = (string)($argv[1] ?? '');
$selfSha = hash_file('sha256', __FILE__);
if (!in_array($mode, ['--preflight', '--execute', '--audit', '--diagnose-log'], true)
    || ($mode === '--execute'
        && ((string)getenv('CDX_EM_RIGHT_APPROVE') !== 'ROOT_RELEASED_C_RIGHT_PREVIEW_V1'
            || (string)getenv('CDX_EM_RIGHT_SHA') !== $selfSha))
    || ($mode === '--audit' && (string)getenv('CDX_EM_RIGHT_AUDIT_SHA') !== $selfSha)) {
    echo json_encode(['status' => 'default-deny', 'http_sent' => false,
        'dml_sent' => false]), PHP_EOL;
    exit(78);
}
set_exception_handler(static function (Throwable $error): void {
    echo json_encode(['status' => 'redacted_stop',
        'error_class' => get_class($error), 'error_code' => (string)$error->getCode()]), PHP_EOL;
    exit(2);
});
$root = '/tmp/zhct-dm-poc-20260928.fcpiiN/store';
$base = 'http://127.0.0.1:18080';
$pid = 112175;
$private = '/tmp/cdx-em-right-preview-20261004';
$journal = $private . '/recovery.json';
if (is_link($root) || !is_dir($root) || !is_file($root . '/application/database.php')
    || PHP_VERSION_ID < 70300 || PHP_VERSION_ID >= 70400
    || !in_array('dm', PDO::getAvailableDrivers(), true)
    || 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('Approved C runtime/source drift');
}
$cmdline = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
$children = trim((string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children'));
if (strpos($cmdline, '127.0.0.1:18080') === false
    || strpos($cmdline, $root . '/public') === false || $children !== '') {
    throw new RuntimeException('C isolated listener drift');
}
$boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
if (!is_array($boundary) || empty($boundary['external_stubs'])
    || empty($boundary['redis_isolated']) || !empty($boundary['real_queue'])) {
    throw new RuntimeException('C external/queue isolation not proven');
}
$config = require $root . '/application/database.php';
if (!is_array($config) || (string)($config['hostname'] ?? '') !== '127.0.0.1'
    || (int)($config['hostport'] ?? 0) !== 5237
    || strtoupper((string)($config['database'] ?? '')) !== 'ZHCT_APP'
    || strtoupper((string)($config['username'] ?? '')) !== 'ZHCT_APP') {
    throw new RuntimeException('Nonproduction C schema drift');
}
try {
    $db = new PDO('dm:host=127.0.0.1;port=5237;schema=ZHCT_APP',
        (string)$config['username'], (string)$config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER,
            PDO::ATTR_TIMEOUT => 5,
        ]);
} catch (PDOException $error) {
    throw new RuntimeException('C database unavailable; connection details redacted');
}
$schema = strtoupper((string)$db->query(
    "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn());
if ($schema !== 'ZHCT_APP') throw new RuntimeException('C schema identity drift');
$readSource = static function (?string $fixedDate = null, ?int $fixedMeal = null) use ($db): array {
    date_default_timezone_set('PRC');
    $date = $fixedDate ?? date('Y-m-d');
    $clock = date('H:i:s');
    $menus = $db->query('SELECT ID,UUID,LEFT_RESTAURANT_ID,RIGHT_RESTAURANT_ID,'
        . ' DISPLAY_MODE,STORE_ID,STATUS,IS_DELETE 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) throw new RuntimeException('C dual-menu count drift');
    $menu = $menus[0];
    $oldLeft = (int)$menu['left_restaurant_id'];
    $oldRight = (int)$menu['right_restaurant_id'];
    if ($oldLeft <= 0 || $oldRight <= 0 || $oldLeft === $oldRight) {
        throw new RuntimeException('C dual relation invalid');
    }
    $r = $db->prepare('SELECT ID,RESTAURANT_SHOP_ID,STATUS,IS_DELETE,NAME,'
        . ' MORNING_START,MORNING_END,NOON_START,NOON_END,NIGHT_START,NIGHT_END'
        . ' FROM YDY_RESTAURANTS WHERE ID IN (?,?) ORDER BY ID');
    $r->execute([$oldLeft, $oldRight]);
    $restaurants = $r->fetchAll(PDO::FETCH_ASSOC);
    if (count($restaurants) !== 2) throw new RuntimeException('C stall source missing');
    $byId = [];
    foreach ($restaurants as $restaurant) {
        if ((int)$restaurant['status'] !== 1 || (int)$restaurant['is_delete'] !== 2) {
            throw new RuntimeException('C stall inactive');
        }
        $byId[(int)$restaurant['id']] = $restaurant;
    }
    if ((int)$byId[$oldLeft]['restaurant_shop_id']
        !== (int)$byId[$oldRight]['restaurant_shop_id']) {
        throw new RuntimeException('C swap would cross restaurant-shop boundary');
    }
    // On preview, the original right stall is the requested left; its schedule
    // determines the meal period for both sides.
    $previewLeft = $byId[$oldRight];
    $meal = 4;
    foreach (['morning' => 1, 'noon' => 2, 'night' => 3] as $name => $number) {
        $from = substr((string)$previewLeft[$name . '_start'], 0, 5) . ':00';
        $to = substr((string)$previewLeft[$name . '_end'], 0, 5) . ':59';
        if ($clock >= $from && $clock <= $to) { $meal = $number; break; }
    }
    if ($fixedMeal !== null) $meal = $fixedMeal;
    if ($meal !== 3) throw new RuntimeException('C expected natural dinner source unavailable');
    $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');
    $q->execute([$oldLeft, $date, $meal]);
    $rightDishes = $q->fetchAll(PDO::FETCH_ASSOC);
    if (count($rightDishes) < 1 || count($rightDishes) > 100) {
        throw new RuntimeException('C right-preview source count drift');
    }
    $q->execute([$oldRight, $date, $meal]);
    $leftDishes = $q->fetchAll(PDO::FETCH_ASSOC);
    if ($leftDishes !== []) throw new RuntimeException('C left-preview source not empty');
    $ids = array_values(array_unique(array_map(static function (array $row): int {
        return (int)$row['dish_id'];
    }, $rightDishes)));
    $tagMap = [];
    if ($ids) {
        $placeholder = implode(',', array_fill(0, count($ids), '?'));
        $t = $db->prepare('SELECT DISHES_ID,NAME FROM YDY_DISHES_TAG'
            . ' WHERE IS_DELETE=0 AND DISHES_ID IN (' . $placeholder . ') ORDER BY ID');
        $t->execute($ids);
        foreach ($t->fetchAll(PDO::FETCH_ASSOC) as $tag) {
            $tagMap[(int)$tag['dishes_id']][] = $tag['name'];
        }
    }
    $devices = $db->prepare('SELECT ID,MENU_ID,STATUS,IS_DELETE'
        . ' FROM YDY_ELECTRONIC_MENU_DEVICE WHERE MENU_ID=? ORDER BY ID');
    $devices->execute([(int)$menu['id']]);
    $source = [$menu, $restaurants, $rightDishes, $leftDishes,
        $tagMap, $devices->fetchAll(PDO::FETCH_ASSOC)];
    return compact('date', 'clock', 'meal', 'oldLeft', 'oldRight',
        'restaurants', 'rightDishes', 'leftDishes', 'tagMap')
        + ['digest' => hash('sha256', serialize($source))];
};
$auditSaved = null;
if ($mode === '--audit') {
    if (!is_file($journal)) throw new RuntimeException('Recovery journal missing');
    $auditSaved = json_decode((string)file_get_contents($journal), true);
    if (!is_array($auditSaved)
        || !preg_match('/^CDXEMRP[A-F0-9]{12}$/D', (string)($auditSaved['marker'] ?? ''))
        || !preg_match('/^\d{4}-\d{2}-\d{2}$/D', (string)($auditSaved['date'] ?? ''))
        || (int)($auditSaved['meal'] ?? 0) !== 3) {
        throw new RuntimeException('Recovery journal invalid');
    }
}
$source = $mode === '--audit'
    ? $readSource((string)$auditSaved['date'], (int)$auditSaved['meal'])
    : $readSource();
if ($mode === '--diagnose-log') {
    $path = '/tmp/zhct-dm-poc-20260928.fcpiiN/store/runtime/log/202610/04.log';
    $log = (string)file_get_contents($path);
    $request = strrpos($log, '/p/electronicMenu/previewData');
    $start = $request === false ? false : strpos($log, '[ log ] ', $request);
    if ($start === false) throw new RuntimeException('C recorded preview response missing');
    $tail = substr($log, $start + 8);
    $recorded = json_decode(substr($tail, 0, strcspn($tail, "\r\n")), true);
    if (!is_array($recorded) || !is_array($recorded['data'] ?? null)) {
        throw new RuntimeException('C recorded preview response unparsable');
    }
    $data = $recorded['data'];
    $stalls = $data['stalls'] ?? [];
    $right = $stalls[1]['dishes'] ?? [];
    $bad = ['uuid' => 0, 'name' => 0, 'price' => 0,
        'nutrients' => 0, 'image' => 0, 'tags' => 0];
    $sourceWithBackslash = 0;
    $normalizedImageBad = 0;
    foreach ($source['rightDishes'] as $index => $raw) {
        $dish = $right[$index] ?? [];
        if (($dish['uuid'] ?? null) !== $raw['uuid']) $bad['uuid']++;
        if (($dish['name'] ?? null) !== $raw['name']) $bad['name']++;
        if (($dish['price'] ?? null) !== number_format((float)$raw['price'], 2, '.', '')) {
            $bad['price']++;
        }
        foreach (['carbohydrate', 'protein', 'fat'] as $field) {
            if (!is_string($dish[$field] ?? null)
                || $dish[$field] !== number_format((float)$raw[$field], 2, '.', '')) {
                $bad['nutrients']++;
            }
        }
        $file = (string)$raw['file_path'];
        if (strpos($file, '\\') !== false) $sourceWithBackslash++;
        $normalizedFile = str_replace('\\', '/', $file);
        if (!is_string($dish['image_url'] ?? null)
            || ($normalizedFile === '' && $dish['image_url'] !== '')
            || ($normalizedFile !== ''
                && substr($dish['image_url'], -strlen($normalizedFile)) !== $normalizedFile)) {
            $normalizedImageBad++;
        }
        if (!is_string($dish['image_url'] ?? null)
            || ($file === '' && $dish['image_url'] !== '')
            || ($file !== '' && substr($dish['image_url'], -strlen($file)) !== $file)) {
            $bad['image']++;
        }
        if (($dish['health_tags'] ?? null) !== ($source['tagMap'][(int)$raw['dish_id']] ?? [])) {
            $bad['tags']++;
        }
    }
    echo json_encode(['status' => 'recorded_response_select_only_diagnosis',
        'code_integer_zero' => ($recorded['code'] ?? null) === 0,
        'business_date_match' => ($data['context']['business_date'] ?? null) === $source['date'],
        'meal_times_match' => ($data['context']['meal_times'] ?? null) === $source['meal'],
        'left_empty' => ($stalls[0]['dishes'] ?? null) === [],
        'right_count_match' => count($right) === count($source['rightDishes']),
        'position_match' => ($stalls[0]['position'] ?? null) === 'left'
            && ($stalls[1]['position'] ?? null) === 'right',
        'restaurant_ids_match' => ($stalls[0]['restaurant_id'] ?? null) === $source['oldRight']
            && ($stalls[1]['restaurant_id'] ?? null) === $source['oldLeft'],
        'source_sha_match_pre_http' => $source['digest']
            === '2b14217d4fe3517aefcc5e6cdf5fb35f0411b10c7dff243fe1e5672525a6e08c',
        'source_file_path_with_backslash_count' => $sourceWithBackslash,
        'normalized_image_mismatch_count' => $normalizedImageBad,
        'mismatch_counts' => $bad, 'business_http_sent' => false,
        'dml_sent' => false], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
$actorCount = (int)$db->query("SELECT COUNT(*) FROM YDY_USER"
    . " WHERE ACCOUNT LIKE 'CDXEMRP%'")->fetchColumn();
if ($mode === '--preflight') {
    if (is_dir($private) || $actorCount !== 0) {
        throw new RuntimeException('Prior right-preview state requires review');
    }
    $withImage = 0;
    foreach ($source['rightDishes'] as $dish) {
        if ((string)$dish['file_path'] !== '') $withImage++;
    }
    echo json_encode(['status' => 'select_only_ready', 'lane' => 'C',
        'business_date' => $source['date'], 'clock' => $source['clock'],
        'meal_times' => $source['meal'],
        'same_shop' => true, 'preview_left_source_count' => 0,
        'preview_right_source_count' => count($source['rightDishes']),
        'preview_right_with_image' => $withImage,
        'preview_right_tagged_dish_count' => count($source['tagMap']),
        'source_sha' => $source['digest'], 'stale_actor_count' => 0,
        'http_sent' => false, 'dml_sent' => false], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
if ($mode === '--audit') {
    if ((string)($auditSaved['source_sha'] ?? '') !== $source['digest']) {
        throw new RuntimeException('Audit journal/source drift');
    }
    $zero = $db->prepare('SELECT COUNT(*) FROM YDY_USER WHERE UUID=? OR ACCOUNT=?');
    $zero->execute([$auditSaved['marker'], $auditSaved['marker']]);
    if ((int)$zero->fetchColumn() !== 0) {
        throw new RuntimeException('Temporary actor remains; no broad cleanup');
    }
    if (!unlink($journal) || !rmdir($private)) {
        throw new RuntimeException('Exact private journal removal failed');
    }
    echo json_encode(['status' => 'independent_zero_pass', 'lane' => 'C',
        'actor_zero' => true, 'source_hash_restored' => true,
        'journal_removed' => true, 'http_sent' => false, 'dml_sent' => false]), PHP_EOL;
    exit(0);
}
if ($actorCount !== 0 || is_dir($private)
    || (string)getenv('CDX_EM_RIGHT_SOURCE_SHA') !== $source['digest']) {
    throw new RuntimeException('C execute gate not satisfied');
}
if (!mkdir($private, 0700) || !chmod($private, 0700)) {
    throw new RuntimeException('Private recovery directory unavailable');
}
$marker = 'CDXEMRP' . strtoupper(bin2hex(random_bytes(6)));
$saved = ['marker' => $marker, 'actor_id' => 0,
    'date' => $source['date'], 'meal' => $source['meal'],
    'source_sha' => $source['digest'], 'phase' => 'before_actor_insert'];
$writeJournal = static function () use ($journal, &$saved): void {
    if (file_put_contents($journal, json_encode($saved), LOCK_EX) === false
        || !chmod($journal, 0600)) {
        throw new RuntimeException('Private recovery journal write failed');
    }
};
$writeJournal();
$result = ['lane' => 'C', 'status' => 'FAIL',
    'right_source_count' => count($source['rightDishes'])];
$failure = null;
try {
    $insert = $db->prepare('INSERT INTO YDY_USER'
        . ' (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]);
    $actor = $db->prepare('SELECT ID FROM YDY_USER'
        . ' WHERE UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
    $actor->execute([$marker, $marker, 'CODEX']);
    $saved['actor_id'] = (int)$actor->fetchColumn();
    if ($saved['actor_id'] <= 0) throw new RuntimeException('Actor readback failed');
    $saved['phase'] = 'actor_inserted';
    $writeJournal();
    require_once $root . '/application/base/Security.php';
    $token = \app\base\Security::encrypt(implode("\t", [
        (string)$saved['actor_id'], $marker, $marker, '0', '', '', $marker,
    ]));
    if (!is_string($token) || $token === '') throw new RuntimeException('Token unavailable');
    $saved['phase'] = 'http_started';
    $writeJournal();
    $body = @file_get_contents($base . '/p/electronicMenu/previewData', false,
        stream_context_create(['http' => [
            'method' => 'POST',
            'header' => "Content-Type: application/x-www-form-urlencoded\r\ntoken: " . $token . "\r\n",
            'content' => http_build_query([
                'display_mode' => 2, 'left_restaurant_id' => $source['oldRight'],
                'right_restaurant_id' => $source['oldLeft'], 'carousel_interval' => 8,
            ], '', '&', PHP_QUERY_RFC3986),
            'timeout' => 12, 'ignore_errors' => true,
        ]]));
    $headers = $http_response_header ?? [];
    preg_match('/HTTP\/\S+\s+(\d+)/', (string)($headers[0] ?? ''), $match);
    $response = is_string($body) ? json_decode($body, true) : null;
    if ((int)($match[1] ?? 0) !== 200 || !is_array($response)
        || ($response['code'] ?? null) !== 0 || !is_array($response['data'] ?? null)) {
        throw new RuntimeException('C 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) !== $source['date']
        || ($data['context']['meal_times'] ?? null) !== $source['meal']
        || ($stalls[0]['position'] ?? null) !== 'left'
        || ($stalls[1]['position'] ?? null) !== 'right'
        || ($stalls[0]['restaurant_id'] ?? null) !== $source['oldRight']
        || ($stalls[1]['restaurant_id'] ?? null) !== $source['oldLeft']
        || !is_array($stalls[0]['dishes'] ?? null)
        || $stalls[0]['dishes'] !== []
        || !is_array($stalls[1]['dishes'] ?? null)
        || count($stalls[1]['dishes']) !== count($source['rightDishes'])) {
        throw new RuntimeException('C right preview shape/source mismatch');
    }
    foreach ($source['rightDishes'] as $index => $raw) {
        $dish = $stalls[1]['dishes'][$index] ?? null;
        if (!is_array($dish)
            || ($dish['uuid'] ?? null) !== $raw['uuid']
            || ($dish['name'] ?? null) !== $raw['name']
            || ($dish['price'] ?? null) !== number_format((float)$raw['price'], 2, '.', '')
            || !is_string($dish['image_url'] ?? null)
            || !is_array($dish['health_tags'] ?? null)) {
            throw new RuntimeException('C right preview dish/source mismatch');
        }
        foreach (['carbohydrate', 'protein', 'fat'] as $field) {
            if (!is_string($dish[$field] ?? null)
                || $dish[$field] !== number_format((float)$raw[$field], 2, '.', '')) {
                throw new RuntimeException('C right preview nutrient type/value mismatch');
            }
        }
        // getImgFullPath() normalizes backslashes in prefix + source path.
        $path = str_replace('\\', '/', (string)$raw['file_path']);
        if (($path === '' && $dish['image_url'] !== '')
            || ($path !== '' && substr($dish['image_url'], -strlen($path)) !== $path)
            || $dish['health_tags'] !== ($source['tagMap'][(int)$raw['dish_id']] ?? [])) {
            throw new RuntimeException('C right preview image/tag mismatch');
        }
    }
    $after = $readSource();
    if ($after['digest'] !== $source['digest']) {
        throw new RuntimeException('C source changed during preview');
    }
    $result += ['http_code' => 200, 'business_code' => 0,
        'right_json_count' => count($stalls[1]['dishes']),
        'left_empty' => true, 'right_source_match' => true,
        'money_nutrients_image_tags_match' => true,
        'source_hash_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]),
        ]))];
    $result['status'] = 'HTTP_PASS_PENDING_INDEPENDENT_AUDIT';
} catch (Throwable $error) {
    $failure = get_class($error) . ':' . (string)$error->getCode();
} finally {
    try {
        $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('Actor exact delete count invalid');
        }
        $saved['phase'] = 'actor_deleted';
        $writeJournal();
    } catch (Throwable $cleanupError) {
        $failure = ($failure ? $failure . ';' : '') . 'cleanup:'
            . get_class($cleanupError) . ':' . (string)$cleanupError->getCode();
    }
}
if ($failure !== null) {
    $result['status'] = 'FAIL_AUDIT_REQUIRED';
    $result['redacted_failure'] = $failure;
}
$result['actor_cleanup_attempted'] = true;
$result['independent_audit_required'] = true;
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($failure === null ? 0 : 1);
