<?php

declare(strict_types=1);

// Synthetic-only A/B/C evaluation boundaries. Each write scenario is independently cleaned.
$root = (string)getenv('POC_TARGET_ROOT');
$baseUrl = rtrim((string)getenv('POC_BASE_URL'), '/');
$dialect = (string)getenv('POC_DIALECT');
$label = (string)getenv('POC_LABEL');
$ownId = (int)getenv('POC_ORDER_ID');
$ownMarker = (string)getenv('POC_ORDER_MARKER');
$otherId = (int)getenv('POC_OTHER_ORDER_ID');
$otherMarker = (string)getenv('POC_OTHER_ORDER_MARKER');
$scenario = (string)getenv('POC_SCENARIO');
$validScenarios = ['preflight', 'readonly', 'empty_detail', 'missing_detail',
    'missing_environment_score', 'missing_service_score', 'comment_500', 'comment_501',
    'comment_65535', 'comment_65536', 'detail_comment_65536', 'unicode4', 'html_unicode4'];
if (!in_array($label, ['A', 'B', 'C'], true) || !in_array($dialect, ['mysql', 'dm'], true)
    || !in_array($scenario, $validScenarios, true)
    || !preg_match('#^http://127\.0\.0\.1:[0-9]{2,5}$#D', $baseUrl)
    || !preg_match('/^CDXPAY[A-F0-9]{8}$/D', $ownMarker)
    || !preg_match('/^CDXPAY[A-F0-9]{8}$/D', $otherMarker)
    || $ownId <= 0 || $otherId <= 0 || $ownId === $otherId
    || !is_file($root . '/vendor/autoload.php')) {
    throw new RuntimeException('Invalid isolated evaluation boundary fixture');
}
if ($dialect === 'mysql') {
    $localFile = (string)getenv('POC_MYSQL_LOCAL_ENV');
    if (basename($localFile) !== '.env.local') throw new RuntimeException('Only ai_api .env.local is allowed');
    $local = parse_ini_file($localFile, true, INI_SCANNER_RAW);
    if (!is_array($local) || !isset($local['DATABASE'], $local['DATABASE_ZHCT'])) {
        throw new RuntimeException('Development database config unavailable');
    }
    foreach (['DATABASE' => 'AI_', 'DATABASE_ZHCT' => ''] as $section => $prefix) {
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
            'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $source => $target) {
            putenv('POC_' . $prefix . $target . '=' . (string)$local[$section][$source]);
        }
    }
}
foreach (['HOST', 'PORT', 'SCHEMA', 'USER', 'PASSWORD',
    'AI_HOST', 'AI_PORT', 'AI_SCHEMA', 'AI_USER', 'AI_PASSWORD'] as $part) {
    if (getenv('POC_' . $part) === false) throw new RuntimeException('Isolated database environment incomplete');
}
$boundary = json_decode((string)file_get_contents($baseUrl . '/__poc_boundary'), true);
if (empty($boundary['external_stubs'])) throw new RuntimeException('External stubs inactive');
define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName('poc')->initialize();
$builderSha = null;
$requestSha = null;
if ($dialect === 'dm') {
    $builderFile = (new ReflectionClass(\app\common\database\DmBuilder::class))->getFileName();
    $builderSha = hash_file('sha256', $builderFile);
    $requestFile = (new ReflectionClass(\cores\Request::class))->getFileName();
    $requestSha = hash_file('sha256', $requestFile);
}

$dsn = static function (string $prefix) use ($dialect): string {
    $host = getenv('POC_' . $prefix . 'HOST');
    $port = getenv('POC_' . $prefix . 'PORT');
    $schema = getenv('POC_' . $prefix . 'SCHEMA');
    return $dialect === 'dm' ? "dm:host=$host;port=$port;schema=$schema"
        : "mysql:host=$host;port=$port;dbname=$schema;charset=utf8mb4";
};
$store = new PDO($dsn(''), getenv('POC_USER'), getenv('POC_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 4]);
$ai = new PDO($dsn('AI_'), getenv('POC_AI_USER'), getenv('POC_AI_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 4]);
$t = static function (string $name, bool $isAi = false) use ($dialect): string {
    $raw = ($isAi ? 'yoshop_' : 'ydy_') . $name;
    return $dialect === 'dm' ? strtoupper($raw) : $raw;
};
$sOrder = $t('meal_order');
$sUser = $t('user');
$sMenu = $t('initial_menu');
$sEval = $t('order_evaluate');
$sDetail = $t('order_evaluate_detail');
$aUser = $t('user', true);
$comment = $dialect === 'dm' ? '"COMMENT"' : '`COMMENT`';
$schemaSql = $dialect === 'dm'
    ? "SELECT TABLE_NAME,DATA_TYPE,DATA_LENGTH FROM USER_TAB_COLUMNS WHERE TABLE_NAME IN ('YDY_ORDER_EVALUATE','YDY_ORDER_EVALUATE_DETAIL') AND COLUMN_NAME='COMMENT'"
    : "SELECT TABLE_NAME,COLUMN_TYPE AS DATA_TYPE,CHARACTER_MAXIMUM_LENGTH AS DATA_LENGTH FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME IN ('ydy_order_evaluate','ydy_order_evaluate_detail') AND COLUMN_NAME='comment'";
$schemaStatement = $store->prepare($schemaSql);
$schemaStatement->execute($dialect === 'dm' ? [] : [getenv('POC_SCHEMA')]);
$commentSchema = array_map(static function (array $row): array {
    $row = array_change_key_case($row, CASE_LOWER);
    return ['table' => strtolower((string)$row['table_name']),
        'type' => strtoupper((string)$row['data_type']),
        'length' => $row['data_length'] === null ? null : (int)$row['data_length']];
}, $schemaStatement->fetchAll(PDO::FETCH_ASSOC));
$read = static function (PDO $pdo, string $sql, array $params = []): array {
    $q = $pdo->prepare($sql); $q->execute($params);
    return array_change_key_case($q->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
};
$all = static function (PDO $pdo, string $sql, array $params = []): array {
    $q = $pdo->prepare($sql); $q->execute($params);
    return array_map(static function (array $row): array {
        return array_change_key_case($row, CASE_LOWER);
    }, $q->fetchAll(PDO::FETCH_ASSOC));
};
$count = static function (PDO $pdo, string $sql, array $params = []): int {
    $q = $pdo->prepare($sql); $q->execute($params); return (int)$q->fetchColumn();
};
$text = static function ($value): string {
    if (is_resource($value)) return (string)stream_get_contents($value);
    return (string)$value;
};
$snapshot = static function (int $id, string $marker) use ($store, $ai, $read, $count,
    $sOrder, $sUser, $aUser, $sEval, $sDetail): array {
    $order = $read($store, "SELECT ID,MSGID,ORDER_NO,USER_ID,ORDER_STATUS,EVALUATE_STATUS,EVALUATE_ID,UPDATE_TIME"
        . " FROM $sOrder WHERE ID=?", [$id]);
    $user = $read($store, "SELECT ID,UUID,ACCOUNT,AI_USER_ID FROM $sUser WHERE ID=?",
        [(int)($order['user_id'] ?? 0)]);
    $aiUser = $read($ai, "SELECT USER_ID,STORE_ID,IS_DELETE FROM $aUser WHERE USER_ID=?",
        [(int)($user['ai_user_id'] ?? 0)]);
    $evalCount = $count($store, "SELECT COUNT(*) FROM $sEval WHERE MEAL_ORDER_ID=?", [$id]);
    $detailCount = $count($store, "SELECT COUNT(*) FROM $sDetail WHERE MEAL_ORDER_ID=?", [$id]);
    if (!$order || (string)$order['msgid'] !== $marker || (int)$order['order_status'] !== 30
        || (int)$order['evaluate_status'] !== 1 || (int)$order['evaluate_id'] !== 0
        || !$user || (string)$user['account'] !== $marker
        || strpos((string)$user['uuid'], 'CDXUSER-') !== 0
        || !$aiUser || (int)$aiUser['store_id'] !== 10001 || (int)$aiUser['is_delete'] !== 0
        || $evalCount !== 0 || $detailCount !== 0) {
        throw new RuntimeException('Synthetic order state not clean for boundary test');
    }
    return ['order' => $order, 'user' => $user, 'eval_count' => $evalCount,
        'detail_count' => $detailCount];
};
$own = $snapshot($ownId, $ownMarker);
$other = $snapshot($otherId, $otherMarker);
$menu = $read($store, "SELECT DISHES_UUID FROM $sMenu WHERE MEAL_ORDER_ID=? ORDER BY ID", [$ownId]);
if (!$menu || (int)$own['user']['ai_user_id'] === (int)$other['user']['ai_user_id']) {
    throw new RuntimeException('Synthetic cross-user menu/identity guard failed');
}
$preflight = ['label' => $label, 'dialect' => $dialect, 'scenario' => $scenario,
    'dm_builder_sha256' => $builderSha,
    'request_sha256' => $requestSha,
    'own_order_id' => $ownId, 'other_order_id' => $otherId,
    'own_original_status' => (int)$own['order']['evaluate_status'],
    'own_original_pointer' => (int)$own['order']['evaluate_id'],
    'other_original_status' => (int)$other['order']['evaluate_status'],
    'other_original_pointer' => (int)$other['order']['evaluate_id'],
    'both_synthetic_and_unevaluated' => true, 'mapped_menu' => true,
    'external_stubs' => true, 'comment_schema' => $commentSchema];
if ($scenario === 'preflight') {
    echo json_encode(['phase' => 'preflight', 'result' => $preflight],
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
    exit(0);
}

$request = static function (string $method, string $path, ?string $token, array $body = []) use ($baseUrl): array {
    $headers = "platform: POC\r\nContent-Type: application/json\r\n";
    if ($token !== null) $headers .= 'Access-Token: ' . $token . "\r\n";
    $options = ['method' => $method, 'header' => $headers, 'timeout' => 35, 'ignore_errors' => true];
    if ($method !== 'GET') $options['content'] = json_encode($body, JSON_UNESCAPED_UNICODE);
    $raw = file_get_contents($baseUrl . $path, false, stream_context_create(['http' => $options]));
    $statusLine = $http_response_header[0] ?? '';
    $data = is_string($raw) ? json_decode($raw, true) : null;
    if (!is_array($data)) throw new RuntimeException('Non-JSON HTTP response');
    unset($data['trace_id']);
    return ['http' => (int)(preg_match('/HTTP\/\S+\s+(\d+)/', $statusLine, $m) ? $m[1] : 0),
        'status' => (int)($data['status'] ?? -1), 'message' => (string)($data['message'] ?? ''),
        'data' => $data['data'] ?? null];
};
$reviewMarker = 'CDXPAREVAL' . strtoupper(bin2hex(random_bytes(4)));
$token = $reviewMarker . 'TOKEN' . strtoupper(bin2hex(random_bytes(12)));
$cleanup = static function (int $id, string $marker, array $original) use ($store, $read, $all, $count,
    $text, $sOrder, $sEval, $sDetail, $comment): array {
    $evaluations = $all($store, "SELECT ID,USER_ID,ORDER_NO,$comment FROM $sEval WHERE MEAL_ORDER_ID=?", [$id]);
    $now = $read($store, "SELECT EVALUATE_STATUS,EVALUATE_ID,UPDATE_TIME FROM $sOrder WHERE ID=?", [$id]);
    if (count($evaluations) > 1) throw new RuntimeException('Multiple synthetic-order evaluations; preserve for review');
    $ownedId = 0;
    $detailRows = [];
    if ($evaluations) {
        $evaluation = $evaluations[0];
        if ((int)$evaluation['user_id'] !== (int)$original['user']['id']
            || (string)$evaluation['order_no'] !== (string)$original['order']['order_no']
            || strpos($text($evaluation['comment']), $marker) !== 0) {
            throw new RuntimeException('Evaluation is not marker-owned; preserve for review');
        }
        $ownedId = (int)$evaluation['id'];
        $detailRows = $all($store, "SELECT ID,$comment FROM $sDetail WHERE EVALUATE_ID=?", [$ownedId]);
        foreach ($detailRows as $detail) {
            if (strpos($text($detail['comment']), $marker) !== 0) {
                throw new RuntimeException('Detail is not marker-owned; preserve for review');
            }
        }
    }
    $isOriginal = (int)($now['evaluate_status'] ?? -1) === (int)$original['order']['evaluate_status']
        && (int)($now['evaluate_id'] ?? -1) === (int)$original['order']['evaluate_id'];
    $isOwned = $ownedId > 0 && (int)($now['evaluate_status'] ?? -1) === 2
        && (int)($now['evaluate_id'] ?? -1) === $ownedId;
    if (!$isOriginal && !$isOwned) throw new RuntimeException('Order pointer not owned; preserve for review');
    if ($ownedId > 0) {
        $store->beginTransaction();
        try {
            if ($isOwned) {
                $update = $store->prepare("UPDATE $sOrder SET EVALUATE_STATUS=?,EVALUATE_ID=?,UPDATE_TIME=?"
                    . ' WHERE ID=? AND EVALUATE_STATUS=2 AND EVALUATE_ID=?');
                $update->execute([(int)$original['order']['evaluate_status'], (int)$original['order']['evaluate_id'],
                    (string)$original['order']['update_time'], $id, $ownedId]);
                if ($update->rowCount() !== 1) throw new RuntimeException('Order restore count mismatch');
            }
            $deleteDetail = $store->prepare("DELETE FROM $sDetail WHERE EVALUATE_ID=? AND MEAL_ORDER_ID=?");
            $deleteDetail->execute([$ownedId, $id]);
            if ($deleteDetail->rowCount() !== count($detailRows)) {
                throw new RuntimeException('Detail delete count mismatch');
            }
            $deleteEval = $store->prepare("DELETE FROM $sEval WHERE ID=? AND MEAL_ORDER_ID=? AND USER_ID=?");
            $deleteEval->execute([$ownedId, $id, (int)$original['user']['id']]);
            if ($deleteEval->rowCount() !== 1) throw new RuntimeException('Evaluation delete count mismatch');
            $store->commit();
        } catch (Throwable $error) {
            if ($store->inTransaction()) $store->rollBack();
            throw $error;
        }
    }
    $final = $read($store, "SELECT EVALUATE_STATUS,EVALUATE_ID,UPDATE_TIME FROM $sOrder WHERE ID=?", [$id]);
    $result = [
        'evaluations_zero' => $count($store, "SELECT COUNT(*) FROM $sEval WHERE MEAL_ORDER_ID=?", [$id]) === 0,
        'details_zero' => $count($store, "SELECT COUNT(*) FROM $sDetail WHERE MEAL_ORDER_ID=?", [$id]) === 0,
        'status_pointer_restored' => (int)($final['evaluate_status'] ?? -1) === (int)$original['order']['evaluate_status']
            && (int)($final['evaluate_id'] ?? -1) === (int)$original['order']['evaluate_id'],
        'update_time_restored' => (string)($final['update_time'] ?? '') === (string)$original['order']['update_time'],
    ];
    if (in_array(false, $result, true)) throw new RuntimeException('Cleanup verification incomplete');
    return $result;
};

$output = ['preflight' => $preflight, 'marker' => $reviewMarker, 'result' => [], 'cleanup' => []];
$runError = null;
$cleanupError = null;
$tokenCreated = false;
try {
    \think\facade\Cache::set($token, ['user' => ['user_id' => (int)$own['user']['ai_user_id']],
        'password_version' => 0, 'login_type' => 'thirdparty'], 600);
    $tokenCreated = true;
    $base = '?storeId=10001';
    $form = ['orderId' => $ownId, 'environment_score' => 5, 'service_score' => 4,
        'comment' => $reviewMarker, 'detail' => [[
            'dishes_uuid' => (string)$menu['dishes_uuid'], 'dishes_score' => 5,
            'dishes_taste' => [], 'comment' => $reviewMarker,
        ]]];
    if ($scenario === 'readonly') {
        $crossGet = $request('GET', '/api/mealOrder/evaluate' . $base . '&orderId=' . $otherId, $token);
        $crossForm = $form;
        $crossForm['orderId'] = $otherId;
        $crossSubmit = $request('POST', '/api/mealOrder/evaluateSubmit' . $base, $token,
            ['form' => $crossForm]);
        $missingId = $form;
        unset($missingId['orderId']);
        $missing = $request('POST', '/api/mealOrder/evaluateSubmit' . $base, $token,
            ['form' => $missingId]);
        $output['result'] = [
            'cross_get' => ['http' => $crossGet['http'], 'status' => $crossGet['status']],
            'cross_submit' => ['http' => $crossSubmit['http'], 'status' => $crossSubmit['status']],
            'missing_order_id' => ['http' => $missing['http'], 'status' => $missing['status']],
        ];
    } else {
        if ($scenario === 'empty_detail') $form['detail'] = [];
        if ($scenario === 'missing_detail') unset($form['detail']);
        if ($scenario === 'missing_environment_score') unset($form['environment_score']);
        if ($scenario === 'missing_service_score') unset($form['service_score']);
        if ($scenario === 'comment_500' || $scenario === 'comment_501'
            || $scenario === 'comment_65535' || $scenario === 'comment_65536') {
            $length = (int)substr($scenario, strlen('comment_'));
            $form['comment'] = $reviewMarker . str_repeat('X', $length - strlen($reviewMarker));
            $form['detail'][0]['comment'] = $reviewMarker;
        }
        if ($scenario === 'unicode4') {
            $form['comment'] = $reviewMarker . '中文🧪';
            $form['detail'][0]['comment'] = $form['comment'];
        }
        if ($scenario === 'detail_comment_65536') {
            $form['detail'][0]['comment'] = $reviewMarker
                . str_repeat('X', 65536 - strlen($reviewMarker));
        }
        if ($scenario === 'html_unicode4') {
            $form['comment'] = $reviewMarker . '<b>中文🧪</b>';
            $form['detail'][0]['comment'] = $form['comment'];
        }
        $expectedPersistedComment = $scenario === 'html_unicode4'
            ? htmlspecialchars((string)$form['comment'], ENT_COMPAT) : (string)$form['comment'];
        $result = $request('POST', '/api/mealOrder/evaluateSubmit' . $base, $token,
            ['form' => $form]);
        $evalRows = $all($store, "SELECT ID,ENVIRONMENT_SCORE,SERVICE_SCORE,$comment FROM $sEval WHERE MEAL_ORDER_ID=?",
            [$ownId]);
        $current = $read($store, "SELECT EVALUATE_STATUS,EVALUATE_ID FROM $sOrder WHERE ID=?", [$ownId]);
        $detailCount = $count($store, "SELECT COUNT(*) FROM $sDetail WHERE MEAL_ORDER_ID=?", [$ownId]);
        $storedComment = $evalRows ? $text($evalRows[0]['comment']) : '';
        $storedDetail = $evalRows ? $read($store,
            "SELECT $comment FROM $sDetail WHERE EVALUATE_ID=? ORDER BY ID", [(int)$evalRows[0]['id']]) : [];
        $storedDetailComment = $storedDetail ? $text($storedDetail['comment']) : '';
        $expectedDetailComment = isset($form['detail'][0]['comment']) ? (string)$form['detail'][0]['comment'] : null;
        if ($scenario === 'html_unicode4' && $expectedDetailComment !== null) {
            $expectedDetailComment = htmlspecialchars($expectedDetailComment, ENT_COMPAT);
        }
        $afterGet = null;
        if ($result['status'] === 200) {
            $afterGet = $request('GET', '/api/mealOrder/evaluate' . $base . '&orderId=' . $ownId, $token);
        }
        $output['result'] = [
            'http' => $result['http'], 'business_status' => $result['status'],
            'message_prefix' => mb_substr($result['message'], 0, 80),
            'expected_comment_bytes' => strlen((string)$form['comment']),
            'expected_detail_comment_bytes' => isset($form['detail'][0]['comment'])
                ? strlen((string)$form['detail'][0]['comment']) : null,
            'expected_persisted_bytes' => strlen($expectedPersistedComment),
            'stored_comment_bytes' => strlen($storedComment),
            'stored_comment_exact' => $evalRows ? $storedComment === $expectedPersistedComment : null,
            'stored_detail_comment_exact' => $storedDetail
                ? $storedDetailComment === $expectedDetailComment : null,
            'stored_comment_has_replacement_question' => strpos($storedComment, '?') !== false,
            'evaluation_rows' => count($evalRows), 'detail_rows' => $detailCount,
            'order_status' => (int)($current['evaluate_status'] ?? -1),
            'order_pointer_matches_new' => $evalRows
                ? (int)($current['evaluate_id'] ?? -1) === (int)$evalRows[0]['id'] : false,
            'score_environment' => $evalRows ? $evalRows[0]['environment_score'] : null,
            'score_service' => $evalRows ? $evalRows[0]['service_score'] : null,
            'post_get_status' => $afterGet['status'] ?? null,
            'post_get_comment_exact' => $afterGet
                ? (string)($afterGet['data']['overall']['comment'] ?? '') === $expectedPersistedComment : null,
            'post_get_detail_comment_exact' => $afterGet && $expectedDetailComment !== null
                ? (string)($afterGet['data']['detail'][0]['comment'] ?? '') === $expectedDetailComment : null,
        ];
    }
} catch (Throwable $error) {
    $runError = get_class($error) . ': ' . mb_substr($error->getMessage(), 0, 130);
} finally {
    try {
        $output['cleanup']['own'] = $cleanup($ownId, $reviewMarker, $own);
        $output['cleanup']['other'] = $cleanup($otherId, $reviewMarker, $other);
    } catch (Throwable $error) {
        $cleanupError = get_class($error) . ': ' . mb_substr($error->getMessage(), 0, 130);
    }
    if ($tokenCreated) {
        try {
            \think\facade\Cache::delete($token);
            $output['cleanup']['token_deleted'] = !\think\facade\Cache::has($token);
        } catch (Throwable $error) {
            $cleanupError = ($cleanupError ? $cleanupError . '; ' : '') . 'cache ' . get_class($error);
        }
    }
}
$output['harness_status'] = $runError === null && $cleanupError === null ? 'completed' : 'failed';
if ($runError) $output['harness_error'] = $runError;
if ($cleanupError) $output['cleanup_error'] = $cleanupError;
echo json_encode($output, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
if ($output['harness_status'] !== 'completed') exit(1);
