<?php
declare(strict_types=1);

// Single-use, read-only verification of the existing B loopback POC.
// Never print database secrets, UUIDs, user IDs, nicknames, or HTTP bodies.
const B_ROOT = '/private/tmp/zhct-http-ai-macos.x2CCZX';
const B_SOURCE = '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api';
const B_ENV = '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local';
const B_MODEL_SHA = '7a5091ecad4a8ee4d55630da309114a9d927f6185bbf7313859ede17903ea025';
const B_CONFIG_SHA = 'e4c02e8067e7d5038a1db68a059d2160cfad00d28ca1a82d07cb4ff19de99dd7';
const B_AI_ID_SHA = '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b';
const B_STORE_ID_SHA = '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401';
const B_DEPT_SHA = '9b392fa3664b286bfcd27b1f8c6bc3f8e5cc0f4ab0ea495d764edf2e40251779';
const A_SOURCE_SHA = '56a61bf8de501bec11170b6300d30854fea0493d9e2c07a66479bb82cc3cc187';
const A_TYPES_SHA = '023b4f1032ea565348e21df173749ea347a8278d7e9e73d87709850360d917b8';
const A_BODY_SHAS = [
    'weightRank' => '215f5608d8d38538f7ada4c77a1b4c30c4809e50dcd172d95c0e8f46150ed50d',
    'dietRank' => '8cb3c0c9ccd1468e9e9ec0ad82acf43df0c662bb978978e09c862670be8a7c79',
    'courseRank' => 'f8450e8c100e039efd7728acae27ff8ee9e95d266d6265d1429c9f8085186745',
];

if ($argc !== 2 || $argv[1] !== '--execute'
    || getenv('CDX_B_SCORE_FINAL_APPROVED') !== 'THREE_READ_ONCE_V1'
    || !hash_equals(hash_file('sha256', __FILE__),
        (string)getenv('CDX_B_SCORE_FINAL_SHA'))) {
    fwrite(STDERR, "DENIED: reviewed B read-only phase required\n");
    exit(78);
}
if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 70500) {
    fwrite(STDERR, "DENIED: PHP7.4 required\n");
    exit(78);
}

$stage = 'static-boundary';
set_error_handler(static function (int $severity): bool {
    if (!(error_reporting() & $severity)) return false;
    throw new ErrorException('redacted warning', 0, $severity);
});
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'halt', 'stage' => $stage,
        'class' => get_class($error),
        'error_sha256' => hash('sha256', $error->getMessage())]), PHP_EOL;
    exit(3);
});
set_time_limit(65);

function bAssertService(): void
{
    if (!is_dir(B_ROOT) || is_link(B_ROOT)
        || (fileperms(B_ROOT) & 0777) !== 0700
        || realpath(B_ROOT . '/app') !== realpath(B_SOURCE . '/app')
        || realpath(B_ROOT . '/vendor') !== realpath(B_SOURCE . '/vendor')) {
        throw new RuntimeException('fixed B root/source boundary drift');
    }
    $listeners = (string)shell_exec('/usr/sbin/lsof -nP -iTCP:18101 -sTCP:LISTEN 2>/dev/null');
    $command = (string)shell_exec('/bin/ps -p 13127 -o command= 2>/dev/null');
    if (!preg_match('/\b13127\b/', $listeners)
        || strpos($listeners, '0xc1b5193ecc2ef2dc') === false
        || strpos($command, '-S 127.0.0.1:18101') === false
        || strpos($command, '/tmp/zhct-http-ai-macos.x2CCZX/public') === false) {
        throw new RuntimeException('fixed B process/socket ownership drift');
    }
    $paths = [
        'app/api/controller/user/Score.php' => '6260b430fb796fb01bb259ef97d1b61649ded459f40fae1a0b7cc40fb611bcc7',
        'app/api/model/user/UserScore.php' => B_MODEL_SHA,
        'app/common/model/zhct/Staff.php' => '43488d567bf658f78b8131043711fe1ab37baa8699b053a22d2540de144468d8',
        'app/common/model/user/UserScore.php' => '3cc36a9b65dcde9cc1d7faf936b7ca6e13794362fede343cf4170165afb11136',
        'app/api/controller/Controller.php' => '3bf4829abc99399472e8f7f9a6b95c56dc814f656c0aab3fceb606cf1dfab07c',
        'cores/middleware/AppLog.php' => 'f42505e34c0f4d52f5758b77102197357043ec2426bada2510b858d52b5fc62f',
        'config/database.php' => '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02',
        'config/cache.php' => 'ccf1c56eb4178f3eaad8775e26a27742b2cacf662b46fb058cbb91d23783b8c6',
        'config/log.php' => '16025200f4a396d32a181151da621a017352c1e8c648fe3c84e66c961d878e9a',
        'external_stubs.php' => 'ed0ea9f23c70e504cf5331b7a95fe2a9fd6704beabf254a474efbe8ecb4d59a0',
    ];
    foreach ($paths as $path => $sha) {
        if (!is_file(B_ROOT . '/' . $path)
            || hash_file('sha256', B_ROOT . '/' . $path) !== $sha) {
            throw new RuntimeException('fixed B source/config/stub drift');
        }
    }
    require_once B_ROOT . '/vendor/autoload.php';
    $actual = realpath((new ReflectionClass(\app\api\model\user\UserScore::class))->getFileName());
    if ($actual !== realpath(B_SOURCE . '/app/api/model/user/UserScore.php')
        || hash_file('sha256', $actual) !== B_MODEL_SHA) {
        throw new RuntimeException('actual Composer model drift');
    }
    $dir = B_ROOT . '/config';
    $names = array_values(array_diff(scandir($dir) ?: [], ['.', '..']));
    sort($names);
    if (count($names) !== 23) throw new RuntimeException('B config count drift');
    $tree = [];
    foreach ($names as $name) {
        $file = $dir . '/' . $name;
        if (!preg_match('/^[a-z_]+\.php$/D', $name) || !is_file($file)
            || is_link($file) || (fileperms($file) & 0777) !== 0644) {
            throw new RuntimeException('B config type/mode drift');
        }
        $tree[] = $name . "\t" . hash_file('sha256', $file) . "\t0644";
    }
    sort($tree);
    if (hash('sha256', implode("\n", $tree)) !== B_CONFIG_SHA) {
        throw new RuntimeException('B restored config tree drift');
    }
}

function bStubState(): string
{
    $file = B_ROOT . '/runtime/poc-stub-hits.log';
    if (!file_exists($file) && !is_link($file)) return 'absent';
    if (!is_file($file) || is_link($file)) {
        throw new RuntimeException('B stub hit evidence type drift');
    }
    return (string)filesize($file) . ':' . hash_file('sha256', $file);
}

function bConnections(): array
{
    if (realpath(B_ENV) !== B_ENV) throw new RuntimeException('local env path drift');
    $ini = parse_ini_file(B_ENV, true, INI_SCANNER_RAW);
    if (!is_array($ini) || !isset($ini['DATABASE'], $ini['DATABASE_ZHCT'])) {
        throw new RuntimeException('code-local dev config unavailable');
    }
    $out = [];
    foreach ([['DATABASE', B_AI_ID_SHA], ['DATABASE_ZHCT', B_STORE_ID_SHA]] as [$name, $sha]) {
        $s = $ini[$name];
        if (strtolower((string)($s['TYPE'] ?? '')) !== 'mysql'
            || strtolower((string)($s['CHARSET'] ?? '')) !== 'utf8') {
            throw new RuntimeException('code-local dev dialect/charset drift');
        }
        foreach (['HOSTNAME', 'HOSTPORT', 'DATABASE', 'USERNAME', 'PASSWORD'] as $key) {
            if ((string)($s[$key] ?? '') === '') throw new RuntimeException('dev DB config incomplete');
        }
        $pdo = new PDO('mysql:host=' . $s['HOSTNAME'] . ';port=' . (int)$s['HOSTPORT']
            . ';dbname=' . $s['DATABASE'] . ';charset=utf8',
            $s['USERNAME'], $s['PASSWORD'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
        $identity = (string)$pdo->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())"
        )->fetchColumn();
        if (!hash_equals($sha, hash('sha256', $identity))) {
            throw new RuntimeException('code-local dev DB identity drift');
        }
        $out[] = $pdo;
    }
    return $out;
}

function bRows(PDO $db, string $sql, array $args, int $cap): array
{
    $start = microtime(true);
    $stmt = $db->prepare($sql);
    $stmt->execute($args);
    $rows = [];
    while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
        $rows[] = array_change_key_case($row, CASE_LOWER);
        if (count($rows) > $cap) throw new RuntimeException('rank source row cap exceeded');
    }
    $stmt->closeCursor();
    if (microtime(true) - $start > 5.0) {
        throw new RuntimeException('rank source SELECT exceeded five seconds');
    }
    return $rows;
}

function bDepartmentIds(PDO $store, string $uuid): array
{
    $rows = bRows($store, 'SELECT DISTINCT s.AI_USER_ID FROM ydy_staff s'
        . ' JOIN ydy_staff_department sd ON sd.STAFF_UUID=s.UUID'
        . ' WHERE sd.DEPARTMENT_UUID=? AND s.IS_SHOW=1 AND s.AI_USER_ID>0'
        . ' ORDER BY s.AI_USER_ID LIMIT 1001', [$uuid], 1001);
    if (!$rows || count($rows) > 1000) throw new RuntimeException('department users out of bound');
    return array_map(static function (array $r): int {
        $id = (int)($r['ai_user_id'] ?? 0);
        if ($id <= 0) throw new RuntimeException('department AI ID invalid');
        return $id;
    }, $rows);
}

function bOriginal(PDO $ai, array $ids, int $type): array
{
    $marks = implode(',', array_fill(0, count($ids), '?'));
    $sql = 'SELECT s.USER_ID,s.STORE_ID,s.TYPE,s.SCORE,u.NICK_NAME'
        . ' FROM yoshop_user_score s JOIN yoshop_user u'
        . ' ON u.USER_ID=s.USER_ID AND u.IS_DELETE=0'
        . ' WHERE s.STORE_ID=10001 AND s.TYPE=? AND s.USER_ID IN (' . $marks . ')'
        . ' ORDER BY s.SCORE DESC,s.USER_ID ASC LIMIT 1001';
    $rows = bRows($ai, $sql, array_merge([$type], $ids), 1001);
    if (!$rows || count($rows) > 1000) throw new RuntimeException('rank score rows out of bound');
    return $rows;
}

function bTuple(array $row, bool $response): array
{
    foreach (['user_id', 'store_id', 'type', 'score', 'nick_name'] as $key) {
        if (!array_key_exists($key, $row)) throw new RuntimeException('rank row key missing');
    }
    if ($response && (!array_key_exists('bestCount', $row)
        || array_key_exists('bestcount', $row)
        || !array_key_exists('nickname', $row)
        || (string)$row['bestCount'] !== (string)$row['score']
        || (string)$row['nickname'] !== (string)$row['nick_name']
        || gettype($row['bestCount']) !== gettype($row['score'])
        || gettype($row['nickname']) !== gettype($row['nick_name']))) {
        throw new RuntimeException('bestCount/nickname alias contract drift');
    }
    return [(string)$row['user_id'], (string)$row['store_id'],
        (string)$row['type'], (string)$row['score'], (string)$row['nick_name']];
}

function bMultiset(array $rows, bool $response): array
{
    $out = [];
    foreach ($rows as $row) {
        $key = json_encode(bTuple($row, $response),
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($key)) throw new RuntimeException('rank canonical JSON failed');
        $out[$key] = ($out[$key] ?? 0) + 1;
    }
    ksort($out, SORT_STRING);
    return $out;
}

function bGet(string $action): array
{
    $url = 'http://127.0.0.1:18101/api/user.score/' . $action . '?limit=50';
    $context = stream_context_create(['http' => ['method' => 'GET',
        'timeout' => 15, 'follow_location' => 0, 'ignore_errors' => true,
        'header' => "platform: MP-WEIXIN\r\nversion: 2.0.51\r\nenv-version: develop\r\nAccess-Token: \r\nContent-Type: application/json;charset=utf-8\r\n"]]);
    $handle = @fopen($url, 'rb', false, $context);
    if ($handle === false) throw new RuntimeException('B loopback GET failed');
    $raw = stream_get_contents($handle, 1048577);
    $meta = stream_get_meta_data($handle);
    fclose($handle);
    if (!is_string($raw) || strlen($raw) > 1048576 || !empty($meta['timed_out'])) {
        throw new RuntimeException('B GET timeout or response cap');
    }
    $line = (string)(($meta['wrapper_data'] ?? [])[0] ?? '');
    if (!preg_match('/^HTTP\/\S+\s+200\b/', $line)) {
        throw new RuntimeException('B GET HTTP non-200');
    }
    $json = json_decode($raw, true);
    if (!is_array($json)) throw new RuntimeException('B GET non-JSON');
    return $json;
}

$stage = 'fixed-B-service-and-code-local-dev-identity';
bAssertService();
$stubBefore = bStubState();
[$ai, $store] = bConnections();
require_once __DIR__ . '/score_rank_candidate_lib.php';
if (hash_file('sha256', __DIR__ . '/score_rank_candidate_lib.php')
    !== '34ef24773101dcf09631dcc186940d060b1ebadf730bf94671552a81b0498004') {
    throw new RuntimeException('reviewed candidate library drift');
}
$candidate = cdxDeriveRankDepartment($ai, $store, false);
if ($candidate['sha256'] !== B_DEPT_SHA
    || $candidate['counts'] !== [1 => 14, 2 => 10, 3 => 5]
    || $candidate['source_users_by_type'] !== [1 => 26, 2 => 18, 3 => 7]
    || $candidate['links'] !== 39) {
    throw new RuntimeException('frozen natural department sample drift');
}
$uuid = $candidate['uuid'];
unset($candidate['uuid']);
$idsBefore = bDepartmentIds($store, $uuid);
$sourceBefore = [];
$sourceShas = [];
foreach ([1, 2, 3] as $type) {
    $sourceBefore[$type] = bOriginal($ai, $idsBefore, $type);
    $sourceShas[$type] = hash('sha256', json_encode(
        bMultiset($sourceBefore[$type], false), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
if (array_map('count', $sourceBefore) !== [1 => 16, 2 => 10, 3 => 5]) {
    throw new RuntimeException('A/B frozen natural source row counts drift');
}

$stage = 'three-whitelisted-GETs';
$results = [];
foreach (['weightRank' => 1, 'dietRank' => 2, 'courseRank' => 3] as $action => $type) {
    bAssertService();
    if (bStubState() !== $stubBefore) throw new RuntimeException('stub activity before GET');
    $body = bGet($action);
    if (bStubState() !== $stubBefore) throw new RuntimeException('stub activity after GET');
    $list = $body['data']['list']['data'] ?? null;
    if (($body['status'] ?? null) !== 200 || !is_string($body['message'] ?? null)
        || !array_key_exists('trace_id', $body)
        || !is_array($body['data']['list'] ?? null)
        || !is_array($list) || count($list) !== count($sourceBefore[$type])) {
        throw new RuntimeException('B business envelope/nonempty count drift');
    }
    $types = null;
    $lastScore = null;
    foreach ($list as $row) {
        if (!is_array($row) || (int)($row['store_id'] ?? 0) !== 10001
            || (int)($row['type'] ?? 0) !== $type) {
            throw new RuntimeException('B row scope/type drift');
        }
        $keys = array_keys($row);
        sort($keys);
        if ($keys !== ['bestCount', 'nick_name', 'nickname', 'score',
            'store_id', 'type', 'user_id']) {
            throw new RuntimeException('B response row key set drift');
        }
        bTuple($row, true);
        $score = (int)$row['score'];
        if ($lastScore !== null && $score > $lastScore) {
            throw new RuntimeException('B score ordering drift');
        }
        $lastScore = $score;
        $itemTypes = array_map('gettype', $row);
        ksort($itemTypes);
        foreach (['nick_name', 'nickname'] as $nameKey) {
            if (!in_array($itemTypes[$nameKey], ['string', 'NULL'], true)) {
                throw new RuntimeException('B nickname JSON type invalid');
            }
            $itemTypes[$nameKey] = 'string|null';
        }
        if ($types !== null && $types !== $itemTypes) {
            throw new RuntimeException('B mixed JSON field types');
        }
        $types = $itemTypes;
    }
    $sourceMulti = bMultiset($sourceBefore[$type], false);
    $responseMulti = bMultiset($list, true);
    if ($sourceMulti !== $responseMulti) {
        throw new RuntimeException('B own original rows/response mismatch');
    }
    $shell = $body['data']['list'];
    unset($shell['data']);
    $total = count($sourceBefore[$type]);
    if ((int)($shell['total'] ?? -1) !== $total
        || (int)($shell['per_page'] ?? -1) !== 50
        || (int)($shell['current_page'] ?? -1) !== 1
        || (int)($shell['last_page'] ?? -1) !== 1) {
        throw new RuntimeException('B pagination values drift');
    }
    $shellTypes = array_map('gettype', $shell);
    ksort($shellTypes);
    $topTypes = ['status' => gettype($body['status']),
        'message' => gettype($body['message']),
        'data' => gettype($body['data']),
        'trace_id' => gettype($body['trace_id'])];
    unset($body['trace_id']);
    $bodySha = hash('sha256', json_encode($body,
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    if (!hash_equals(A_BODY_SHAS[$action], $bodySha)) {
        throw new RuntimeException('A/B full body excluding trace differs');
    }
    $results[$action] = [
        'source_rows' => $total, 'response_rows' => count($list),
        'source_multiset_sha256' => $sourceShas[$type],
        'response_multiset_sha256' => hash('sha256', json_encode($responseMulti,
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
        'field_types_sha256' => hash('sha256', json_encode($types)),
        'top_and_pagination_types_sha256' => hash('sha256',
            json_encode([$topTypes, $shellTypes])),
        'body_without_trace_sha256' => $bodySha,
    ];
}

$stage = 'independent-original-table-after-readback';
$idsAfter = bDepartmentIds($store, $uuid);
if ($idsAfter !== $idsBefore) throw new RuntimeException('Store mapping changed after GETs');
foreach ([1, 2, 3] as $type) {
    $after = bOriginal($ai, $idsAfter, $type);
    $sha = hash('sha256', json_encode(bMultiset($after, false),
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    if (!hash_equals($sourceShas[$type], $sha)) {
        throw new RuntimeException('AI source rows changed after GETs');
    }
}
$sourceAggregate = hash('sha256', json_encode(array_map(
    static function (array $r): array {
        return [$r['source_rows'], $r['source_multiset_sha256']];
    }, $results), JSON_UNESCAPED_SLASHES));
$responseAggregate = hash('sha256', json_encode(array_map(
    static function (array $r): array {
        return [$r['response_rows'], $r['response_multiset_sha256']];
    }, $results), JSON_UNESCAPED_SLASHES));
$typesAggregate = hash('sha256', json_encode(array_map(
    static function (array $r): array {
        return [$r['field_types_sha256'], $r['top_and_pagination_types_sha256']];
    }, $results), JSON_UNESCAPED_SLASHES));
if (!hash_equals(A_SOURCE_SHA, $sourceAggregate)
    || !hash_equals(A_SOURCE_SHA, $responseAggregate)
    || !hash_equals(A_TYPES_SHA, $typesAggregate)) {
    throw new RuntimeException('A/B aggregate original/response/type drift');
}
bAssertService();
if (bStubState() !== $stubBefore) throw new RuntimeException('B stub changed after GETs');
echo json_encode(['status' => 'B-final-score-three-read-limited-pass',
    'role' => 'B-compatible-local-MySQL',
    'operations' => 3, 'passed' => 3, 'model_sha256' => B_MODEL_SHA,
    'department_sha256' => B_DEPT_SHA, 'results' => $results,
    'source_aggregate_sha256' => $sourceAggregate,
    'response_aggregate_sha256' => $responseAggregate,
    'types_aggregate_sha256' => $typesAggregate,
    'A_frozen_response_exact' => true, 'stub_unchanged' => true,
    'names_ids_uuid_output' => false, 'business_dml' => false],
    JSON_UNESCAPED_SLASHES), PHP_EOL;
