<?php

declare(strict_types=1);

// Draft: read-only by default. Real HTTP/DML needs root approval plus an explicit flag.
set_exception_handler(static function (Throwable $error): void {
    fwrite(STDERR, 'CUSTOM_DISH_TEST_FAILED ' . get_class($error)
        . ' code=' . (string)$error->getCode() . PHP_EOL);
    exit(2);
});
$root = (string)getenv('POC_TARGET_ROOT');
$base = rtrim((string)getenv('POC_BASE_URL'), '/');
$dialect = (string)getenv('POC_DIALECT');
$variant = (string)getenv('POC_VARIANT');
$approved = [
    'A' => ['/tmp/zhct-http-ai-macos.zjaU8h', 'http://127.0.0.1:18105', 'mysql'],
    'B' => ['/tmp/zhct-http-ai-macos.x2CCZX', 'http://127.0.0.1:18101', 'mysql'],
    'C' => ['/tmp/zhct-ai-http-20260929.3XDj3w', 'http://127.0.0.1:18102', 'dm'],
];
if (!isset($approved[$variant]) || [$root, $base, $dialect] !== $approved[$variant]
    || !is_file($root . '/vendor/autoload.php')) {
    throw new RuntimeException('Unapproved isolated AI runtime');
}
if ($dialect === 'dm') {
    $pid = (string)getenv('POC_RUNTIME_PID');
    if (!ctype_digit($pid) || (int)$pid <= 1) {
        throw new RuntimeException('AI HTTP PID missing');
    }
    $processEnv = [];
    foreach (explode("\0", (string)@file_get_contents('/proc/' . $pid . '/environ')) as $item) {
        $parts = explode('=', $item, 2);
        if (count($parts) === 2) $processEnv[$parts[0]] = $parts[1];
    }
    if (($processEnv['POC_TARGET_ROOT'] ?? '') !== $root
        || ($processEnv['POC_BASE_URL'] ?? '') !== $base
        || ($processEnv['POC_DIALECT'] ?? '') !== 'dm') {
        throw new RuntimeException('AI HTTP process identity mismatch');
    }
    foreach (['HOST', 'PORT', 'SCHEMA', 'USER', 'PASSWORD'] as $part) {
        $key = 'POC_AI_' . $part;
        if (empty($processEnv[$key])) throw new RuntimeException('AI HTTP DB context missing');
        putenv($key . '=' . $processEnv[$key]);
    }
    if ((string)getenv('POC_AI_HOST') !== '127.0.0.1'
        || (string)getenv('POC_AI_PORT') !== '5238'
        || (string)getenv('POC_AI_SCHEMA') !== 'AIZHCT_APP'
        || (string)getenv('POC_AI_USER') !== 'AIZHCT_APP') {
        throw new RuntimeException('Non-production AI DM boundary failed');
    }
} else {
    $localPath = '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local';
    if (realpath((string)getenv('POC_MYSQL_LOCAL_ENV')) !== realpath($localPath)) {
        throw new RuntimeException('Only code local AI MySQL configuration allowed');
    }
    $local = parse_ini_file($localPath, true, INI_SCANNER_RAW);
    if (!is_array($local) || !isset($local['DATABASE'])) {
        throw new RuntimeException('AI local MySQL configuration absent');
    }
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
        'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $source => $target) {
        putenv('POC_AI_' . $target . '=' . (string)$local['DATABASE'][$source]);
    }
}
$boundaryRaw = @file_get_contents($base . '/__poc_boundary', false,
    stream_context_create(['http' => ['timeout' => 5]]));
$boundary = is_string($boundaryRaw) ? json_decode($boundaryRaw, true) : null;
if (!is_array($boundary) || empty($boundary['external_stubs'])
    || !empty($boundary['real_queue'])) {
    throw new RuntimeException('AI HTTP external isolation absent');
}

define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName('poc')->initialize();
$dsn = $dialect === 'dm'
    ? 'dm:host=' . getenv('POC_AI_HOST') . ';port=' . getenv('POC_AI_PORT')
        . ';schema=' . getenv('POC_AI_SCHEMA')
    : 'mysql:host=' . getenv('POC_AI_HOST') . ';port=' . getenv('POC_AI_PORT')
        . ';dbname=' . getenv('POC_AI_SCHEMA') . ';charset=utf8mb4';
$db = new PDO($dsn, getenv('POC_AI_USER'), getenv('POC_AI_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
if ($dialect === 'dm'
    && (string)$db->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")
        ->fetchColumn() !== 'AIZHCT_APP') {
    throw new RuntimeException('Effective AI DM schema mismatch');
}
$table = static function (string $name) use ($dialect): string {
    return $dialect === 'dm' ? strtoupper($name) : $name;
};
$names = [];
foreach (['user', 'staff', 'dishes', 'foods', 'dishes_foods', 'dishes_tag',
    'dishes_cate', 'upload_file', 'dishes_unsuitable', 'config_enum', 'store'] as $short) {
    $names[$short] = $table('yoshop_' . $short);
}
$count = static function (string $sql, array $args = []) use ($db): int {
    $stmt = $db->prepare($sql);
    $stmt->execute($args);
    return (int)$stmt->fetchColumn();
};
$rows = static function (string $sql, array $args = []) use ($db): array {
    $stmt = $db->prepare($sql);
    $stmt->execute($args);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
};
$assert = static function (bool $yes, string $message): void {
    if (!$yes) throw new RuntimeException($message);
};
foreach ($names as $name) {
    $db->query('SELECT 1 FROM ' . $name . ' WHERE 1=0');
}
$requiredColumns = [
    'user' => ['USER_ID', 'STORE_ID', 'NICK_NAME', 'IS_DELETE'],
    'staff' => ['USER_ID'],
    'dishes' => ['ID', 'NAME', 'ADD_USER_ID', 'SOURCE', 'USER_DELETE',
        'STORE_ID', 'UNIT_NUMBER', 'ENERGY', 'WEIGHT', 'IS_DELETE'],
    'foods' => ['ID', 'NAME', 'ADD_USER_ID', 'SOURCE', 'STORE_ID'],
    'dishes_foods' => ['ID', 'DISHES_ID', 'FOODS_ID'],
    'dishes_tag' => ['ID', 'DISHES_ID'],
];
foreach ($requiredColumns as $short => $columns) {
    $stmt = $dialect === 'dm'
        ? $db->prepare('SELECT COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME=?')
        : $db->prepare('SELECT COLUMN_NAME FROM information_schema.COLUMNS'
            . ' WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=?');
    $stmt->execute([$names[$short]]);
    $actual = array_map('strtoupper', $stmt->fetchAll(PDO::FETCH_COLUMN));
    $assert(count(array_diff($columns, $actual)) === 0,
        'AI custom dishes required columns missing');
}
$assert($count('SELECT COUNT(*) FROM ' . $names['store']
    . ' WHERE STORE_ID=? AND IS_DELETE=0', [10001]) === 1,
    'Approved AI store missing/ambiguous');
$marker = 'CDXPARDSH' . strtoupper(bin2hex(random_bytes(4)));
$assert($count('SELECT COUNT(*) FROM ' . $names['user'] . ' WHERE NICK_NAME=?', [$marker]) === 0
    && $count('SELECT COUNT(*) FROM ' . $names['dishes'] . ' WHERE NAME=?', [$marker]) === 0
    && $count('SELECT COUNT(*) FROM ' . $names['foods'] . ' WHERE NAME=?', [$marker]) === 0,
    'Synthetic marker already present');
if ((string)getenv('POC_APPROVED_DISHES_WRITE') !== '1') {
    echo json_encode(['suite' => 'custom-dishes-' . $variant,
        'mode' => 'read-only-preflight', 'required_tables_and_columns' => true,
        'external_stubs' => true, 'marker_zero' => true], JSON_UNESCAPED_SLASHES) . PHP_EOL;
    exit(0);
}

$token = $marker . 'TOKEN' . strtoupper(bin2hex(random_bytes(10)));
$userId = 0;
$dishId = 0;
$foodId = 0;
$results = [];
$cleanup = [];
$failure = '';
$unsettledRequest = false;
$request = static function (string $path, ?array $body = null, ?string $auth = null,
    int $timeoutSeconds = 30) use ($base): array {
    $headers = "platform: POC\r\nContent-Type: application/json;charset=utf-8\r\n";
    if ($auth !== null) $headers .= 'Access-Token: ' . $auth . "\r\n";
    $opts = ['method' => $body === null ? 'GET' : 'POST', 'header' => $headers,
        'timeout' => $timeoutSeconds, 'ignore_errors' => true];
    if ($body !== null) {
        $opts['content'] = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    $raw = @file_get_contents($base . $path, false,
        stream_context_create(['http' => $opts]));
    $line = $http_response_header[0] ?? '';
    preg_match('/HTTP\/\S+\s+(\d+)/', $line, $match);
    $json = is_string($raw) ? json_decode($raw, true) : null;
    return ['http' => isset($match[1]) ? (int)$match[1] : 0,
        'transport_complete' => is_string($raw) && is_array($json),
        'status' => is_array($json) ? (int)($json['status'] ?? -1) : -1,
        'message' => is_array($json) ? (string)($json['message'] ?? '') : '',
        'data' => is_array($json) ? ($json['data'] ?? null) : null];
};
$url = static function (string $action): string {
    return '/api/dishes/' . $action . '?storeId=10001';
};
$ownDish = static function (array $row) use (&$userId, $marker): bool {
    return (int)($row['add_user_id'] ?? 0) === $userId
        && (int)($row['source'] ?? 0) === 3
        && (string)($row['name'] ?? '') === $marker;
};
try {
    $db->prepare('INSERT INTO ' . $names['user']
        . ' (STORE_ID,MOBILE,NICK_NAME,BALANCE,GIFT_BALANCE,IS_DELETE,CREATE_TIME)'
        . ' VALUES (10001,?,?,?,?,0,?)')
        ->execute(['', $marker, '0.00', '0.00', time()]);
    $users = $rows('SELECT USER_ID FROM ' . $names['user'] . ' WHERE NICK_NAME=?', [$marker]);
    $assert(count($users) === 1, 'Synthetic AI user missing/ambiguous');
    $userId = (int)$users[0]['user_id'];
    $assert($userId > 0 && $count('SELECT COUNT(*) FROM ' . $names['staff']
        . ' WHERE USER_ID=?', [$userId]) === 0, 'Synthetic user/staff boundary failed');
    \think\facade\Cache::set($token, ['user' => ['user_id' => $userId],
        'password_version' => 0, 'login_type' => 'thirdparty'], 300);

    $form = ['name' => $marker, 'number' => '1', 'unit' => '2', 'energy' => '100',
        'energyUnit' => 1, 'img_url' => '', 'file_id' => 0, 'protein' => '10',
        'fat' => '5', 'carbohydrate' => '20', 'sodium' => '0',
        'dietary_fiber' => '0', 'gl' => '0', 'va' => '0', 'vc' => '0',
        'zinc' => '0', 'calcium' => '0'];
    $unauth = $request($url('customAdd'), ['form' => $form]);
    $assert($unauth['status'] === 401
        && $count('SELECT COUNT(*) FROM ' . $names['dishes'] . ' WHERE NAME=?', [$marker]) === 0,
        'Unauthenticated custom dish wrote rows');
    $results['unauth_rejected_zero_write'] = true;
    $invalid = $request($url('customAdd'), ['form' => array_replace($form, ['name' => ''])], $token);
    $assert($invalid['status'] !== 200
        && $count('SELECT COUNT(*) FROM ' . $names['dishes'] . ' WHERE NAME=?', [$marker]) === 0,
        'Missing name accepted or wrote rows');
    $results['missing_name_rejected_zero_write'] = true;

    $created = $request($url('customAdd'), ['form' => $form], $token);
    $dishRows = $rows('SELECT ID,NAME,ADD_USER_ID,SOURCE,USER_DELETE,STORE_ID,ENERGY,WEIGHT'
        . ' FROM ' . $names['dishes'] . ' WHERE NAME=? AND ADD_USER_ID=?', [$marker, $userId]);
    $foodRows = $rows('SELECT ID,NAME,ADD_USER_ID,SOURCE,STORE_ID FROM ' . $names['foods']
        . ' WHERE NAME=? AND ADD_USER_ID=?', [$marker, $userId]);
    $assert(count($dishRows) === 1 && count($foodRows) === 1,
        'Custom dish/food rows missing or ambiguous');
    $dishId = (int)$dishRows[0]['id'];
    $foodId = (int)$foodRows[0]['id'];
    $assert($dishId > 0 && $foodId > 0 && $ownDish($dishRows[0])
        && (int)$foodRows[0]['source'] === 3
        && (int)$foodRows[0]['store_id'] === 10001
        && (int)$dishRows[0]['store_id'] === 10001
        && (int)$dishRows[0]['user_delete'] === 0
        && $count('SELECT COUNT(*) FROM ' . $names['dishes_foods']
            . ' WHERE DISHES_ID=? AND FOODS_ID=?', [$dishId, $foodId]) === 1
        && $created['http'] === 200 && $created['status'] === 200
        && $created['message'] === '添加成功'
        && (int)($created['data']['id'] ?? 0) === $dishId,
        'Custom dish create response/original tables mismatch');
    $results['create_four_table_readback'] = true;

    $list = $request($url('list'), ['categoryId' => -1,
        'dishesName' => $marker, 'page' => 1], $token);
    $found = array_values(array_filter((array)($list['data']['list']['data'] ?? []),
        static function ($item) use ($dishId): bool {
            return (int)($item['id'] ?? 0) === $dishId;
        }));
    $assert($list['http'] === 200 && $list['status'] === 200
        && count($found) === 1, 'Custom dish list did not include synthetic row');
    $results['list_nonempty_and_owned'] = true;

    $detail = $request($url('detail'), ['id' => $dishId], $token);
    $detailData = $detail['data']['detail'] ?? [];
    $detailChecks = [
        'http_200' => $detail['http'] === 200,
        'business_200' => $detail['status'] === 200,
        'detail_object' => is_array($detailData),
        'id_match' => (int)($detailData['id'] ?? 0) === $dishId,
        'name_match' => (string)($detailData['name'] ?? '') === $marker,
        'foods_array' => is_array($detailData['foods'] ?? null),
        'foods_count_one' => is_array($detailData['foods'] ?? null)
            && count($detailData['foods']) === 1,
    ];
    $results['detail_checks'] = $detailChecks;
    $assert(!in_array(false, $detailChecks, true),
        'Custom dish detail/foods mismatch');
    $results['detail_nested_food_count'] = 1;

    $detailOne = $request($url('detailWithWeight'),
        ['id' => $dishId, 'weight' => '100', 'number' => '1'], $token);
    $detailTwo = $request($url('detailWithWeight'),
        ['id' => $dishId, 'weight' => '100', 'number' => '2'], $token);
    $one = $detailOne['data']['detail'] ?? [];
    $two = $detailTwo['data']['detail'] ?? [];
    $weightChecks = [
        'one_business_200' => $detailOne['status'] === 200,
        'two_business_200' => $detailTwo['status'] === 200,
        'one_id_match' => (int)($one['id'] ?? 0) === $dishId,
        'two_id_match' => (int)($two['id'] ?? 0) === $dishId,
        'energy_doubles' => abs((float)($two['energy'] ?? -1)
            - 2 * (float)($one['energy'] ?? -1)) < 0.001,
        'weight_doubles' => abs((float)($two['weight'] ?? -1)
            - 2 * (float)($one['weight'] ?? -1)) < 0.001,
    ];
    $results['weight_checks'] = $weightChecks;
    $assert(!in_array(false, $weightChecks, true),
        'Custom dish quantity calculation mismatch');
    $results['detail_weight_quantity_formula'] = true;
    $results['response_types'] = ['list_id' => gettype($found[0]['id'] ?? null),
        'detail_id' => gettype($detailData['id'] ?? null),
        'detail_energy' => gettype($detailData['energy'] ?? null),
        'weight_energy' => gettype($one['energy'] ?? null)];

    $deleteStarted = microtime(true);
    $deleted = $request($url('customDel'), ['id' => $dishId], $token, 60);
    $deleteElapsedMs = (int)round((microtime(true) - $deleteStarted) * 1000);
    if (!$deleted['transport_complete']) {
        // A timed-out HTTP write may still be running; never race it with cleanup.
        $unsettledRequest = true;
        $results['delete_checks'] = ['transport_complete' => false,
            'elapsed_ms' => $deleteElapsedMs];
        throw new RuntimeException('Custom delete outcome unsettled; cleanup deferred');
    }
    $deleteChecks = [
        'transport_complete' => true,
        'http_200' => $deleted['http'] === 200,
        'business_200' => $deleted['status'] === 200,
        'message_delete_success' => $deleted['message'] === '删除成功',
        'soft_delete_one_row' => $count('SELECT COUNT(*) FROM ' . $names['dishes']
            . ' WHERE ID=? AND ADD_USER_ID=? AND SOURCE=3 AND USER_DELETE=1',
            [$dishId, $userId]) === 1,
        'within_client_15s' => $deleteElapsedMs <= 15000,
    ];
    $results['delete_checks'] = $deleteChecks;
    $results['delete_elapsed_ms'] = $deleteElapsedMs;
    $after = $request($url('list'), ['categoryId' => -1,
        'dishesName' => $marker, 'page' => 1], $token);
    $afterFound = array_filter((array)($after['data']['list']['data'] ?? []),
        static function ($item) use ($dishId): bool {
            return (int)($item['id'] ?? 0) === $dishId;
        });
    $results['after_delete_list_hidden'] = $after['status'] === 200
        && count($afterFound) === 0;
    $assert(!in_array(false, $deleteChecks, true)
        && $results['after_delete_list_hidden'],
        'Custom delete response, client budget or list mismatch');
    $results['soft_delete_hidden_from_list'] = true;
} catch (Throwable $e) {
    $failure = get_class($e) . ':' . hash('sha256', $e->getMessage());
} finally {
    try {
        if ($unsettledRequest) {
            $cleanup = ['deferred_inflight' => true];
        } else {
        \think\facade\Cache::delete($token);
        if ($userId > 0) {
            $dishRows = $rows('SELECT ID,NAME,ADD_USER_ID,SOURCE FROM ' . $names['dishes']
                . ' WHERE NAME=? AND ADD_USER_ID=?', [$marker, $userId]);
            $foodRows = $rows('SELECT ID,NAME,ADD_USER_ID,SOURCE FROM ' . $names['foods']
                . ' WHERE NAME=? AND ADD_USER_ID=?', [$marker, $userId]);
            $assert(count($dishRows) <= 1 && count($foodRows) <= 1,
                'Synthetic dish/food ambiguous; refusing cleanup');
            foreach ($dishRows as $row) {
                $assert($ownDish($row), 'Unexpected dish owner; refusing cleanup');
                $dishId = (int)$row['id'];
            }
            foreach ($foodRows as $row) {
                $assert((int)$row['add_user_id'] === $userId
                    && (int)$row['source'] === 3 && (string)$row['name'] === $marker,
                    'Unexpected food owner; refusing cleanup');
                $foodId = (int)$row['id'];
            }
            if ($dishId > 0) {
                $relations = $rows('SELECT ID,FOODS_ID FROM ' . $names['dishes_foods']
                    . ' WHERE DISHES_ID=?', [$dishId]);
                $assert(count($relations) <= 1 && (count($relations) === 0
                    || (int)$relations[0]['foods_id'] === $foodId),
                    'Unexpected synthetic dish relation; refusing cleanup');
                $db->beginTransaction();
                try {
                    $db->prepare('DELETE FROM ' . $names['dishes_tag'] . ' WHERE DISHES_ID=?')
                        ->execute([$dishId]);
                    $db->prepare('DELETE FROM ' . $names['dishes_foods']
                        . ' WHERE DISHES_ID=? AND FOODS_ID=?')->execute([$dishId, $foodId]);
                    $db->prepare('DELETE FROM ' . $names['dishes']
                        . ' WHERE ID=? AND ADD_USER_ID=? AND NAME=? AND SOURCE=3')
                        ->execute([$dishId, $userId, $marker]);
                    $db->commit();
                } catch (Throwable $e) {
                    $db->rollBack();
                    throw $e;
                }
            }
            if ($foodId > 0) {
                $assert($count('SELECT COUNT(*) FROM ' . $names['dishes_foods']
                    . ' WHERE FOODS_ID=?', [$foodId]) === 0,
                    'Synthetic food still has relations; refusing cleanup');
                $db->prepare('DELETE FROM ' . $names['foods']
                    . ' WHERE ID=? AND ADD_USER_ID=? AND NAME=? AND SOURCE=3')
                    ->execute([$foodId, $userId, $marker]);
            }
            $assert($count('SELECT COUNT(*) FROM ' . $names['dishes']
                . ' WHERE NAME=? AND ADD_USER_ID=?', [$marker, $userId]) === 0
                && $count('SELECT COUNT(*) FROM ' . $names['foods']
                    . ' WHERE NAME=? AND ADD_USER_ID=?', [$marker, $userId]) === 0,
                'Synthetic dish/food cleanup incomplete');
            $db->prepare('DELETE FROM ' . $names['user']
                . ' WHERE USER_ID=? AND NICK_NAME=?')->execute([$userId, $marker]);
        }
        $cleanup = ['user_zero' => $count('SELECT COUNT(*) FROM ' . $names['user']
                . ' WHERE NICK_NAME=?', [$marker]) === 0,
            'dishes_zero' => $count('SELECT COUNT(*) FROM ' . $names['dishes']
                . ' WHERE NAME=?', [$marker]) === 0,
            'foods_zero' => $count('SELECT COUNT(*) FROM ' . $names['foods']
                . ' WHERE NAME=?', [$marker]) === 0,
            'relation_zero' => $dishId <= 0 || $count('SELECT COUNT(*) FROM '
                . $names['dishes_foods'] . ' WHERE DISHES_ID=?', [$dishId]) === 0,
            'tag_zero' => $dishId <= 0 || $count('SELECT COUNT(*) FROM '
                . $names['dishes_tag'] . ' WHERE DISHES_ID=?', [$dishId]) === 0];
        }
    } catch (Throwable $e) {
        $cleanup['error_class'] = get_class($e);
        $cleanup['error_sha256'] = hash('sha256', $e->getMessage());
    }
}
$clean = !in_array(false, $cleanup, true) && !isset($cleanup['error_class']);
echo json_encode(['suite' => 'custom-dishes-' . $variant, 'marker' => $marker,
    'results' => $results, 'failure' => $failure, 'cleanup' => $cleanup],
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit($failure === '' && $clean ? 0 : 1);
