<?php
declare(strict_types=1);

$stage = 'default_deny';
if (!hash_equals('CDX_CAT8F_RUNNER_20261002_V1',
    (string)getenv('CDX_CAT8F_APPROVAL'))) {
    echo json_encode(['status' => 'default_deny', 'database_connected' => false,
        'cache_accessed' => false, 'business_http_sent' => false,
        'dml_sent' => false]), PHP_EOL;
    exit(78);
}
$redactedError = static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'redacted_error', 'stage' => $stage,
        'class' => get_class($error), 'business_pass' => false]), PHP_EOL;
    exit(2);
};
set_exception_handler($redactedError);
set_error_handler(static function (int $severity): bool {
    if (!(error_reporting() & $severity)) return false;
    throw new ErrorException('redacted_warning', 0, $severity);
});
$stage = 'source_review';
require __DIR__ . '/common.php';
cat8f_approved('CDX_CAT8F_RUNNER_20261002_V1', __FILE__);
require __DIR__ . '/contract.php';
$data = cat8f_manifest();
$target = (string)getenv('CDX_CAT8F_TARGET');
if ($target !== $data['target'] || ($data['status'] ?? null) !== 'ready') {
    throw new RuntimeException('Ready exact-target fixture required');
}
$stage = 'isolated_runtime_and_account';
try {
    $ctx = cat8_context($target);
} catch (Throwable $error) {
    $redactedError($error);
}
set_exception_handler($redactedError);
if ($ctx['schema'] !== $data['schema']) throw new RuntimeException('Schema drift');
/** @var PDO $db */
$db = $ctx['pdo'];
$rows = cat8f_operator($db, $data['user_name']);
$userId = (int)($data['store_user_id'] ?? 0);
$cache = cat8f_cache_state($data);
if (count($rows) !== 1 || $userId <= 0 || !cat8f_owned($rows[0], $data)
    || array_sum(cat8f_refs($db, $userId)) !== 0
    || !($cache['live'] ?? false) || !($cache['ttl_valid'] ?? false)
    || ($cache['session'] ?? null) !== cat8f_session($data, $userId)
    || (int)$data['expires_at'] <= time() + 80) {
    throw new RuntimeException('Independent synthetic account/session gate failed');
}

$stage = 'natural_dish_food_fixture';
$candidateSql = 'SELECT d.ID AS dish_id,d.NAME AS dish_name,d.CATEGORY AS dish_category,'
    . ' d.FILE_ID AS file_id,d.COOK_METHOD AS cook_method,'
    . ' f.ID AS food_id,f.NAME AS food_name,f.CATEGORY AS food_category,'
    . ' df.FOODS_WEIGHT AS original_foods_weight,'
    . ' df.FOODS_RATIO AS original_foods_ratio,'
    . ' dc.NAME AS dish_category_name,fc.NAME AS food_category_name'
    . ' FROM yoshop_dishes d INNER JOIN yoshop_dishes_foods df ON df.DISHES_ID=d.ID'
    . ' INNER JOIN yoshop_foods f ON f.ID=df.FOODS_ID'
    . ' INNER JOIN yoshop_dishes_cate dc ON dc.ID=d.CATEGORY'
    . ' INNER JOIN yoshop_foods_cate fc ON fc.ID=f.CATEGORY'
    . ' WHERE d.STORE_ID=10001 AND d.IS_DELETE=0 AND d.USER_DELETE=0'
    . ' AND df.STORE_ID=10001 AND df.IS_DELETE=0'
    . ' AND f.STORE_ID=10001 AND f.IS_DELETE=0 ORDER BY d.ID,df.ID';
$candidate = $db->query($candidateSql)->fetch(PDO::FETCH_ASSOC);
if (!is_array($candidate) || (int)$candidate['dish_id'] <= 0
    || (int)$candidate['food_id'] <= 0) {
    throw new RuntimeException('Same-store natural dish-food-category fixture unavailable');
}
// This DM PDO session returns legacy GBK bytes for Chinese source names.
// The real frontend sends UTF-8 JSON; convert only test request/expectation
// strings, and require a lossless round trip. Never rewrite source rows.
if ($target === 'C') {
    foreach (['dish_name', 'food_name', 'dish_category_name',
        'food_category_name'] as $field) {
        $rawName = $candidate[$field] ?? null;
        $utf8Name = is_string($rawName) ? @iconv('GBK', 'UTF-8', $rawName) : false;
        $roundtrip = is_string($utf8Name)
            ? @iconv('UTF-8', 'GBK', $utf8Name) : false;
        if (!is_string($utf8Name) || !mb_check_encoding($utf8Name, 'UTF-8')
            || $roundtrip !== $rawName) {
            throw new RuntimeException('Natural name cannot be losslessly encoded for frontend JSON');
        }
        $candidate[$field] = $utf8Name;
    }
}
$dishId = (int)$candidate['dish_id'];
$foodId = (int)$candidate['food_id'];
$dishCateId = (int)$candidate['dish_category'];
$foodCateId = (int)$candidate['food_category'];
$scope = [
    'yoshop_dishes' => ['ID=? AND STORE_ID=10001', [$dishId], 'ID'],
    'yoshop_foods' => ['ID=? AND STORE_ID=10001', [$foodId], 'ID'],
    'yoshop_dishes_foods' => ['DISHES_ID=? AND STORE_ID=10001', [$dishId], 'ID'],
    'yoshop_dishes_cate' => ['ID=?', [$dishCateId], 'ID'],
    'yoshop_foods_cate' => ['ID=?', [$foodCateId], 'ID'],
    'yoshop_dishes_unsuitable' => ['DISHES_ID=? AND STORE_ID=10001', [$dishId], 'ID'],
    'yoshop_dishes_tag' => ['DISHES_ID=? AND STORE_ID=10001', [$dishId], 'ID'],
    'yoshop_config_enum' => ['ENUM_TYPE=? AND ENUM_KEY=?',
        ['4', (string)$candidate['cook_method']], 'ID'],
];
if ((int)$candidate['file_id'] > 0) {
    $scope['yoshop_upload_file'] = ['FILE_ID=? AND STORE_ID=10001',
        [(int)$candidate['file_id']], 'FILE_ID'];
}
$fingerprint = static function (PDO $pdo, array $targets): array {
    $out = [];
    foreach ($targets as $table => [$where, $bind, $pk]) {
        $statement = $pdo->prepare('SELECT * FROM ' . $table . ' WHERE ' . $where
            . ' ORDER BY ' . $pk);
        $statement->execute($bind);
        $hash = hash_init('sha256');
        $count = 0;
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            ksort($row);
            $encoded = json_encode($row, JSON_UNESCAPED_UNICODE
                | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
            if (!is_string($encoded)) throw new RuntimeException('Unhashable source row');
            hash_update($hash, $encoded . "\n");
            $count++;
        }
        $out[$table] = ['rows' => $count, 'sha256' => hash_final($hash)];
    }
    return $out;
};
$before = $fingerprint($db, $scope);
foreach (['yoshop_dishes', 'yoshop_foods', 'yoshop_dishes_cate',
    'yoshop_foods_cate'] as $table) {
    if ($before[$table]['rows'] !== 1) throw new RuntimeException('Natural row not exact one');
}
if ($before['yoshop_dishes_foods']['rows'] < 1) {
    throw new RuntimeException('Natural dish-food pivot missing');
}

// These are the eight current ai_store axios POST readers. null means no body.
$cases = [
    ['dish_list', '/store/dishes.dishes/list',
        ['search' => $candidate['dish_name']], ['list', 'data'], 'dish_row'],
    ['dish_detail', '/store/dishes.dishes/detail',
        ['id' => $dishId], ['detail'], 'dish_detail'],
    ['dish_cate', '/store/dishes.dishes/cate', null, ['list'], 'dish_tree'],
    ['dish_library', '/store/dishes.dishes/dishLibrary',
        ['name' => $candidate['dish_name']], ['list'], 'dish_row'],
    ['food_list', '/store/dishes.foods/list',
        ['search' => $candidate['food_name']], ['list', 'data'], 'food_row'],
    ['food_detail', '/store/dishes.foods/detail',
        ['id' => $foodId], ['detail'], 'food_detail'],
    ['food_all', '/store/dishes.foods/all',
        ['search' => $candidate['food_name']], ['list'], 'food_row'],
    ['food_cate', '/store/dishes.foods/cate', null, ['list'], 'food_tree'],
];
$results = [];
$failure = null;
$stage = 'eight_frontend_json_post_requests';
foreach ($cases as [$name, $path, $form, $payloadPath, $kind]) {
    if ((int)$data['expires_at'] <= time() + 70) {
        $failure = 'ttl_requires_renew';
        break;
    }
    $body = $form === null ? '' : json_encode($form,
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
    $headers = 'Access-Token: ' . $data['token'] . "\r\nAccept: application/json\r\n";
    if ($form !== null) {
        $headers .= "Content-Type: application/json;charset=UTF-8\r\n";
    }
    $context = stream_context_create(['http' => [
        'method' => 'POST', 'timeout' => 60, 'ignore_errors' => true,
        'header' => $headers, 'content' => $body,
    ]]);
    $started = microtime(true);
    try {
        $raw = file_get_contents('http://127.0.0.1:' . $ctx['port']
            . $path . '?storeId=10001', false, $context);
    } catch (Throwable $error) {
        $raw = false;
    }
    $elapsed = (int)round((microtime(true) - $started) * 1000);
    $statusLine = $http_response_header[0] ?? '';
    $http = preg_match('/HTTP\/\S+\s+(\d+)/', $statusLine, $match)
        ? (int)$match[1] : 0;
    $json = is_string($raw) ? json_decode($raw, true) : null;
    $payload = is_array($json) ? ($json['data'] ?? null) : null;
    foreach ($payloadPath as $part) {
        $payload = is_array($payload) ? ($payload[$part] ?? null) : null;
    }
    $selected = null;
    $nestedFoodTypes = [];
    if ($kind === 'dish_row' || $kind === 'food_row') {
        $selected = cat8f_row($payload,
            $kind === 'dish_row' ? $dishId : $foodId,
            (string)$candidate[$kind === 'dish_row' ? 'dish_name' : 'food_name']);
    } elseif ($kind === 'dish_detail' || $kind === 'food_detail') {
        $expectedId = $kind === 'dish_detail' ? $dishId : $foodId;
        $expectedName = (string)$candidate[$kind === 'dish_detail' ? 'dish_name' : 'food_name'];
        $selected = is_array($payload)
            && isset($payload['id'], $payload['name'])
            && (string)$payload['id'] === (string)$expectedId
            && (string)$payload['name'] === $expectedName ? $payload : null;
        if ($kind === 'dish_detail' && $selected !== null) {
            $linkedFood = false;
            foreach (($selected['foods'] ?? []) as $food) {
                if (is_array($food) && isset($food['foods_id'])
                    && (string)$food['foods_id'] === (string)$foodId
                    && isset($food['foods_weight'], $food['foods_ratio'])
                    && is_numeric($food['foods_weight'])
                    && is_numeric($food['foods_ratio'])
                    && abs((float)$food['foods_weight']
                        - (float)$candidate['original_foods_weight']) < 0.001
                    && abs((float)$food['foods_ratio']
                        - (float)$candidate['original_foods_ratio']) < 0.001) {
                    $linkedFood = true;
                    $nestedFoodTypes = [
                        'foods_weight' => gettype($food['foods_weight']),
                        'foods_ratio' => gettype($food['foods_ratio']),
                    ];
                }
            }
            if (!$linkedFood) $selected = null;
        }
    } else {
        $found = cat8f_tree($payload,
            $kind === 'dish_tree' ? $dishCateId : $foodCateId,
            (string)$candidate[$kind === 'dish_tree'
                ? 'dish_category_name' : 'food_category_name']);
        $selected = $found ? ['category_tree_reaches_original_id' => true] : null;
    }
    $types = [];
    if (is_array($selected)) {
        foreach ($selected as $key => $value) $types[(string)$key] = gettype($value);
        ksort($types);
    }
    if (is_array($json)) unset($json['trace_id']);
    $results[$name] = ['http' => $http,
        'business_status' => is_array($json) ? ($json['status'] ?? null) : null,
        'elapsed_ms' => $elapsed, 'original_row_exact_match' => $selected !== null,
        'selected_field_types' => $types,
        'nested_food_relation_types' => $nestedFoodTypes,
        'normalized_response_sha256' => is_array($json)
            ? hash('sha256', json_encode($json, JSON_UNESCAPED_UNICODE
                | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE)) : null];
    if ($http !== 200 || !is_array($json) || (int)($json['status'] ?? 0) !== 200
        || $selected === null || $elapsed >= 60000) {
        $failure = $name;
        break;
    }
}

$stage = 'selected_original_tables_unchanged';
$after = $fingerprint($db, $scope);
$accountAfter = cat8f_operator($db, $data['user_name']);
$unchanged = $before === $after && count($accountAfter) === 1
    && cat8f_owned($accountAfter[0], $data)
    && array_sum(cat8f_refs($db, $userId)) === 0;
echo json_encode(['status' => $failure === null && $unchanged
        ? 'eight_positive_read_probes_not_full_contract' : 'batch_red_or_blocked',
    'target' => $target, 'request_encoding' => 'frontend_json',
    'business_http_count' => count($results), 'cases' => $results,
    'first_failure' => $failure, 'selected_source_rows_unchanged' => $unchanged,
    'source_scope_row_counts' => array_map(static function (array $item): int {
        return $item['rows'];
    }, $after), 'dml_sent' => false, 'full_contract_pass' => false]), PHP_EOL;
if ($failure !== null || !$unchanged || count($results) !== 8) exit(3);
