<?php
declare(strict_types=1);

// Bounded synthetic read-contract probe. No physical device, external call, or real user.
$mode = (string)($argv[1] ?? '');
$dialect = (string)getenv('POC_DIALECT');
$root = (string)getenv('POC_TARGET_ROOT');
$baseUrl = rtrim((string)getenv('POC_BASE_URL'), '/');
if (!in_array($mode, ['--preflight', '--execute', '--audit'], true)
    || !in_array($dialect, ['mysql', 'dm'], true) || $root === '' || $baseUrl === '') {
    exit(78);
}
if ($dialect === 'mysql') {
    $path = (string)getenv('POC_MYSQL_LOCAL_ENV');
    if ($path !== '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local') {
        throw new RuntimeException('Only code local development MySQL is allowed');
    }
    $ini = parse_ini_file($path, true, INI_SCANNER_RAW);
    if (!is_array($ini) || !isset($ini['DATABASE'], $ini['DATABASE_ZHCT'])) {
        throw new RuntimeException('Code local dual database config unavailable');
    }
    foreach (['DATABASE' => 'POC_AI_', 'DATABASE_ZHCT' => 'POC_'] as $section => $prefix) {
        if (strtolower((string)($ini[$section]['TYPE'] ?? '')) !== 'mysql') {
            throw new RuntimeException('Non-MySQL local config');
        }
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
            'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $from => $to) {
            putenv($prefix . $to . '=' . (string)$ini[$section][$from]);
        }
    }
} else {
    $pid = (int)getenv('POC_IMPORT_SERVER_PID');
    $cmdline = $pid > 0 ? @file_get_contents('/proc/' . $pid . '/cmdline') : false;
    if (!is_string($cmdline) || strpos($cmdline, '127.0.0.1:18102') === false
        || strpos($cmdline, $root . '/public') === false) {
        throw new RuntimeException('Isolated C HTTP process identity mismatch');
    }
    $processEnv = @file_get_contents('/proc/' . $pid . '/environ');
    if (!is_string($processEnv)) throw new RuntimeException('C process config unavailable');
    foreach (explode("\0", $processEnv) as $entry) {
        if (strpos($entry, 'POC_') === 0 && strpos($entry, '=') !== false
            && strpos($entry, 'POC_TARGET_ROOT=') !== 0
            && strpos($entry, 'POC_BASE_URL=') !== 0) putenv($entry);
    }
}
foreach (['POC_AI_HOST', 'POC_AI_PORT', 'POC_AI_SCHEMA', 'POC_AI_USER',
    'POC_AI_PASSWORD', 'POC_HOST', 'POC_PORT', 'POC_SCHEMA', 'POC_USER',
    'POC_PASSWORD'] as $key) {
    if ((string)getenv($key) === '') throw new RuntimeException('Dual database config incomplete');
}
if ($dialect === 'dm' && ((string)getenv('POC_AI_SCHEMA') !== 'AIZHCT_APP'
    || (string)getenv('POC_AI_PORT') !== '5238'
    || (string)getenv('POC_SCHEMA') !== 'ZHCT_APP'
    || (string)getenv('POC_PORT') !== '5237')) {
    throw new RuntimeException('Unexpected C DM schema or port');
}
$boundary = json_decode((string)@file_get_contents($baseUrl . '/__poc_boundary'), true);
if (empty($boundary['external_stubs'])) throw new RuntimeException('External stubs missing');
if (!is_file($root . '/config/cache.php') || !is_dir($root . '/runtime/cache')) {
    throw new RuntimeException('Isolated File cache missing');
}
$sourceFiles = [
    $root . '/app/api/controller/wolland/Rope.php',
    $root . '/app/api/service/WellandRope.php',
    $root . '/app/api/model/User.php',
];
$sourceHashes = [];
foreach ($sourceFiles as $file) {
    if (!is_file($file)) throw new RuntimeException('Selected source unavailable');
    $sourceHashes[] = hash_file('sha256', $file);
}
$sourceHash = hash('sha256', implode('|', $sourceHashes));
$scriptHash = hash_file('sha256', __FILE__);
if ($mode === '--execute' && ((string)getenv('CDX_DEVICE_SCRIPT_SHA') !== $scriptHash
    || (string)getenv('CDX_DEVICE_SOURCE_SHA') !== $sourceHash)) {
    throw new RuntimeException('Code/script changed after preflight');
}

define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName('poc')->initialize();
$loadedService = (new ReflectionClass(\app\api\service\WellandRope::class))->getFileName();
if (!is_string($loadedService) || hash_file('sha256', $loadedService) !== $sourceHashes[1]) {
    throw new RuntimeException('Actual Composer-loaded rope service differs from isolated source');
}
if (strtolower((string)\think\facade\Config::get('cache.default')) !== 'file'
    || realpath((string)\think\facade\Config::get('cache.stores.file.path'))
        !== realpath($root . '/runtime/cache')) {
    throw new RuntimeException('CLI cache does not match isolated HTTP File cache');
}
$pdo = static function (bool $ai) use ($dialect): PDO {
    $prefix = $ai ? 'POC_AI_' : 'POC_';
    $dsn = $dialect === 'dm'
        ? 'dm:host=' . getenv($prefix . 'HOST') . ';port=' . getenv($prefix . 'PORT')
            . ';schema=' . getenv($prefix . 'SCHEMA')
        : 'mysql:host=' . getenv($prefix . 'HOST') . ';port=' . getenv($prefix . 'PORT')
            . ';dbname=' . getenv($prefix . 'SCHEMA') . ';charset=utf8';
    $db = new PDO($dsn, getenv($prefix . 'USER'), getenv($prefix . 'PASSWORD'),
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
    return $db;
};
$ai = $pdo(true);
$store = $pdo(false);
$table = static function (string $name) use ($dialect): string {
    return $dialect === 'dm' ? strtoupper($name) : strtolower($name);
};
$userTable = $table('yoshop_user');
$sessionTable = $table('ydy_measurement_session');
$rawTable = $table('ydy_measurement_session_raw');
$metricTable = $table('ydy_health_record');
$metricDict = $table('ydy_health_metric');
$count = static function (PDO $db, string $sql, array $args = []): int {
    $stmt = $db->prepare($sql);
    $stmt->execute($args);
    return (int)$stmt->fetchColumn();
};
$one = static function (PDO $db, string $sql, array $args = []): array {
    $stmt = $db->prepare($sql);
    $stmt->execute($args);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    return is_array($row) ? array_change_key_case($row, CASE_LOWER) : [];
};
if ($mode === '--preflight') {
    foreach ([$userTable => $ai, $sessionTable => $store,
        $rawTable => $store, $metricTable => $store] as $name => $db) {
        $db->query('SELECT * FROM ' . $name . ' WHERE 1=0');
    }
    $metrics = $count($store, "SELECT COUNT(*) FROM $metricDict WHERE "
        . "METRIC_CODE IN ('jump_count','activity_duration','calories_burned')");
    if ($metrics !== 3) throw new RuntimeException('Rope metric dictionary incomplete');
    echo json_encode(['mode' => 'preflight', 'dialect' => $dialect,
        'schema_hashes' => [hash('sha256', (string)getenv('POC_AI_SCHEMA')),
            hash('sha256', (string)getenv('POC_SCHEMA'))],
        'source_sha256' => $sourceHash, 'script_sha256' => $scriptHash,
        'metric_dictionary' => $metrics, 'external_stubs' => true,
        'business_dml' => 0, 'http' => 0]), PHP_EOL;
    exit(0);
}

if ($mode === '--audit') {
    $journalPath = (string)($argv[2] ?? '');
    if (!is_file($journalPath) || is_link($journalPath)
        || (((int)fileperms($journalPath) & 0077) !== 0)) {
        throw new RuntimeException('Private audit journal unavailable');
    }
    $journal = json_decode((string)file_get_contents($journalPath), true);
    if (!is_array($journal) || ($journal['state'] ?? '') !== 'deleted_needs_audit'
        || !preg_match('/^CDXDEVROPE[0-9A-F]{8}$/', (string)($journal['marker'] ?? ''))
        || (int)($journal['user_id'] ?? 0) <= 0) {
        throw new RuntimeException('Private journal ownership mismatch');
    }
    $userId = (int)$journal['user_id'];
    $sessionId = (int)$journal['session_id'];
    $remaining = [
        'user' => $count($ai, "SELECT COUNT(*) FROM $userTable WHERE USER_ID=? AND NICK_NAME=?",
            [$userId, $journal['marker']]),
        'session' => $count($store, "SELECT COUNT(*) FROM $sessionTable WHERE AI_USER_ID=?",
            [$userId]),
        'raw' => $sessionId > 0
            ? $count($store, "SELECT COUNT(*) FROM $rawTable WHERE SESSION_ID=?", [$sessionId]) : 0,
        'metrics' => $count($store, "SELECT COUNT(*) FROM $metricTable WHERE AI_USER_ID=?",
            [$userId]),
        'token' => \think\facade\Cache::has((string)$journal['token']) ? 1 : 0,
    ];
    if (array_sum($remaining) !== 0) throw new RuntimeException('Synthetic cleanup not zero');
    if (!unlink($journalPath)) throw new RuntimeException('Private journal removal uncertain');
    echo json_encode(['mode' => 'audit', 'dialect' => $dialect,
        'remaining' => $remaining, 'journal_removed' => true]), PHP_EOL;
    exit(0);
}

$marker = 'CDXDEVROPE' . strtoupper(bin2hex(random_bytes(4)));
$token = $marker . '-T-' . strtoupper(bin2hex(random_bytes(10)));
$date = '2027-12-13';
$stoppedAt = strtotime($date . ' 12:00:00') * 1000;
$userId = 0;
$sessionId = 0;
$journalPath = '';
$cleanupError = null;
$sourceBefore = ['user' => $count($ai, "SELECT COUNT(*) FROM $userTable WHERE NICK_NAME=?", [$marker])];
if ($sourceBefore['user'] !== 0) throw new RuntimeException('Marker collision');
$journalPath = tempnam(sys_get_temp_dir(), 'cdx-device-rope-');
if ($journalPath === false || !chmod($journalPath, 0600)) {
    throw new RuntimeException('Private recovery journal unavailable');
}
$journal = ['state' => 'before_insert', 'marker' => $marker, 'token' => $token,
    'user_id' => 0, 'session_id' => 0];
if (file_put_contents($journalPath, json_encode($journal), LOCK_EX) === false) {
    throw new RuntimeException('Private recovery journal write failed');
}
$request = static function (string $path, string $token) use ($baseUrl): array {
    $context = stream_context_create(['http' => [
        'method' => 'GET', 'timeout' => 20, 'ignore_errors' => true,
        'follow_location' => 0,
        'header' => 'Access-Token: ' . $token . "\r\n"
            . "platform: MP-WEIXIN\r\nversion: 2.0.51\r\nenv-version: develop\r\n",
    ]]);
    $started = microtime(true);
    $stream = @fopen($baseUrl . $path, 'rb', false, $context);
    if ($stream === false) throw new RuntimeException('HTTP transport unknown');
    try { $raw = stream_get_contents($stream, 262145); }
    finally { fclose($stream); }
    $http = isset($http_response_header[0])
        && preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0], $m)
        ? (int)$m[1] : 0;
    if (!is_string($raw) || strlen($raw) > 262144 || $http !== 200) {
        throw new RuntimeException('HTTP status or body size mismatch');
    }
    $body = json_decode($raw, true);
    if (!is_array($body)) throw new RuntimeException('HTTP body not JSON');
    return ['http' => $http, 'status' => (int)($body['status'] ?? -1),
        'data' => $body['data'] ?? null,
        'elapsed_ms' => (int)round((microtime(true) - $started) * 1000)];
};
try {
    $ai->prepare("INSERT INTO $userTable "
        . '(STORE_ID,MOBILE,NICK_NAME,BALANCE,GIFT_BALANCE,IS_DELETE,CREATE_TIME) '
        . 'VALUES (10001,?,?,?,?,0,?)')
        ->execute(['', $marker, '0.00', '0.00', time()]);
    $userId = (int)($one($ai, "SELECT USER_ID FROM $userTable WHERE NICK_NAME=?",
        [$marker])['user_id'] ?? 0);
    if ($userId <= 0) throw new RuntimeException('Synthetic AI user missing');
    $journal['state'] = 'user_created';
    $journal['user_id'] = $userId;
    file_put_contents($journalPath, json_encode($journal), LOCK_EX);
    $service = new \app\api\service\WellandRope();
    if (!$service->upload($userId, [
        'mac' => $marker, 'mode' => 'free', 'stoppedAt' => $stoppedAt,
        'dateKey' => $date, 'count' => 123, 'duration' => 60, 'calories' => 5.5,
    ])) throw new RuntimeException('Synthetic rope upload failed');
    $sessions = $count($store, "SELECT COUNT(*) FROM $sessionTable WHERE AI_USER_ID=? "
        . "AND VENDOR='welland' AND DEVICE_TYPE='jump_rope'", [$userId]);
    $session = $one($store, "SELECT SESSION_ID,START_TIME,END_TIME,IS_VALID FROM $sessionTable "
        . "WHERE AI_USER_ID=? AND VENDOR='welland' AND DEVICE_TYPE='jump_rope'", [$userId]);
    $sessionId = (int)($session['session_id'] ?? 0);
    if ($sessions !== 1 || $sessionId <= 0 || (int)$session['is_valid'] !== 1
        || substr((string)$session['start_time'], 0, 10) !== $date) {
        throw new RuntimeException('Synthetic session source mismatch');
    }
    $journal['state'] = 'session_created';
    $journal['session_id'] = $sessionId;
    file_put_contents($journalPath, json_encode($journal), LOCK_EX);
    $metrics = [];
    foreach (['jump_count', 'activity_duration', 'calories_burned'] as $code) {
        $row = $one($store, "SELECT VALUE FROM $metricTable WHERE SESSION_ID=? "
            . 'AND AI_USER_ID=? AND METRIC_CODE=?', [$sessionId, $userId, $code]);
        if (!isset($row['value'])) throw new RuntimeException('Synthetic metric missing');
        $metrics[$code] = (float)$row['value'];
    }
    if ($metrics['jump_count'] !== 123.0 || $metrics['activity_duration'] !== 60.0
        || $metrics['calories_burned'] !== 5.5
        || $count($store, "SELECT COUNT(*) FROM $rawTable WHERE SESSION_ID=?", [$sessionId]) !== 1) {
        throw new RuntimeException('Synthetic source values mismatch');
    }
    $journal['state'] = 'before_http';
    if (file_put_contents($journalPath, json_encode($journal), LOCK_EX) === false) {
        throw new RuntimeException('Private recovery journal write failed');
    }
    if (!\think\facade\Cache::set($token,
        ['user' => ['user_id' => $userId], 'password_version' => 0,
            'login_type' => 'thirdparty'], 300)
        || !\think\facade\Cache::has($token)) {
        throw new RuntimeException('Short File token unavailable');
    }
    $paths = [
        'stats' => '/api/wolland.rope/stats?storeId=10001&date=' . $date,
        'all' => '/api/wolland.rope/all?storeId=10001&start_date=' . $date . '&end_date=' . $date,
        'session' => '/api/wolland.rope/session?storeId=10001&session_id=' . $sessionId,
    ];
    $responses = [];
    foreach ($paths as $name => $path) {
        $responses[$name] = $request($path, $token);
        if ($responses[$name]['status'] !== 200) {
            throw new RuntimeException('Rope read business status mismatch: ' . $name);
        }
    }
    $stats = $responses['stats']['data'];
    $all = $responses['all']['data'];
    $detail = $responses['session']['data'];
    $dayRows = is_array($stats) ? (array)($stats['last30Days'] ?? []) : [];
    $matchingDays = array_values(array_filter($dayRows,
        static function ($row) use ($date): bool {
            return is_array($row) && ($row['date'] ?? null) === $date
                && (int)($row['count'] ?? -1) === 123;
        }));
    if (!is_array($stats) || !is_array($all) || count($all) !== 1
        || !is_array($detail) || count($dayRows) !== 30
        || count($matchingDays) !== 1
        || (int)($stats['totalCount'] ?? -1) !== 123
        || (string)($stats['totalMinutes'] ?? '') !== '1分00秒'
        || abs((float)($stats['totalCalories'] ?? -1) - 5.5) > 0.001
        || (string)($all[0]['date'] ?? '') !== $date
        || (int)($all[0]['totalCount'] ?? -1) !== 123
        || (int)($all[0]['totalSessions'] ?? -1) !== 1
        || (string)($all[0]['totalDurationText'] ?? '') !== '1分00秒'
        || (int)($all[0]['sessions'][0]['sessionId'] ?? -1) !== $sessionId
        || (int)($detail['sessionId'] ?? -1) !== $sessionId
        || (int)($detail['count'] ?? -1) !== 123
        || (int)($detail['durationSeconds'] ?? -1) !== 60
        || (string)($detail['durationText'] ?? '') !== '1分00秒'
        || abs((float)($detail['calories'] ?? -1) - 5.5) > 0.001) {
        throw new RuntimeException('Rope read data not matched to original tables');
    }
    $sourceAfter = [
        'session' => $count($store, "SELECT COUNT(*) FROM $sessionTable WHERE SESSION_ID=? AND AI_USER_ID=?",
            [$sessionId, $userId]),
        'raw' => $count($store, "SELECT COUNT(*) FROM $rawTable WHERE SESSION_ID=?", [$sessionId]),
        'metrics' => $count($store, "SELECT COUNT(*) FROM $metricTable WHERE SESSION_ID=? AND AI_USER_ID=?",
            [$sessionId, $userId]),
    ];
    if ($sourceAfter !== ['session' => 1, 'raw' => 1, 'metrics' => 3]) {
        throw new RuntimeException('Rope source changed during reads');
    }
    $sessionAfter = $one($store, "SELECT START_TIME,END_TIME,IS_VALID FROM $sessionTable "
        . 'WHERE SESSION_ID=? AND AI_USER_ID=?', [$sessionId, $userId]);
    if ($sessionAfter['start_time'] !== $session['start_time']
        || $sessionAfter['end_time'] !== $session['end_time']
        || (int)$sessionAfter['is_valid'] !== 1) {
        throw new RuntimeException('Rope session changed during reads');
    }
    foreach ($metrics as $code => $expected) {
        $row = $one($store, "SELECT VALUE FROM $metricTable WHERE SESSION_ID=? "
            . 'AND AI_USER_ID=? AND METRIC_CODE=?', [$sessionId, $userId, $code]);
        if (!isset($row['value']) || abs((float)$row['value'] - $expected) > 0.001) {
            throw new RuntimeException('Rope metric changed during reads');
        }
    }
    $typeMap = static function ($value) use (&$typeMap): array {
        if (!is_array($value)) return ['type' => gettype($value)];
        $result = [];
        foreach ($value as $key => $part) {
            if (is_array($part) && ($part === []
                || array_keys($part) === range(0, count($part) - 1))) {
                $result[$key] = ['type' => 'list', 'count' => count($part),
                    'item' => isset($part[0]) ? $typeMap($part[0]) : null];
            } else {
                $result[$key] = $typeMap($part);
            }
        }
        return $result;
    };
    $out = ['mode' => 'execute', 'dialect' => $dialect, 'marker' => $marker,
        'user_id' => $userId, 'session_id' => $sessionId,
        'source' => ['raw_rows' => 1, 'metric_rows' => 3,
            'jump_count' => 123, 'duration_seconds' => 60, 'calories' => 5.5],
        'synthetic_values' => [
            'stats_total_minutes' => $stats['totalMinutes'],
            'stats_last_day' => $matchingDays[0],
            'all_date' => $all[0]['date'],
            'all_start_time' => $all[0]['sessions'][0]['startTime'],
            'all_total_duration' => $all[0]['totalDurationText'],
            'session_start_time' => $detail['startTime'],
            'session_end_time' => $detail['endTime'],
            'session_duration' => $detail['durationText'],
        ],
        'responses' => []];
    foreach ($responses as $name => $response) {
        $out['responses'][$name] = [
            'http' => $response['http'], 'status' => $response['status'],
            'elapsed_ms' => $response['elapsed_ms'],
            'shape' => $typeMap($response['data']),
        ];
    }
    echo json_encode($out, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
} finally {
    if ($token !== '') {
        try { \think\facade\Cache::delete($token); }
        catch (Throwable $e) { $cleanupError = 'token'; }
    }
    if ($userId <= 0) {
        try {
            $found = $one($ai, "SELECT USER_ID FROM $userTable WHERE NICK_NAME=?", [$marker]);
            $userId = (int)($found['user_id'] ?? 0);
        } catch (Throwable $e) { $cleanupError = 'user_lookup'; }
    }
    if ($sessionId <= 0 && $userId > 0) {
        $session = $one($store, "SELECT SESSION_ID FROM $sessionTable WHERE AI_USER_ID=? "
            . "AND VENDOR='welland' AND DEVICE_TYPE='jump_rope'", [$userId]);
        $sessionId = (int)($session['session_id'] ?? 0);
    }
    if ($sessionId > 0 && $userId > 0) {
        try {
            $store->beginTransaction();
            $store->prepare("DELETE FROM $metricTable WHERE SESSION_ID=? AND AI_USER_ID=?")
                ->execute([$sessionId, $userId]);
            $store->prepare("DELETE FROM $rawTable WHERE SESSION_ID=?")
                ->execute([$sessionId]);
            $store->prepare("DELETE FROM $sessionTable WHERE SESSION_ID=? AND AI_USER_ID=?")
                ->execute([$sessionId, $userId]);
            $store->commit();
        } catch (Throwable $e) {
            if ($store->inTransaction()) $store->rollBack();
            $cleanupError = 'store';
        }
    }
    if ($userId > 0) {
        try {
            $ai->prepare("DELETE FROM $userTable WHERE USER_ID=? AND NICK_NAME=?")
                ->execute([$userId, $marker]);
        } catch (Throwable $e) { $cleanupError = 'ai'; }
    }
    if ($journalPath !== '' && is_file($journalPath)) {
        $journal['state'] = $cleanupError === null ? 'deleted_needs_audit' : 'cleanup_uncertain';
        $journal['session_id'] = $sessionId;
        file_put_contents($journalPath, json_encode($journal), LOCK_EX);
    }
    if ($cleanupError !== null) {
        echo json_encode(['mode' => 'cleanup_uncertain', 'scope' => $cleanupError,
            'journal' => $journalPath]), PHP_EOL;
    } else {
        echo json_encode(['mode' => 'cleanup_needs_independent_audit',
            'journal' => $journalPath, 'marker' => $marker,
            'user_id' => $userId, 'session_id' => $sessionId]), PHP_EOL;
    }
}
