<?php
declare(strict_types=1);

// Reuses only prior CDXPAY/CDXPARMOB audit identities. The sole write is a
// random <=300-second token in the isolated per-runtime File cache.
if (getenv('CDX_READ8_HTTP') !== 'APPROVED_OWNED_AUDIT_READ_V1') {
    echo json_encode(['status' => 'default_deny', 'http' => 0, 'db_dml' => 0]), PHP_EOL;
    exit(78);
}
$target = (string)getenv('CDX_READ8_TARGET');
$role = (string)getenv('CDX_READ8_ROLE');
if (!in_array($target, ['A', 'B', 'C'], true)
    || !in_array($role, ['mobile', 'cash', 'subsidy'], true)) {
    throw new RuntimeException('Explicit target and role required');
}
$expectedSha = (string)getenv('CDX_READ8_SHA');
if (strlen($expectedSha) !== 64 || !hash_equals($expectedSha, (string)hash_file('sha256', __FILE__))) {
    throw new RuntimeException('Script SHA mismatch');
}
putenv('CDX_READ2_TARGET=' . $target);
putenv('CDX_READ2_SCHEMA_PREFLIGHT=1');
require $target === 'C'
    ? '/tmp/zhct-ai-read2-VgKO4CDY/read2_schema_preflight.php'
    : __DIR__ . '/../parallel-ai-next-read-safe4/read2_schema_preflight.php';
$identityFields = ['POC_AI_HOST', 'POC_AI_PORT', 'POC_AI_SCHEMA', 'POC_AI_USER',
    'POC_HOST', 'POC_PORT', 'POC_SCHEMA', 'POC_USER'];
$identity = array_map(static function (string $key) use ($config): string {
    return (string)($config[$key] ?? '');
}, $identityFields);
$approvedIdentityHash = $target === 'C'
    ? 'da9d709b8a8a1163c08e68efcbb69edb85d9d8b365f7b7705a0a2141aa4860bd'
    : 'd908a48732b1a4874c5d8427ab60186eb8272aa4e0c5af34716eadafee482c88';
if (in_array('', $identity, true)
    || !hash_equals($approvedIdentityHash,
        hash('sha256', json_encode($identity, JSON_UNESCAPED_SLASHES)))
    || ($target === 'C'
        && !hash_equals('bcf83be7714eaec2734fb7dccddabc18c83c34b4201ee567f5044fb4bd54f526',
            hash('sha256', php_uname('n'))))) {
    throw new RuntimeException('Approved development or nonproduction host identity mismatch');
}
$ai->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$store->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);

$root = ['A' => '/private/tmp/zhct-http-ai-macos.zjaU8h',
    'B' => '/private/tmp/zhct-http-ai-macos.x2CCZX',
    'C' => '/tmp/zhct-ai-http-20260929.3XDj3w'][$target];
$port = ['A' => 18105, 'B' => 18101, 'C' => 18102][$target];
$sourceRoot = $target === 'C'
    ? '/tmp/zhct-dm-poc-20260928.fcpiiN/ai_api'
    : ($target === 'B'
        ? '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api'
        : '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/baseline-a-ai-api-V4.0.40');
if ($target !== 'A') {
    if (!function_exists('bcadd')) {
        throw new RuntimeException('B/C fixed-scale decimal runtime requires BCMath');
    }
    $expectedSources = [
        'app/api/model/recharge/Order.php' => '7f1284b84ba1e5c12406beec9ff7fadeea083313bf066234d1f4b4ec4ae35c4f',
        'app/api/model/user/BalanceLog.php' => '56849b29d8768349d184431314ef0f410f8f84330a758795691e7890194ced65',
        'app/api/model/user/SubsidyLog.php' => '248e22b1b472e0cb21e1fee5af88e44933c70c060e681e80db842146be0ced85',
        'app/common/service/FixedScaleDecimalResponse.php' => 'c3bce04654a313e2918667a8a1019eb7b4c45b98edf9a5218d3e8c40b8d333cc',
    ];
    foreach ($expectedSources as $relative => $sha) {
        $path = $sourceRoot . '/' . $relative;
        if (!is_file($path) || !hash_equals($sha, (string)hash_file('sha256', $path))) {
            throw new RuntimeException('B/C approved response source changed');
        }
    }
}
$cachePath = realpath($root . '/runtime/cache');
if (realpath($root) !== $root || $cachePath !== $root . '/runtime/cache'
    || (((int)fileperms($root) & 0077) !== 0)
    || !hash_equals('ccf1c56eb4178f3eaad8775e26a27742b2cacf662b46fb058cbb91d23783b8c6',
        (string)hash_file('sha256', $root . '/config/cache.php'))
    || is_file($root . '/.env') || is_file($root . '/.env.poc')
    || getenv('CACHE_DRIVER') !== false || getenv('cache.driver') !== false) {
    throw new RuntimeException('Private File cache boundary mismatch');
}
$httpEnvironment = $target === 'C' ? ($raw ?? '') : ($cmdline ?? '');
if (!is_string($httpEnvironment)
    || preg_match('/(?:^|[\s\x00])(?:CACHE_DRIVER|cache\.driver)=/i', $httpEnvironment) === 1) {
    throw new RuntimeException('HTTP File cache override detected');
}
foreach ($config as $key => $value) {
    if (strpos($key, 'POC_') === 0) putenv($key . '=' . $value);
}
define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require $root . '/vendor/autoload.php';
if ($target !== 'A') {
    $classFiles = [
        \app\api\model\recharge\Order::class => 'app/api/model/recharge/Order.php',
        \app\api\model\user\BalanceLog::class => 'app/api/model/user/BalanceLog.php',
        \app\api\model\user\SubsidyLog::class => 'app/api/model/user/SubsidyLog.php',
        \app\common\service\FixedScaleDecimalResponse::class
            => 'app/common/service/FixedScaleDecimalResponse.php',
    ];
    foreach ($classFiles as $class => $relative) {
        if ((new ReflectionClass($class))->getFileName() !== $sourceRoot . '/' . $relative) {
            throw new RuntimeException('Composer loaded unexpected response source path');
        }
    }
}
(new \think\App($root))->setEnvName('poc')->initialize();
if (strtolower((string)\think\facade\Config::get('cache.default')) !== 'file'
    || realpath((string)\think\facade\Config::get('cache.stores.file.path')) !== $cachePath
    || (string)\think\facade\Config::get('cache.stores.file.prefix') !== '') {
    throw new RuntimeException('CLI effective File cache differs from HTTP root');
}

$markers = [
    'A' => ['mobile' => 'CDXPARMOB426CA4B0', 'cash' => 'CDXPAY112FC189', 'subsidy' => 'CDXPAY7E0B0221'],
    'B' => ['mobile' => 'CDXPARMOB838A9419', 'cash' => 'CDXPAY72BEB125', 'subsidy' => 'CDXPAYD914599B'],
    'C' => ['mobile' => 'CDXPARMOB9411C2CB', 'cash' => 'CDXPAY661BEE6B', 'subsidy' => 'CDXPAY03721E84'],
];
$marker = $markers[$target][$role];
$table = static function (string $name) use ($dialect): string {
    return $dialect === 'dm' ? 'YOSHOP_' . $name : 'yoshop_' . strtolower($name);
};
$sTable = static function (string $name) use ($dialect): string {
    return $dialect === 'dm' ? 'YDY_' . $name : 'ydy_' . strtolower($name);
};
$rows = static function (PDO $pdo, string $sql, array $bind = []): array {
    $statement = $pdo->prepare($sql);
    $statement->execute($bind);
    return $statement->fetchAll(PDO::FETCH_ASSOC);
};
$digest = static function (array $value): string {
    ksort($value);
    return hash('sha256', json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
};
$userRows = $rows($ai, 'SELECT * FROM ' . $table('USER')
    . ' WHERE NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0', [$marker]);
if (count($userRows) !== 1 || (int)$userRows[0]['user_id'] <= 0) {
    throw new RuntimeException('Owned synthetic account not uniquely present');
}
$userId = (int)$userRows[0]['user_id'];
$storeRows = $rows($store, 'SELECT ID,PASSWORD_VERSION FROM ' . $sTable('USER')
    . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$userId]);
if (count($storeRows) !== 1) {
    throw new RuntimeException('Owned synthetic Store account mapping not unique');
}
$passwordVersion = (int)$storeRows[0]['password_version'];
$sourceTable = $role === 'mobile' ? $table('USER_BALANCE_LOG')
    : ($role === 'subsidy' ? $table('USER_SUBSIDY_LOG') : $table('RECHARGE_ORDER'));
$sourceRows = $rows($ai, 'SELECT * FROM ' . $sourceTable
    . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]);
if (count($sourceRows) < 1 || count($sourceRows) > 20) {
    throw new RuntimeException('Owned source row count outside audited range');
}
$sourceFingerprint = hash('sha256', json_encode($sourceRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$userFingerprint = $digest($userRows[0]);
$sourceIds = array_map(static function (array $row) use ($role): int {
    return (int)$row[$role === 'cash' ? 'order_id' : 'log_id'];
}, $sourceRows);
$sourceById = [];
foreach ($sourceRows as $row) {
    $sourceById[(int)$row[$role === 'cash' ? 'order_id' : 'log_id']] = $row;
}
$toCents = static function ($value): ?int {
    if (!is_string($value) && !is_int($value) && !is_float($value)) return null;
    $text = (string)$value;
    if (preg_match('/^(-?)(\d+)(?:\.(\d{1,2}))?$/', $text, $match) !== 1) return null;
    $cents = ((int)$match[2] * 100) + (int)str_pad($match[3] ?? '', 2, '0');
    return ($match[1] ?? '') === '-' ? -$cents : $cents;
};
$assertMoney = static function (array $actual, array $expected, array $fields) use ($toCents): bool {
    foreach ($fields as $field) {
        if (!array_key_exists($field, $actual) || !array_key_exists($field, $expected)
            || $toCents($actual[$field]) === null
            || $toCents($actual[$field]) !== $toCents($expected[$field])) {
            return false;
        }
    }
    return true;
};
$fixedScaleStrings = static function (array $actual, array $fields): bool {
    foreach ($fields as $field) {
        if (!isset($actual[$field]) || !is_string($actual[$field])
            || preg_match('/^-?\d+\.\d{2}$/D', $actual[$field]) !== 1) {
            return false;
        }
    }
    return true;
};
$picked = $sourceRows[0];
if ($role !== 'cash') {
    foreach ($sourceRows as $row) {
        if ((int)($row['scene'] ?? 0) === 40) { $picked = $row; break; }
    }
}
$date = $role !== 'cash' ? (int)$picked['create_time'] : time();
$year = (int)date('Y', $date);
$month = (int)date('n', $date);

$token = 'CDXREAD8T' . strtoupper(bin2hex(random_bytes(18)));
$journalDir = sys_get_temp_dir() . '/cdx-ai-read8-' . strtolower(bin2hex(random_bytes(8)));
if (!mkdir($journalDir, 0700) || !chmod($journalDir, 0700)) {
    throw new RuntimeException('Private token journal unavailable');
}
$journal = $journalDir . '/token.json';
$journalData = ['target' => $target, 'role' => $role, 'root' => $root,
    'token' => $token, 'created_at' => time(), 'expires_at' => time() + 300];
if (file_put_contents($journal, json_encode($journalData, JSON_UNESCAPED_SLASHES), LOCK_EX) === false
    || !chmod($journal, 0600)) {
    throw new RuntimeException('Private token journal write failed');
}
$result = ['target' => $target, 'role' => $role, 'source_row_count' => count($sourceRows),
    'cases' => [], 'journal' => $journal, 'cache_cleanup' => 'not_started'];
$tokenSaved = false;
try {
    $model = \app\api\model\User::detail($userId);
    if (empty($model) || (int)$model['user_id'] !== $userId) {
        throw new RuntimeException('Owned synthetic login model missing');
    }
    $tokenSaved = \think\facade\Cache::set($token, [
        'user' => $model, 'store_id' => 10001, 'is_login' => true,
        'password_version' => $passwordVersion, 'login_type' => 'sms',
    ], 300);
    if (!$tokenSaved || !\think\facade\Cache::has($token)) {
        throw new RuntimeException('Private short token unavailable');
    }
    $routes = $role === 'mobile'
        ? [['name' => 'balance_list', 'path' => '/api/balance.Log/list',
            'query' => ['scene' => 0, 'year' => $year, 'month' => $month]],
           ['name' => 'balance_detail', 'path' => '/api/balance.Log/detail',
            'query' => ['logId' => (int)$picked['log_id']]]]
        : ($role === 'subsidy'
            ? [['name' => 'subsidy_list', 'path' => '/api/subsidy.Log/list',
                'query' => ['scene' => 0, 'year' => $year, 'month' => $month]],
               ['name' => 'subsidy_detail', 'path' => '/api/subsidy.Log/detail',
                'query' => ['logId' => (int)$picked['log_id']]]]
            : [['name' => 'recharge_order_list', 'path' => '/api/recharge.Order/list', 'query' => []],
               ['name' => 'recharge_center', 'path' => '/api/Recharge/center',
                'query' => ['client' => 'MP-WEIXIN']]]);
    foreach ($routes as $route) {
        $query = http_build_query(array_merge(['storeId' => 10001], $route['query']));
        $context = stream_context_create(['http' => [
            'method' => 'GET', 'timeout' => 15, 'ignore_errors' => true,
            'header' => 'Access-Token: ' . $token . "\r\n"
                . "platform: MP-WEIXIN\r\nversion: 2.0.51\r\nenv-version: develop\r\n",
        ]]);
        $start = microtime(true);
        $stream = @fopen('http://127.0.0.1:' . $port . $route['path']
            . '?' . $query, 'rb', false, $context);
        if ($stream === false) {
            throw new RuntimeException('Bounded owned-audit HTTP stream unavailable');
        }
        try {
            $raw = stream_get_contents($stream, 1048577);
        } finally {
            fclose($stream);
        }
        if (!is_string($raw) || strlen($raw) > 1048576) {
            throw new RuntimeException('Owned-audit HTTP response exceeded one MiB');
        }
        $elapsed = (int)round((microtime(true) - $start) * 1000);
        $http = isset($http_response_header[0])
            && preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0], $match)
            ? (int)$match[1] : 0;
        $body = is_string($raw) ? json_decode($raw, true) : null;
        $data = is_array($body) ? ($body['data'] ?? null) : null;
        $case = ['http' => $http, 'business_status' => is_array($body) ? ($body['status'] ?? null) : null,
            'elapsed_ms' => $elapsed, 'data_keys' => is_array($data) ? array_keys($data) : [],
            'selected_types' => []];
        if ($http !== 200 || !is_array($body) || (int)($body['status'] ?? -1) !== 200
            || !is_array($data) || $elapsed >= 15000) {
            $message = is_array($body) ? (string)($body['message'] ?? '') : '';
            $case['safe_error_message'] = strlen($message) <= 200
                && preg_match('/^[\p{Han}\s，。！：、（）\-]+$/u', $message) === 1
                ? $message : null;
            $case['error_message_sha256'] = hash('sha256', $message);
            $result['cases'][$route['name']] = $case;
            throw new RuntimeException('Positive owned-audit HTTP contract failed');
        }
        if (substr($route['name'], -5) === '_list') {
            $list = $data['list'] ?? null;
            $items = is_array($list) ? ($list['data'] ?? null) : null;
            if (!is_array($items) || count($items) < 1) {
                $result['cases'][$route['name']] = $case;
                throw new RuntimeException('Owned-audit list did not return source rows');
            }
            $found = false;
            foreach ($items as $item) {
                if (!is_array($item) || !in_array((int)($item[$role === 'cash' ? 'order_id' : 'log_id'] ?? 0),
                    $sourceIds, true)) continue;
                $found = true;
                $source = $sourceById[(int)$item[$role === 'cash' ? 'order_id' : 'log_id']];
                $moneyFields = $role === 'cash'
                    ? ['pay_price', 'gift_money', 'actual_money', 'recharg_before',
                        'recharg_after', 'refund_money', 'use_money', 'refundable_money']
                    : ($role === 'mobile' ? ['money', 'balance_after']
                        : ['money', 'subsidy_balance_after']);
                $case['money_matches_own_source'] = $assertMoney($item, $source, $moneyFields);
                if (!$case['money_matches_own_source']) {
                    throw new RuntimeException('List money differs from owned source');
                }
                if ($target !== 'A' && $role !== 'cash') {
                    $case['fixed_scale_money_types'] = $fixedScaleStrings($item, $moneyFields);
                    if (!$case['fixed_scale_money_types']) {
                        throw new RuntimeException('List money JSON type/scale differs from fixed-scale contract');
                    }
                }
                $case['selected_types'] = array_map('gettype', array_intersect_key($item,
                    array_fill_keys(['log_id', 'order_id', 'scene', 'money', 'balance_after',
                        'subsidy_balance_after', 'pay_price', 'gift_money', 'actual_money',
                        'withdrawable_amount', 'activity_principal_amount',
                        'recharg_before', 'recharg_after', 'refund_money', 'use_money',
                        'refundable_money', 'custom_money', 'pay_status',
                        'create_time'], true)));
                break;
            }
            $case['source_id_in_page'] = $found;
            $case['page_row_count'] = count($items);
            if (!$found) {
                $result['cases'][$route['name']] = $case;
                throw new RuntimeException('Owned source ID absent from list page');
            }
        } elseif (substr($route['name'], -7) === '_detail') {
            $detail = $data['data'] ?? null;
            if (!is_array($detail)
                || (int)($detail['log_id'] ?? 0) !== (int)$picked['log_id']) {
                $result['cases'][$route['name']] = $case;
                throw new RuntimeException('Owned source log ID absent from detail');
            }
            $case['source_id_match'] = true;
            $moneyFields = $role === 'mobile' ? ['money', 'balance_after']
                : ['money', 'subsidy_balance_after'];
            $case['money_matches_own_source'] = $assertMoney($detail, $picked, $moneyFields);
            if (!$case['money_matches_own_source']
                || (int)($detail['scene'] ?? 0) !== (int)$picked['scene']
                || (int)($detail['order_id'] ?? 0) !== (int)$picked['order_id']) {
                throw new RuntimeException('Detail data differs from owned source');
            }
            if ($target !== 'A' && $role !== 'cash') {
                $case['fixed_scale_money_types'] = $fixedScaleStrings($detail, $moneyFields);
                if (!$case['fixed_scale_money_types']) {
                    throw new RuntimeException('Detail money JSON type/scale differs from fixed-scale contract');
                }
            }
            if ((int)$picked['scene'] === 40 && (int)$picked['order_id'] > 0) {
                $refundRows = $rows($store, 'SELECT ORDER_NO FROM ' . $sTable('MEAL_ORDER_REFUND')
                    . ' WHERE ID=? AND IS_DELETE=0', [(int)$picked['order_id']]);
                $case['cross_store_refund_order_no_match'] = count($refundRows) === 1
                    && (string)($detail['order_no'] ?? '') === (string)$refundRows[0]['order_no'];
                if (!$case['cross_store_refund_order_no_match']) {
                    throw new RuntimeException('Cross-Store refund reference mismatch');
                }
            }
            $case['selected_types'] = array_map('gettype', array_intersect_key($detail,
                array_fill_keys(['log_id', 'order_id', 'scene', 'money', 'balance_after',
                    'subsidy_balance_after', 'order_no', 'pay_method', 'create_time'], true)));
        } elseif ($route['name'] === 'recharge_center') {
            $personal = $data['personal'] ?? null;
            if (!is_array($personal) || (int)($personal['user_id'] ?? 0) !== $userId
                || !isset($data['setting'], $data['planList'], $data['paymentMethods'])) {
                $result['cases'][$route['name']] = $case;
                throw new RuntimeException('Owned user/setting center contract mismatch');
            }
            $case['owned_user_match'] = true;
            $case['plan_count'] = is_array($data['planList']) ? count($data['planList']) : null;
            $case['payment_method_count'] = is_array($data['paymentMethods']) ? count($data['paymentMethods']) : null;
        }
        $result['cases'][$route['name']] = $case;
    }
    $afterUser = $rows($ai, 'SELECT * FROM ' . $table('USER')
        . ' WHERE USER_ID=? AND NICK_NAME=? AND STORE_ID=10001', [$userId, $marker]);
    $afterSource = $rows($ai, 'SELECT * FROM ' . $sourceTable
        . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]);
    if (count($afterUser) !== 1 || $digest($afterUser[0]) !== $userFingerprint
        || hash('sha256', json_encode($afterSource, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !== $sourceFingerprint) {
        throw new RuntimeException('Read route changed audit source rows');
    }
    $result['source_rows_unchanged'] = true;
} catch (Throwable $error) {
    $result['error_class'] = get_class($error);
    $result['error_kind'] = 'positive_or_source_assertion_failed';
} finally {
    if ($tokenSaved) {
        try {
            \think\facade\Cache::delete($token);
            $result['cache_cleanup'] = \think\facade\Cache::has($token)
                ? 'failed' : 'first_process_zero';
        } catch (Throwable $error) {
            $result['cache_cleanup'] = 'failed';
        }
    } else {
        $result['cache_cleanup'] = 'never_set';
    }
    try {
        $afterUser = $rows($ai, 'SELECT * FROM ' . $table('USER')
            . ' WHERE USER_ID=? AND NICK_NAME=? AND STORE_ID=10001', [$userId, $marker]);
        $afterSource = $rows($ai, 'SELECT * FROM ' . $sourceTable
            . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]);
        $result['source_rows_unchanged_even_on_error'] = count($afterUser) === 1
            && $digest($afterUser[0]) === $userFingerprint
            && hash('sha256', json_encode($afterSource,
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === $sourceFingerprint;
    } catch (Throwable $error) {
        $result['source_rows_unchanged_even_on_error'] = null;
    }
}
$result['status'] = !isset($result['error_class'])
    && $result['cache_cleanup'] === 'first_process_zero'
    ? 'pending_independent_token_zero' : 'failed_or_deferred';
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($result['status'] === 'pending_independent_token_zero' ? 0 : 3);
