<?php

declare(strict_types=1);

$root = getenv('POC_APP_ROOT');
$dialect = getenv('POC_DIALECT');
$marker = getenv('POC_MARKER');
$baseUrl = rtrim((string)getenv('POC_BASE_URL'), '/');
$refundFaultUrl = rtrim((string)getenv('POC_REFUND_FAULT_URL'), '/');
$toCents = static function ($value): int {
    $value = (string)$value;
    if (!preg_match('/^(-?)([0-9]+)(?:\.([0-9]{1,2}))?$/D', $value, $parts)) {
        throw new RuntimeException('Unexpected money representation');
    }
    return ($parts[1] === '-' ? -1 : 1)
        * ((int)$parts[2] * 100 + (int)str_pad($parts[3] ?? '', 2, '0'));
};
$fromCents = static function (int $cents): string {
    return ($cents < 0 ? '-' : '') . intdiv(abs($cents), 100) . '.'
        . str_pad((string)(abs($cents) % 100), 2, '0', STR_PAD_LEFT);
};
$shopMoney = (string)(getenv('POC_SHOP_MONEY') ?: '5.00');
if ($toCents($shopMoney) <= 0) exit(2);
if (!$root || !$baseUrl || !in_array($dialect, ['mysql', 'dm'], true)
    || !preg_match('/^CDXPAY[A-F0-9]{8}$/', (string)$marker)) exit(2);
foreach (['HOST', 'PORT', 'SCHEMA', 'USER', 'PASSWORD',
    'AI_HOST', 'AI_PORT', 'AI_SCHEMA', 'AI_USER', 'AI_PASSWORD'] as $part) {
    if (!getenv('POC_' . $part)) exit(2);
}
define('APP_PATH', $root . '/application/');
require $root . '/external_stubs.php';
require $root . '/thinkphp/base.php';
require $root . '/application/common.php';
if (!function_exists('config')) {
    function config($name = '', $value = null, $range = '')
    {
        return $value === null ? \think\Config::get($name) : \think\Config::set($name, $value, $range);
    }
}
set_exception_handler(static function (Throwable $error): void {
    fwrite(STDERR, 'SHOP_REFUND_PROBE_FAIL ' . get_class($error) . ': '
        . substr($error->getMessage(), 0, 250) . PHP_EOL);
    exit(1);
});
$config = [
    'type' => $dialect === 'dm' ? \app\cores\database\Dm::class : 'mysql',
    'charset' => $dialect === 'mysql' && getenv('POC_MYSQL_UTF8MB4') === '1'
        ? 'utf8mb4' : 'utf8',
    'hostname' => getenv('POC_HOST'), 'hostport' => (int)getenv('POC_PORT'),
    'database' => getenv('POC_SCHEMA'), 'username' => getenv('POC_USER'),
    'password' => getenv('POC_PASSWORD'), 'prefix' => 'ydy_',
    'debug' => false, 'resultset_type' => 'array', 'fields_strict' => true,
    'ai_zhct' => [
        'hostname' => getenv('POC_AI_HOST'), 'hostport' => (int)getenv('POC_AI_PORT'),
        'database' => getenv('POC_AI_SCHEMA'), 'username' => getenv('POC_AI_USER'),
        'password' => getenv('POC_AI_PASSWORD'), 'prefix' => 'yoshop_',
        'charset' => $dialect === 'mysql' && getenv('POC_MYSQL_UTF8MB4') === '1'
            ? 'utf8mb4' : 'utf8',
    ],
];
\think\Config::set($config, 'database');
\think\Config::set([
    'host' => getenv('POC_REDIS_HOST') ?: '127.0.0.1',
    'port' => (int)(getenv('POC_REDIS_PORT') ?: 1),
    'password' => '', 'select' => 0, 'timeout' => 0.2,
], 'redis');
\think\Db::clear();
$store = \think\Db::connect();
$ai = get_ai_db();
$boundary = json_decode((string)file_get_contents($baseUrl . '/__poc_boundary'), true);
$queueMode = (string)getenv('POC_QUEUE_MODE');
$realQueue = $queueMode === 'worker';
$queueFallback = $queueMode === 'fallback';
if (empty($boundary['external_stubs']) || empty($boundary['redis_isolated'])
    || ($realQueue && empty($boundary['real_queue']))
    || ($queueFallback && empty($boundary['queue_push_fault']))) {
    throw new RuntimeException('Payment/refund external boundary unavailable');
}
if ($refundFaultUrl !== '') {
    $faultBoundary = json_decode((string)file_get_contents($refundFaultUrl . '/__poc_boundary'), true);
    if (empty($faultBoundary['external_stubs']) || empty($faultBoundary['redis_isolated'])
        || empty($faultBoundary['refund_fault'])) {
        throw new RuntimeException('Refund fault boundary unavailable');
    }
}
$sourceOrder = $store->name('meal_order')->where('msgid', $marker)->find();
$user = $store->name('user')->where('account', $marker)->where('is_delete', 0)->find();
if (!$sourceOrder || !$user || (int)$sourceOrder['user_id'] !== (int)$user['id']) {
    throw new RuntimeException('Previously verified synthetic payment user is missing');
}
$restaurantId = (int)$sourceOrder['restaurant_id'];
$staffUuid = (string)$user['staff_uuid'];
$aiId = (int)$user['ai_user_id'];
if ($restaurantId <= 0 || $staffUuid === '' || $aiId <= 0
    || (int)$store->name('one_card_account_map')->where('staff_uuid', $staffUuid)->count() !== 0) {
    throw new RuntimeException('Synthetic user is ineligible for local-only test payment');
}
$balanceBeforeCents = $toCents($ai->name('user')->where('user_id', $aiId)->value('balance'));
$account = 'CDXRF' . strtoupper(bin2hex(random_bytes(4)));
$cardId = 'CDXCARD' . strtoupper(bin2hex(random_bytes(6)));
$authPwd = strtoupper(bin2hex(random_bytes(8)));
$authKey = strtoupper(bin2hex(random_bytes(8)));
$authId = 0;
$staffCardId = 0;
$setupStarted = false;
$setupCommitted = false;
try {
    $store->startTrans();
    $setupStarted = true;
    $staffCardId = (int)$store->name('staff_card')->insertGetId([
        'store_id' => 10001, 'staff_uuid' => $staffUuid, 'card_id' => $cardId,
        'card_status' => 1, 'is_delete' => 0,
    ]);
    $authId = (int)$store->name('api_auth')->insertGetId([
        'account' => $account, 'pwd' => $authPwd, 'key' => $authKey,
        'type' => 1, 'restaurant_id' => $restaurantId, 'status' => 1,
        'start_time' => date('Y-m-d H:i:s', time() - 3600),
        'end_time' => date('Y-m-d H:i:s', time() + 3600),
    ]);
    if ($staffCardId <= 0 || $authId <= 0) throw new RuntimeException('Synthetic card/API auth insert failed');
    $store->commit();
    $setupStarted = false;
    $setupCommitted = true;

    $post = static function (string $path, array $payload, string $forcedNo, ?string $targetUrl = null)
        use ($baseUrl, $account, $authPwd, $authKey): array {
        $context = stream_context_create(['http' => [
            'method' => 'POST',
            'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
                . 'account: ' . $account . "\r\n"
                . 'pwd: ' . $authPwd . "\r\n"
                . 'key: ' . $authKey . "\r\n"
                . 'X-Poc-Order-No: ' . $forcedNo . "\r\n",
            'content' => http_build_query($payload, '', '&', PHP_QUERY_RFC3986),
            'timeout' => 30, 'ignore_errors' => true,
        ]]);
        $body = file_get_contents(($targetUrl ?: $baseUrl) . $path, false, $context);
        $response = is_string($body) ? json_decode($body, true) : null;
        if (!is_array($response)) throw new RuntimeException('Formal Shop HTTP response was not JSON');
        return $response;
    };
    $forcedOrderNo = date('ymdHi') . sprintf('%05d', random_int(1, 99999));
    echo json_encode(['dialect' => $dialect, 'status' => 'starting_shop_http',
        'source_marker' => $marker, 'shop_order_no' => $forcedOrderNo]) . PHP_EOL;
    $payment = $post('/api/shop/order', ['card_id' => $cardId, 'money' => $shopMoney], $forcedOrderNo);
    if ((int)($payment['code'] ?? -1) !== 0) {
        throw new RuntimeException('Formal Shop payment failed: ' . substr((string)($payment['message'] ?? ''), 0, 120));
    }
    $paidOrderNo = $queueFallback ? (string)($payment['data']['order_no'] ?? '') : $forcedOrderNo;
    if ($paidOrderNo === '') throw new RuntimeException('Shop payment response has no order number');
    $work = $store->name('consume_order_work')->where('order_no', $paidOrderNo)->find();
    $queuedParams = $work ? json_decode((string)$work['payload'], true) : null;
    if (!is_array($queuedParams) || (string)($queuedParams['card_id'] ?? '') !== $cardId) {
        throw new RuntimeException('Synthetic Shop order queue payload not persisted');
    }
    if ($queueFallback) {
        $redis = new \Redis();
        $redis->connect((string)getenv('POC_REDIS_HOST'), (int)getenv('POC_REDIS_PORT'), 1.0);
        $faultReached = $redis->get('poc:queue_push_fault:' . $forcedOrderNo) === '1';
        $redis->del('poc:queue_push_fault:' . $forcedOrderNo);
        if (!$faultReached) {
            throw new RuntimeException('Synthetic queue push fault did not reach business fallback');
        }
    } elseif ($realQueue) {
        $dmExtensions = '';
        if ($dialect === 'dm') {
            $lib = (string)getenv('POC_DM_PHP_LIB');
            $pdo = (string)getenv('POC_DM_PHP_PDO');
            if (!is_file($lib) || !is_file($pdo)) {
                throw new RuntimeException('DM queue worker driver paths unavailable');
            }
            $dmExtensions = ' -d extension=' . escapeshellarg($lib)
                . ' -d extension=' . escapeshellarg($pdo);
        }
        $command = escapeshellarg(PHP_BINARY)
            . $dmExtensions
            . ' -d auto_prepend_file=' . escapeshellarg($root . '/external_stubs.php')
            . ' ' . escapeshellarg($root . '/think')
            . ' queue:work --queue=serve_meal_order_BUSINESS_VALUE --sleep=0 --tries=1 --no-interaction';
        putenv('POC_REAL_QUEUE=1');
        putenv('POC_EXTERNAL_STUBS=1');
        for ($job = 0; $job < 2; $job++) {
            $process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $root);
            if (!is_resource($process)) throw new RuntimeException('Isolated queue worker could not start');
            $stdout = stream_get_contents($pipes[1]);
            $stderr = stream_get_contents($pipes[2]);
            fclose($pipes[1]);
            fclose($pipes[2]);
            $exitCode = proc_close($process);
            if ($exitCode !== 0 || strpos($stdout, 'Processed:') === false) {
                throw new RuntimeException('Isolated queue worker did not process job ' . ($job + 1)
                    . '; exit=' . $exitCode . '; stderr_present=' . ($stderr !== '' ? 'yes' : 'no'));
            }
        }
    } else {
        \app\common\service\Order::forceNextOrderNo($forcedOrderNo);
        if (!(new \app\api\service\Consume())->orderPayFromQueue($queuedParams)) {
            throw new RuntimeException('Queue consumer could not persist Shop order');
        }
    }
    $order = $store->name('meal_order')->where('order_no', $paidOrderNo)->find();
    $consumeSettlements = (int)$ai->name('account_settlement')->where('biz_order_no', $paidOrderNo)
        ->where('biz_type', 'consume')->count();
    if (!$order || (int)$order['user_id'] !== (int)$user['id'] || $consumeSettlements !== 1) {
        throw new RuntimeException('Shop payment readback mismatch; order=' . ($order ? 'yes' : 'no')
            . ', settlements=' . $consumeSettlements
            . ', response=' . substr((string)($payment['message'] ?? ''), 0, 60));
    }
    $orderId = (int)$order['id'];
    $paidCents = $toCents($order['pay_price']);
    if ($queueFallback) {
        $redis = new \Redis();
        $redis->connect((string)getenv('POC_REDIS_HOST'), (int)getenv('POC_REDIS_PORT'), 1.0);
        $syncFaultReached = $redis->get('poc:queue_sync_fault:' . $orderId) === '1';
        $redis->del('poc:queue_sync_fault:' . $orderId);
        if (!$syncFaultReached) {
            throw new RuntimeException('Post-commit nutrition queue failure was not exercised');
        }
        $workAfter = $store->name('consume_order_work')
            ->where('msgid', (string)$queuedParams['msgid'])->find();
        if (!$workAfter || (int)$workAfter['status'] !== 20
            || (int)$workAfter['meal_order_id'] !== $orderId
            || (string)$workAfter['order_no'] !== $paidOrderNo) {
            throw new RuntimeException('Synchronous queue fallback left failed work status or stale order number');
        }
    }
    $balanceAfterPayCents = $toCents($ai->name('user')->where('user_id', $aiId)->value('balance'));
    if ($paidCents <= 0 || $balanceAfterPayCents !== $balanceBeforeCents - $paidCents) {
        throw new RuntimeException('Shop payment balance does not match persisted order');
    }
    if ($refundFaultUrl !== '') {
        $forcedRefundNo = date('ymdHi') . sprintf('%05d', random_int(1, 99999));
        $refundNo = 'RF' . $forcedRefundNo;
        $failedRefund = $post('/api/shop/refund', ['order_no' => $paidOrderNo,
            'refund_amount' => $fromCents($paidCents)], $forcedRefundNo, $refundFaultUrl);
        $redis = new \Redis();
        $redis->connect((string)getenv('POC_REDIS_HOST'), (int)getenv('POC_REDIS_PORT'), 1.0);
        $faultReached = $redis->get('poc:refund_fault:' . $refundNo) === '1';
        $redis->del('poc:refund_fault:' . $refundNo);
        $refundRows = (int)$store->name('meal_order_refund')->where('order_no', $refundNo)->count();
        $refundSettlements = (int)$ai->name('account_settlement')->where('refund_no', $refundNo)->count();
        $orderAfterFault = $store->name('meal_order')->where('id', $orderId)->find();
        $balanceAfterFaultCents = $toCents($ai->name('user')->where('user_id', $aiId)->value('balance'));
        if ((int)($failedRefund['code'] ?? 0) === 0 || !$faultReached
            || $refundRows !== 0 || $refundSettlements !== 0
            || $toCents($orderAfterFault['refund_amount']) !== 0
            || $balanceAfterFaultCents !== $balanceAfterPayCents) {
            throw new RuntimeException('Injected refund account failure left partial business records');
        }
        echo json_encode(['dialect' => $dialect, 'status' => 'passed_refund_fault_rollback',
            'queue_mode' => $realQueue ? 'isolated_worker' : ($queueFallback ? 'fallback' : 'manual_consumer'),
            'source_marker' => $marker, 'shop_order_id' => $orderId,
            'shop_order_no' => $paidOrderNo, 'refund_no' => $refundNo,
            'fault_reached' => true, 'store_refunds' => 0,
            'ai_refund_settlements' => 0, 'balance_unchanged' => true]) . PHP_EOL;
        return;
    }
    $forcedRefundNo = date('ymdHi') . sprintf('%05d', random_int(1, 99999));
    $refund = $post('/api/shop/refund', ['order_no' => $paidOrderNo,
        'refund_amount' => $fromCents($paidCents)], $forcedRefundNo);
    if ((int)($refund['code'] ?? -1) !== 0) {
        throw new RuntimeException('Formal Shop refund failed: ' . substr((string)($refund['message'] ?? ''), 0, 120));
    }
    $refundNo = 'RF' . $forcedRefundNo;
    $refundRow = $store->name('meal_order_refund')->where('meal_order_id', $orderId)
        ->where('order_no', $refundNo)->find();
    $orderAfter = $store->name('meal_order')->where('id', $orderId)->find();
    $refundSettlements = (int)$ai->name('account_settlement')->where('refund_no', $refundNo)
        ->where('biz_type', 'refund')->count();
    $balanceAfterRefundCents = $toCents($ai->name('user')->where('user_id', $aiId)->value('balance'));
    if (!$refundRow || $refundSettlements !== 1
        || $toCents($orderAfter['refund_amount']) !== $paidCents
        || $balanceAfterRefundCents !== $balanceBeforeCents) {
        throw new RuntimeException('Shop refund store/AI records or balance mismatch');
    }
    $duplicate = $post('/api/shop/refund', ['order_no' => $paidOrderNo,
        'refund_amount' => $fromCents($paidCents)], $forcedRefundNo);
    if ((int)($duplicate['code'] ?? 0) === 0
        || (int)$store->name('meal_order_refund')->where('meal_order_id', $orderId)->count() !== 1
        || (int)$ai->name('account_settlement')->where('refund_no', $refundNo)->count() !== 1) {
        throw new RuntimeException('Duplicate refund created another business record');
    }
    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'queue_mode' => $realQueue ? 'isolated_worker' : ($queueFallback ? 'fallback' : 'manual_consumer'),
        'source_marker' => $marker, 'shop_order_id' => $orderId,
        'shop_order_no' => $paidOrderNo, 'refund_id' => (int)$refundRow['id'],
        'refund_no' => $refundNo, 'paid_cents' => $paidCents,
        'balance_restored' => true,
        'duplicate_refund_rejected' => true]) . PHP_EOL;
} finally {
    if ($setupStarted) $store->rollback();
    if ($setupCommitted) {
        $store->name('api_auth')->where('id', $authId)->where('account', $account)->delete();
        $store->name('staff_card')->where('id', $staffCardId)->where('card_id', $cardId)->delete();
    }
}
