<?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 course HTTP 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, 'COURSE_READ_FAIL ' . get_class($error) . ': '
        . mb_substr($error->getMessage(), 0, 320) . 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;
};
$courseTypeTable = $table('yoshop_drill_course_type');
$courseTable = $table('yoshop_drill_course');
$userTable = $table('yoshop_user');
$staffTable = $table('yoshop_staff');
$scoreLogTable = $table('yoshop_user_score_log');
$marker = 'CDXPARCOURSE' . strtoupper(bin2hex(random_bytes(4)));
$token = $marker . 'T' . strtoupper(bin2hex(random_bytes(10)));
$userId = 0;
$staffId = 0;
$typeId = 0;
$courseId = 0;
$typeName = $marker . 'TYPE';
$courseName = $marker . 'COURSE';
$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 $path, ?string $auth = null) use ($baseUrl): array {
    $headers = "platform: POC\r\n";
    if ($auth !== null) $headers .= 'Access-Token: ' . $auth . "\r\n";
    $url = $baseUrl . $path . (strpos($path, '?') === false ? '?' : '&') . 'storeId=10001';
    $context = stream_context_create(['http' => [
        'method' => 'GET', 'header' => $headers, '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 response: ' . $path . ' ' . $httpLine);
    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), 'body' => $body,
    ];
};
$shape = static function (array $response): array {
    $data = $response['body']['data'] ?? null;
    return ['http' => $response['http'], 'status' => $response['status'],
        'elapsed_ms' => $response['elapsed_ms'],
        'data_type' => gettype($data),
        'data_keys' => is_array($data) ? array_keys($data) : []];
};
$findId = static function (array $items, int $id): ?array {
    foreach ($items as $item) {
        if (is_array($item) && (int)($item['id'] ?? 0) === $id) return $item;
    }
    return null;
};

// Type 1/2/3 is selected only when this merchant has no existing active course
// for that type, so detailByType deterministically returns our own fixture.
$targetType = 0;
foreach ([1, 2, 3] as $candidate) {
    $existing = $count("SELECT COUNT(*) FROM $courseTable c JOIN $courseTypeTable t"
        . ' ON c.TYPE_ID=t.ID WHERE c.STORE_ID=10001 AND t.STORE_ID=10001'
        . ' AND c.IS_DELETE=0 AND t.IS_DELETE=0 AND t.TYPE=?', [$candidate]);
    if ($existing === 0) { $targetType = $candidate; break; }
}
$assert($targetType > 0, 'No isolated course type available for deterministic detailByType');

try {
    $insertUser = $db->prepare("INSERT INTO $userTable"
        . ' (STORE_ID,MOBILE,NICK_NAME,BALANCE,GIFT_BALANCE,IS_DELETE,CREATE_TIME)'
        . ' VALUES (10001,?,?,?,?,0,?)');
    $insertUser->execute(['', $marker, '0.00', '0.00', time()]);
    $userId = (int)($read("SELECT USER_ID FROM $userTable WHERE NICK_NAME=?",
        [$marker])['user_id'] ?? 0);
    $insertStaff = $db->prepare("INSERT INTO $staffTable"
        . " (NAME,USER_ID,STORE_ID,HEIGHT,WEIGHT,GENDER,BIRTHDAY,PHONE,IS_DELETE)"
        . " VALUES (?,?,10001,170,60,1,?,'',0)");
    $insertStaff->execute([$marker, $userId, '1990-01-01']);
    $staffId = (int)($read("SELECT ID FROM $staffTable WHERE USER_ID=? AND NAME=?",
        [$userId, $marker])['id'] ?? 0);
    $insertType = $db->prepare("INSERT INTO $courseTypeTable"
        . ' (STORE_ID,NAME,TYPE,IS_DELETE) VALUES (10001,?,?,0)');
    $insertType->execute([$typeName, $targetType]);
    $typeId = (int)($read("SELECT ID FROM $courseTypeTable WHERE NAME=?",
        [$typeName])['id'] ?? 0);
    $insertCourse = $db->prepare("INSERT INTO $courseTable"
        . ' (STORE_ID,TYPE_ID,SERIAL_NO,NAME,INTENSITY,DURATION,IS_DELETE,CREATE_TIME)'
        . ' VALUES (10001,?,1,?,1.25,3720,0,?)');
    $insertCourse->execute([$typeId, $courseName, time()]);
    $courseId = (int)($read("SELECT ID FROM $courseTable WHERE NAME=?",
        [$courseName])['id'] ?? 0);
    $assert(min($userId, $staffId, $typeId, $courseId) > 0,
        'Synthetic course/user identity unavailable');
    $assert($count("SELECT COUNT(*) FROM $scoreLogTable WHERE USER_ID=?", [$userId]) === 0,
        'Synthetic user has preexisting course score log');
    \think\facade\Cache::set($token, ['user' => ['user_id' => $userId],
        'password_version' => 0, 'login_type' => 'thirdparty'], 300);

    $unauth = $request('/api/drill.course/all?type_id=' . $typeId);
    $assert($unauth['status'] === 401, 'Unauthenticated course list was not rejected');
    $types = $request('/api/drill.course_type/all', $token);
    $typeList = (array)($types['body']['data']['list'] ?? []);
    $typeRow = $findId($typeList, $typeId);
    $assert($types['http'] === 200 && $types['status'] === 200
        && $typeRow !== null && (string)$typeRow['name'] === $typeName,
        'Course type all response does not match original row');

    $all = $request('/api/drill.course/all?type_id=' . $typeId, $token);
    $allList = (array)($all['body']['data']['list'] ?? []);
    $allRow = $findId($allList, $courseId);
    $assert($all['http'] === 200 && $all['status'] === 200
        && $allRow !== null && (int)$allRow['typeId'] === $typeId
        && (int)$allRow['duration'] === 3720
        && is_numeric($allRow['calories'] ?? null),
        'Course all response does not match fixture');

    $detail = $request('/api/drill.course/detail?course_id=' . $courseId, $token);
    $detailRow = (array)($detail['body']['data']['detail'] ?? []);
    $assert($detail['http'] === 200 && $detail['status'] === 200
        && (int)($detailRow['id'] ?? 0) === $courseId
        && (int)($detailRow['typeId'] ?? 0) === $typeId
        && (int)($detailRow['is_completed_today'] ?? -1) === 0,
        'Course detail response or zero score state mismatch');

    $byType = $request('/api/drill.Course/detailByType?type=' . $targetType, $token);
    $byTypeRow = (array)($byType['body']['data']['detail'] ?? []);
    $assert($byType['http'] === 200 && $byType['status'] === 200
        && (int)($byTypeRow['id'] ?? 0) === $courseId,
        'Course detailByType JOIN/value result mismatch: '
            . json_encode(['response' => $shape($byType),
                'message' => mb_substr((string)($byType['body']['message'] ?? ''), 0, 180),
                'returned_id' => $byTypeRow['id'] ?? null,
                'expected_id' => $courseId], JSON_UNESCAPED_UNICODE));

    $progress = $request('/api/drill.Course/progressByType?type=' . $targetType, $token);
    $progressData = (array)($progress['body']['data'] ?? []);
    $groups = (array)($progressData['list'] ?? []);
    $flattened = [];
    foreach ($groups as $items) foreach ((array)$items as $item) $flattened[] = $item;
    $progressRow = $findId($flattened, $courseId);
    $assert($progress['http'] === 200 && $progress['status'] === 200
        && (int)($progressData['total_course_count'] ?? 0) === 1
        && (int)($progressData['completed_course_count'] ?? -1) === 0
        && $progressRow !== null,
        'Course progressByType grouped/count result mismatch');

    $invalid = $request('/api/drill.Course/detailByType?type=9', $token);
    $assert($invalid['status'] !== 200, 'Invalid course type accepted');
    $courseDb = $read("SELECT ID,TYPE_ID,SERIAL_NO,NAME,INTENSITY,DURATION,IS_DELETE"
        . " FROM $courseTable WHERE ID=?", [$courseId]);
    $typeDb = $read("SELECT ID,NAME,TYPE,IS_DELETE FROM $courseTypeTable WHERE ID=?",
        [$typeId]);
    $assert((int)($courseDb['id'] ?? 0) === $courseId
        && (int)($courseDb['type_id'] ?? 0) === $typeId
        && (float)($courseDb['intensity'] ?? 0) === 1.25
        && (int)($courseDb['duration'] ?? 0) === 3720
        && (int)($courseDb['is_delete'] ?? -1) === 0
        && (string)($courseDb['name'] ?? '') === $courseName
        && (int)($typeDb['type'] ?? 0) === $targetType
        && (int)($typeDb['is_delete'] ?? -1) === 0
        && (string)($typeDb['name'] ?? '') === $typeName
        && $count("SELECT COUNT(*) FROM $scoreLogTable WHERE USER_ID=?", [$userId]) === 0,
        'Read-only course APIs mutated original tables or fixture changed');

    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'marker' => $marker, 'fixture' => ['type_id' => $typeId,
            'course_id' => $courseId, 'target_type' => $targetType,
            'intensity' => '1.25', 'duration' => 3720,
            'course_score_log_rows' => 0],
        'responses' => ['unauth' => $shape($unauth), 'type_all' => $shape($types),
            'course_all' => $shape($all), 'detail' => $shape($detail),
            'detail_by_type' => $shape($byType), 'progress_by_type' => $shape($progress),
            'invalid_type' => $shape($invalid)],
        'contract' => ['type' => ['id' => gettype($typeRow['id']),
            'name' => gettype($typeRow['name'])],
            'course_all' => ['id' => gettype($allRow['id']),
                'duration' => gettype($allRow['duration']),
                'calories' => gettype($allRow['calories'])],
            'detail' => ['id' => gettype($detailRow['id']),
                'is_completed_today' => gettype($detailRow['is_completed_today'])],
            'progress' => ['total_course_count' => gettype($progressData['total_course_count']),
                'completed_course_count' => gettype($progressData['completed_course_count'])]],
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
} finally {
    \think\facade\Cache::delete($token);
    $unexpectedScoreLogs = $userId > 0
        ? $count("SELECT COUNT(*) FROM $scoreLogTable WHERE USER_ID=?", [$userId]) : 0;
    if ($unexpectedScoreLogs > 0) {
        $db->prepare("DELETE FROM $scoreLogTable WHERE USER_ID=?")->execute([$userId]);
    }
    $db->prepare("DELETE FROM $courseTable WHERE NAME=?")->execute([$courseName]);
    $db->prepare("DELETE FROM $courseTypeTable WHERE NAME=?")->execute([$typeName]);
    $db->prepare("DELETE FROM $staffTable WHERE NAME=?")->execute([$marker]);
    $db->prepare("DELETE FROM $userTable WHERE NICK_NAME=?")->execute([$marker]);
    $left = $count("SELECT COUNT(*) FROM $courseTable WHERE NAME=?", [$courseName])
        + $count("SELECT COUNT(*) FROM $courseTypeTable WHERE NAME=?", [$typeName])
        + $count("SELECT COUNT(*) FROM $staffTable WHERE NAME=?", [$marker])
        + $count("SELECT COUNT(*) FROM $userTable WHERE NICK_NAME=?", [$marker])
        + ($userId > 0
            ? $count("SELECT COUNT(*) FROM $scoreLogTable WHERE USER_ID=?", [$userId]) : 0);
    if ($left !== 0) throw new RuntimeException('Synthetic course cleanup left rows=' . $left);
    fwrite(STDERR, 'Synthetic course cleanup: marker=' . $marker . ' rows=0' . PHP_EOL);
    if ($unexpectedScoreLogs > 0) {
        throw new RuntimeException('Read-only course API wrote score logs: '
            . $unexpectedScoreLogs);
    }
}
