<?php

declare(strict_types=1);

$root = (string)getenv('POC_TARGET_ROOT');
$baseUrl = rtrim((string)getenv('POC_BASE_URL'), '/');
$dialect = (string)getenv('POC_DIALECT');
if ($root === '' || $baseUrl === '' || !in_array($dialect, ['mysql', 'dm'], true)) {
    throw new RuntimeException('Explicit isolated RopeSkipping target required');
}
if ($dialect === 'mysql') {
    $local = parse_ini_file((string)getenv('POC_MYSQL_LOCAL_ENV'), true, INI_SCANNER_RAW);
    if (!is_array($local) || !isset($local['DATABASE'])) {
        throw new RuntimeException('Selected local AI development database unavailable');
    }
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
        'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $from => $to) {
        putenv('POC_AI_' . $to . '=' . (string)$local['DATABASE'][$from]);
    }
}
foreach (['POC_AI_HOST', 'POC_AI_PORT', 'POC_AI_SCHEMA', 'POC_AI_USER',
    'POC_AI_PASSWORD'] as $part) {
    if ((string)getenv($part) === '') throw new RuntimeException('Approved AI database incomplete');
}
$boundary = json_decode((string)file_get_contents($baseUrl . '/__poc_boundary'), true);
if (empty($boundary['external_stubs'])) throw new RuntimeException('External stubs unavailable');

define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName('poc')->initialize();
set_exception_handler(static function (Throwable $error): void {
    fwrite(STDERR, 'ROPE_HTTP_FAIL ' . get_class($error) . ': '
        . mb_substr($error->getMessage(), 0, 420) . PHP_EOL);
    exit(1);
});

$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]);
$table = static function (string $name) use ($dialect): string {
    return $dialect === 'dm' ? strtoupper($name) : $name;
};
$ropeTable = $table('yoshop_rope_skipping');
$userTable = $table('yoshop_user');
$staffTable = $table('yoshop_staff');
$countColumn = $dialect === 'dm' ? '"COUNT"' : '`count`';
foreach ([$ropeTable, $userTable, $staffTable] as $required) {
    $db->query("SELECT 1 FROM $required WHERE 1=0");
}
$marker = 'CDXPARROPE' . strtoupper(bin2hex(random_bytes(4)));
$token = $marker . 'T' . strtoupper(bin2hex(random_bytes(10)));
$today = date('Y-m-d');
$millis = (int)floor(microtime(true) * 1000);
$userId = 0;
$staffId = 0;
$assert = static function (bool $ok, string $message): void {
    if (!$ok) throw new RuntimeException($message);
};
$read = static function (string $sql, array $args = []) use ($db): array {
    $statement = $db->prepare($sql);
    $statement->execute($args);
    $row = $statement->fetch(PDO::FETCH_ASSOC);
    return $row === false ? [] : array_change_key_case($row);
};
$count = static function (string $sql, array $args = []) use ($db): int {
    $statement = $db->prepare($sql);
    $statement->execute($args);
    return (int)$statement->fetchColumn();
};
$request = static function (string $action, ?string $auth, array $payload = [])
    use ($baseUrl): array {
    $method = $action === 'add' ? 'POST' : 'GET';
    $headers = "platform: POC\r\n";
    if ($method === 'POST') $headers .= "Content-Type: application/x-www-form-urlencoded\r\n";
    if ($auth !== null) $headers .= 'Access-Token: ' . $auth . "\r\n";
    $url = $baseUrl . '/api/RopeSkipping/' . $action . '?storeId=10001';
    if ($method === 'GET' && $payload) $url .= '&' . http_build_query($payload);
    $context = stream_context_create(['http' => [
        'method' => $method, 'header' => $headers,
        'content' => $method === 'POST' ? http_build_query(['form' => $payload]) : '',
        'timeout' => 35, 'ignore_errors' => true,
    ]]);
    $started = microtime(true);
    $raw = file_get_contents($url, false, $context);
    $httpLine = $http_response_header[0] ?? '';
    $body = is_string($raw) ? json_decode($raw, true) : null;
    if (!is_array($body)) throw new RuntimeException('Non-JSON rope response ' . $action);
    return [
        'http' => (int)(preg_match('/HTTP\/\S+\s+(\d+)/', $httpLine, $m) ? $m[1] : 0),
        'elapsed_ms' => (int)round((microtime(true) - $started) * 1000),
        'status' => (int)($body['status'] ?? -1),
        'data' => $body['data'] ?? null, 'message' => (string)($body['message'] ?? ''),
    ];
};
$shape = static function (array $response): array {
    return ['http' => $response['http'], 'status' => $response['status'],
        'elapsed_ms' => $response['elapsed_ms'],
        'data_type' => gettype($response['data']),
        'data_keys' => is_array($response['data']) ? array_keys($response['data']) : []];
};
$rows = static function () use ($db, $ropeTable, $countColumn, &$userId, $marker): array {
    $statement = $db->prepare("SELECT ID,USER_ID,STORE_ID,MAC,STOPPED_AT,DATE_KEY,"
        . "$countColumn AS CNT,DURATION,CALORIES,FAT_BURN_EFFICIENCY,TARGET_SECONDS,DATA"
        . " FROM $ropeTable WHERE USER_ID=? AND MAC=? ORDER BY ID");
    $statement->execute([$userId, $marker]);
    return array_map('array_change_key_case', $statement->fetchAll(PDO::FETCH_ASSOC));
};

try {
    $db->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)($read("SELECT USER_ID FROM $userTable WHERE NICK_NAME=?",
        [$marker])['user_id'] ?? 0);
    $db->prepare("INSERT INTO $staffTable"
        . " (NAME,USER_ID,STORE_ID,HEIGHT,WEIGHT,GENDER,BIRTHDAY,PHONE,IS_DELETE)"
        . " VALUES (?,?,10001,170,60,1,?,'',0)")
        ->execute([$marker, $userId, '1990-01-01']);
    $staffId = (int)($read("SELECT ID FROM $staffTable WHERE NAME=? AND USER_ID=?",
        [$marker, $userId])['id'] ?? 0);
    $assert($userId > 0 && $staffId > 0 && $rows() === [],
        'Synthetic rope user/staff fixture incomplete');
    \think\facade\Cache::set($token, ['user' => ['user_id' => $userId],
        'password_version' => 0, 'login_type' => 'thirdparty'], 300);

    $form1 = ['mac' => $marker, 'mode' => 'free', 'modeLabel' => '自由跳',
        'stoppedAt' => $millis, 'dateKey' => $today, 'dateLabel' => '今日',
        'timeLabel' => '12:00', 'count' => 120, 'duration' => 60,
        'calories' => '12.50', 'fatBurnEfficiency' => '0.1234',
        'avgFreq' => 110, 'maxFreq' => 140, 'mostJump' => 80,
        'ropeTrips' => 2, 'target' => ['seconds' => 300],
        'data' => [['second' => 1, 'count' => 2]]];
    $form2 = $form1;
    $form2['stoppedAt'] = $millis + 1000;
    $form2['count'] = 80;
    $form2['duration'] = 40;
    $form2['calories'] = '8.25';
    $form2['data'] = [['second' => 2, 'count' => 3]];

    $unauth = $request('add', null, $form1);
    $invalid = $request('add', $token, ['mac' => $marker, 'mode' => 'free']);
    $assert($unauth['status'] !== 200 && $invalid['status'] !== 200 && $rows() === [],
        'Unauthenticated/invalid rope add wrote a row');

    $add1 = $request('add', $token, $form1);
    $after1 = $rows();
    $assert($add1['http'] === 200 && $add1['status'] === 200
        && count($after1) === 1
        && (int)$after1[0]['user_id'] === $userId
        && (int)$after1[0]['store_id'] === 10001
        && (int)$after1[0]['stopped_at'] === $millis
        && (string)$after1[0]['date_key'] === $today
        && (int)$after1[0]['cnt'] === 120
        && (int)$after1[0]['duration'] === 60
        && abs((float)$after1[0]['calories'] - 12.5) < 0.001,
        'First rope add or original-table readback mismatch: '
            . json_encode(['response' => $shape($add1), 'rows' => count($after1),
                'stored_ms' => $after1[0]['stopped_at'] ?? null], JSON_UNESCAPED_UNICODE));

    $add2 = $request('add', $token, $form2);
    $after2 = $rows();
    $assert($add2['http'] === 200 && $add2['status'] === 200
        && count($after2) === 2
        && (int)$after2[1]['stopped_at'] === $millis + 1000
        && (int)$after2[1]['cnt'] === 80
        && (int)$after2[1]['duration'] === 40
        && abs((float)$after2[1]['calories'] - 8.25) < 0.001,
        'Second rope add or original-table readback mismatch');

    $latest = $request('latest', $token);
    $latestData = (array)$latest['data'];
    $assert($latest['http'] === 200 && $latest['status'] === 200
        && (int)($latestData['id'] ?? 0) === (int)$after2[1]['id']
        && (int)($latestData['stoppedAt'] ?? 0) === $millis + 1000
        && (int)($latestData['count'] ?? 0) === 80
        && (string)($latestData['dateKey'] ?? '') === $today
        && is_array($latestData['data'] ?? null),
        'Rope latest payload mismatch: ' . json_encode($shape($latest)));

    $list = $request('list', $token, ['pagesize' => 10]);
    $pages = (array)($list['data']['list'] ?? []);
    $groups = (array)($pages['data'] ?? []);
    $ourGroup = null;
    foreach ($groups as $group) {
        if (($group['dateKey'] ?? '') === $today) { $ourGroup = $group; break; }
    }
    $assert($list['http'] === 200 && $list['status'] === 200
        && is_array($ourGroup)
        && (int)($ourGroup['count'] ?? 0) === 200
        && (int)($ourGroup['duration'] ?? 0) === 100
        && count((array)($ourGroup['list'] ?? [])) === 2
        && (int)($ourGroup['list'][0]['stoppedAt'] ?? 0) === $millis + 1000
        && count($rows()) === 2,
        'Rope grouped list or SUM/count readback mismatch: '
            . json_encode(['response' => $shape($list),
                'message' => mb_substr($list['message'], 0, 200),
                'group_found' => is_array($ourGroup)], JSON_UNESCAPED_UNICODE));

    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'marker' => $marker, 'normal_millis' => $millis,
        'rows_after_add' => 2, 'group_count' => 200, 'group_duration' => 100,
        'responses' => ['unauth' => $shape($unauth), 'invalid' => $shape($invalid),
            'add1' => $shape($add1), 'add2' => $shape($add2),
            'latest' => $shape($latest), 'list' => $shape($list)],
        'contract' => ['stoppedAt' => gettype($latestData['stoppedAt']),
            'dateKey' => gettype($latestData['dateKey']),
            'calories' => gettype($latestData['calories']),
            'count' => gettype($latestData['count'])],
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
} finally {
    \think\facade\Cache::delete($token);
    $db->prepare("DELETE FROM $ropeTable WHERE MAC=?")->execute([$marker]);
    $db->prepare("DELETE FROM $staffTable WHERE NAME=?")->execute([$marker]);
    $db->prepare("DELETE FROM $userTable WHERE NICK_NAME=?")->execute([$marker]);
    $left = $count("SELECT COUNT(*) FROM $ropeTable WHERE MAC=?", [$marker])
        + $count("SELECT COUNT(*) FROM $staffTable WHERE NAME=?", [$marker])
        + $count("SELECT COUNT(*) FROM $userTable WHERE NICK_NAME=?", [$marker]);
    if ($left !== 0) throw new RuntimeException('Synthetic rope cleanup left rows=' . $left);
    fwrite(STDERR, 'Synthetic rope cleanup: marker=' . $marker . ' rows=0' . PHP_EOL);
}
