<?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 completion 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_COMPLETE_FAIL ' . get_class($error) . ': '
        . mb_substr($error->getMessage(), 0, 450) . 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;
};
$typeTable = $table('yoshop_drill_course_type');
$courseTable = $table('yoshop_drill_course');
$userTable = $table('yoshop_user');
$staffTable = $table('yoshop_staff');
$planTable = $table('yoshop_user_course');
$sportEnergyTable = $table('yoshop_user_sport_energy');
$scoreTable = $table('yoshop_user_score');
$scoreLogTable = $table('yoshop_user_score_log');
// Every table is checked before any insert; a missing table is a schema blocker.
foreach ([$typeTable, $courseTable, $userTable, $staffTable, $planTable,
    $sportEnergyTable, $scoreTable, $scoreLogTable] as $required) {
    $db->query("SELECT 1 FROM $required WHERE 1=0");
}

$marker = 'CDXPARCOURSEW' . strtoupper(bin2hex(random_bytes(4)));
$token = $marker . 'T' . strtoupper(bin2hex(random_bytes(10)));
$today = date('Y-m-d');
$leapDate = '2024-02-29';
$expectDateGuard = getenv('POC_EXPECT_VALID_DATE_GUARD') === '1';
$userId = 0;
$staffId = 0;
$typeId = 0;
$courseId = 0;
$planId = 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 $auth, array $form) use ($baseUrl): array {
    $headers = "platform: POC\r\nContent-Type: application/x-www-form-urlencoded\r\n";
    if ($auth !== null) $headers .= 'Access-Token: ' . $auth . "\r\n";
    $context = stream_context_create(['http' => [
        'method' => 'POST', 'header' => $headers,
        'content' => http_build_query(['form' => $form]),
        'timeout' => 40, 'ignore_errors' => true,
    ]]);
    $started = microtime(true);
    $raw = file_get_contents($baseUrl . '/api/drill.course/complete?storeId=10001',
        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 complete response ' . $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), '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']) : [],
        'exists_type' => is_array($response['data'])
            && array_key_exists('exists', $response['data'])
            ? gettype($response['data']['exists']) : null];
};
$plan = static function () use ($read, $planTable, &$userId, &$courseId, $today): array {
    return $read("SELECT ID,IS_COMPLETED,ENERGY_CONSUMED,FOLLOW_COUNT,TOTAL_ENERGY"
        . " FROM $planTable WHERE USER_ID=? AND COURSE_ID=? AND DATE=?",
        [$userId, $courseId, $today]);
};
$sportEnergy = static function () use ($read, $sportEnergyTable, &$userId, $today): array {
    return $read("SELECT ID,TYPE,ENERGY FROM $sportEnergyTable"
        . ' WHERE USER_ID=? AND DATE=? AND TYPE=2', [$userId, $today]);
};
$scoreLog = static function () use ($read, $scoreLogTable, &$userId, &$courseId, $today): array {
    return $read("SELECT ID,TYPE,COURSE_ID,SCORE,DATE FROM $scoreLogTable"
        . ' WHERE USER_ID=? AND COURSE_ID=? AND DATE=? AND TYPE=3',
        [$userId, $courseId, $today]);
};
$score = static function () use ($read, $scoreTable, &$userId): array {
    return $read("SELECT ID,TYPE,SCORE FROM $scoreTable WHERE USER_ID=? AND TYPE=3",
        [$userId]);
};
$state = static function () use ($count, $read, $plan, $score,
    $scoreLogTable, $scoreTable, $sportEnergyTable, $planTable, &$userId): array {
    $planRow = $plan();
    $scoreRow = $score();
    $totalEnergy = $read("SELECT COALESCE(SUM(ENERGY),0) AS ENERGY_SUM"
        . " FROM $sportEnergyTable WHERE USER_ID=?", [$userId]);
    return [
        'plan_rows' => $count("SELECT COUNT(*) FROM $planTable WHERE USER_ID=?", [$userId]),
        'plan_follow_count' => (int)($planRow['follow_count'] ?? 0),
        'plan_total_energy' => round((float)($planRow['total_energy'] ?? 0), 2),
        'score_rows' => $count("SELECT COUNT(*) FROM $scoreTable WHERE USER_ID=?", [$userId]),
        'score' => (int)($scoreRow['score'] ?? 0),
        'score_log_rows' => $count("SELECT COUNT(*) FROM $scoreLogTable WHERE USER_ID=?", [$userId]),
        'sport_energy_rows' => $count("SELECT COUNT(*) FROM $sportEnergyTable WHERE USER_ID=?", [$userId]),
        'sport_energy_sum' => round((float)($totalEnergy['energy_sum'] ?? 0), 2),
    ];
};

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);
    $db->prepare("INSERT INTO $typeTable"
        . ' (STORE_ID,NAME,TYPE,IS_DELETE) VALUES (10001,?,1,0)')
        ->execute([$typeName]);
    $typeId = (int)($read("SELECT ID FROM $typeTable WHERE NAME=?",
        [$typeName])['id'] ?? 0);
    $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,?)')
        ->execute([$typeId, $courseName, time()]);
    $courseId = (int)($read("SELECT ID FROM $courseTable WHERE NAME=?",
        [$courseName])['id'] ?? 0);
    $db->prepare("INSERT INTO $planTable"
        . ' (STORE_ID,USER_ID,USER_WEIGHT_STATUS_ID,DATE,COURSE_ID,IS_COMPLETED,'
        . ' ENERGY_CONSUMED,FOLLOW_COUNT,TOTAL_ENERGY,DELETE_FLAG)'
        . ' VALUES (10001,?,0,?,?,0,0,0,0,0)')
        ->execute([$userId, $today, $courseId]);
    $planId = (int)($plan()['id'] ?? 0);
    $assert(min($userId, $staffId, $typeId, $courseId, $planId) > 0,
        'Synthetic course write fixture incomplete');
    $assert($count("SELECT COUNT(*) FROM $scoreLogTable WHERE USER_ID=?", [$userId]) === 0
        && $count("SELECT COUNT(*) FROM $sportEnergyTable WHERE USER_ID=?", [$userId]) === 0
        && $count("SELECT COUNT(*) FROM $scoreTable WHERE USER_ID=?", [$userId]) === 0,
        'Synthetic user has preexisting financial/energy/score rows');
    \think\facade\Cache::set($token, ['user' => ['user_id' => $userId],
        'password_version' => 0, 'login_type' => 'thirdparty'], 300);

    $form = ['course_id' => $courseId, 'date' => $today, 'score' => 1];
    $unauth = $request(null, $form);
    $invalid = $request($token, ['course_id' => 0, 'date' => $today]);
    $assert($unauth['status'] === 401 && $invalid['status'] !== 200
        && (int)($plan()['follow_count'] ?? -1) === 0
        && $scoreLog() === [] && $sportEnergy() === [] && $score() === [],
        'Unauthenticated/invalid course complete caused writes');

    $first = $request($token, $form);
    $firstPlan = $plan();
    $firstEnergy = $sportEnergy();
    $firstLog = $scoreLog();
    $firstScore = $score();
    $perCallEnergy = (float)($firstPlan['energy_consumed'] ?? 0);
    $assert($first['http'] === 200 && $first['status'] === 200
        && is_array($first['data']) && ($first['data']['exists'] ?? null) === false
        && (int)($firstPlan['is_completed'] ?? 0) === 1
        && (int)($firstPlan['follow_count'] ?? 0) === 1
        && $perCallEnergy > 0
        && abs((float)($firstPlan['total_energy'] ?? 0) - $perCallEnergy) < 0.02
        && (int)($firstEnergy['type'] ?? 0) === 2
        && abs((float)($firstEnergy['energy'] ?? 0) - $perCallEnergy) < 0.02
        && (int)($firstLog['type'] ?? 0) === 3
        && (int)($firstLog['course_id'] ?? 0) === $courseId
        && (int)($firstLog['score'] ?? 0) === 1
        && (int)($firstScore['score'] ?? 0) === 1,
        'First complete did not persist plan/energy/score atomically: '
            . json_encode(['http' => $shape($first), 'plan' => $firstPlan,
                'energy' => $firstEnergy, 'score_log' => $firstLog,
                'score' => $firstScore], JSON_UNESCAPED_UNICODE));

    $repeat = $request($token, $form);
    $repeatPlan = $plan();
    $repeatEnergy = $sportEnergy();
    $logCount = $count("SELECT COUNT(*) FROM $scoreLogTable"
        . ' WHERE USER_ID=? AND COURSE_ID=? AND DATE=? AND TYPE=3',
        [$userId, $courseId, $today]);
    $scoreCount = $count("SELECT COUNT(*) FROM $scoreTable"
        . ' WHERE USER_ID=? AND TYPE=3', [$userId]);
    $energyCount = $count("SELECT COUNT(*) FROM $sportEnergyTable"
        . ' WHERE USER_ID=? AND DATE=? AND TYPE=2', [$userId, $today]);
    $assert($repeat['http'] === 200 && $repeat['status'] === 200
        && is_array($repeat['data']) && ($repeat['data']['exists'] ?? null) === true
        && (int)($repeatPlan['follow_count'] ?? 0) === 2
        && (int)($repeatPlan['is_completed'] ?? 0) === 1
        && abs((float)($repeatPlan['energy_consumed'] ?? 0) - $perCallEnergy) < 0.02
        && abs((float)($repeatPlan['total_energy'] ?? 0) - 2 * $perCallEnergy) < 0.02
        && abs((float)($repeatEnergy['energy'] ?? 0) - 2 * $perCallEnergy) < 0.02
        && $logCount === 1 && $scoreCount === 1 && $energyCount === 1
        && (int)($score()['score'] ?? 0) === 1,
        'Repeat complete business idempotency/cumulative energy mismatch: '
            . json_encode(['http' => $shape($repeat), 'plan' => $repeatPlan,
                'energy' => $repeatEnergy, 'log_count' => $logCount,
                'score_count' => $scoreCount, 'energy_count' => $energyCount],
                JSON_UNESCAPED_UNICODE));

    $beforeBoundary = $state();
    $missingForm = $request($token, []);
    $afterMissingForm = $state();
    $invalidTextDate = $request($token, ['course_id' => $courseId,
        'date' => 'not-a-date', 'score' => 1]);
    $afterTextDate = $state();
    $invalidCalendarDate = $request($token, ['course_id' => $courseId,
        'date' => '2026-02-30', 'score' => 1]);
    $afterCalendarDate = $state();
    $expectedDateMessage = 'date 必须为有效的 YYYY-MM-DD 日期';
    $dateMessagesMatch = strpos($invalidTextDate['message'], $expectedDateMessage) !== false
        && strpos($invalidCalendarDate['message'], $expectedDateMessage) !== false;
    $boundaryViolation = $missingForm['status'] === 200
        || $invalidTextDate['status'] === 200
        || $invalidCalendarDate['status'] === 200
        || $beforeBoundary !== $afterMissingForm
        || $beforeBoundary !== $afterTextDate
        || $beforeBoundary !== $afterCalendarDate
        || ($expectDateGuard && !$dateMessagesMatch);

    // A separate valid leap-day plan verifies the new guard accepts real dates.
    $db->prepare("INSERT INTO $planTable"
        . ' (STORE_ID,USER_ID,USER_WEIGHT_STATUS_ID,DATE,COURSE_ID,IS_COMPLETED,'
        . ' ENERGY_CONSUMED,FOLLOW_COUNT,TOTAL_ENERGY,DELETE_FLAG)'
        . ' VALUES (10001,?,0,?,?,0,0,0,0,0)')
        ->execute([$userId, $leapDate, $courseId]);
    $leap = $request($token, ['course_id' => $courseId,
        'date' => $leapDate, 'score' => 1]);
    $leapPlan = $read("SELECT ID,IS_COMPLETED,FOLLOW_COUNT,ENERGY_CONSUMED,TOTAL_ENERGY"
        . " FROM $planTable WHERE USER_ID=? AND COURSE_ID=? AND DATE=?",
        [$userId, $courseId, $leapDate]);
    $leapLog = $read("SELECT ID,COURSE_ID,SCORE,DATE FROM $scoreLogTable"
        . ' WHERE USER_ID=? AND COURSE_ID=? AND TYPE=3 AND DATE=?',
        [$userId, $courseId, $leapDate]);
    $leapEnergy = $read("SELECT ID,TYPE,ENERGY,DATE FROM $sportEnergyTable"
        . ' WHERE USER_ID=? AND TYPE=2 AND DATE=?', [$userId, $leapDate]);
    $leapPassed = $leap['status'] === 200
        && is_array($leap['data']) && ($leap['data']['exists'] ?? null) === false
        && (int)($leapPlan['is_completed'] ?? 0) === 1
        && (int)($leapPlan['follow_count'] ?? 0) === 1
        && abs((float)($leapPlan['energy_consumed'] ?? 0) - $perCallEnergy) < 0.02
        && abs((float)($leapPlan['total_energy'] ?? 0) - $perCallEnergy) < 0.02
        && (int)($leapLog['course_id'] ?? 0) === $courseId
        && (int)($leapLog['score'] ?? 0) === 1
        && (int)($leapEnergy['type'] ?? 0) === 2
        && abs((float)($leapEnergy['energy'] ?? 0) - $perCallEnergy) < 0.02;

    echo json_encode(['dialect' => $dialect,
        'status' => $boundaryViolation ? 'boundary-red'
            : ($leapPassed ? 'passed' : 'leap-date-red'),
        'marker' => $marker, 'course_id' => $courseId,
        'per_call_energy' => $perCallEnergy,
        'first' => $shape($first), 'repeat' => $shape($repeat),
        'unauth' => $shape($unauth), 'invalid' => $shape($invalid),
        'rows_after_repeat' => ['plan' => 1, 'sport_energy' => 1,
            'score_log' => $logCount, 'score' => $scoreCount],
        'plan_after_repeat' => ['is_completed' => 1, 'follow_count' => 2,
            'energy_consumed' => (float)$repeatPlan['energy_consumed'],
            'total_energy' => (float)$repeatPlan['total_energy']],
        'sport_energy_after_repeat' => (float)$repeatEnergy['energy'],
        'valid_leap_date' => ['response' => $shape($leap),
            'passed' => $leapPassed, 'plan_follow_count' => (int)($leapPlan['follow_count'] ?? 0),
            'score_log_row' => !empty($leapLog),
            'sport_energy_row' => !empty($leapEnergy)],
        'boundaries' => [
            'missing_form' => ['response' => $shape($missingForm),
                'state_changed' => $beforeBoundary !== $afterMissingForm],
            'text_date' => ['response' => $shape($invalidTextDate),
                'state_changed' => $beforeBoundary !== $afterTextDate],
            'invalid_calendar_date' => ['response' => $shape($invalidCalendarDate),
                'state_changed' => $beforeBoundary !== $afterCalendarDate],
            'date_guard_message_match' => $dateMessagesMatch,
            'text_date_message' => $invalidTextDate['message'],
            'invalid_calendar_message' => $invalidCalendarDate['message'],
            'before' => $beforeBoundary,
            'after_missing_form' => $afterMissingForm,
            'after_text_date' => $afterTextDate,
            'after_invalid_calendar_date' => $afterCalendarDate,
        ],
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
    if ($boundaryViolation || !$leapPassed) {
        throw new RuntimeException('Course date guard or valid leap day failed');
    }
} finally {
    \think\facade\Cache::delete($token);
    foreach ([$scoreLogTable, $scoreTable, $sportEnergyTable, $planTable] as $childTable) {
        if ($userId > 0) {
            $db->prepare("DELETE FROM $childTable WHERE USER_ID=?")->execute([$userId]);
        }
    }
    $db->prepare("DELETE FROM $courseTable WHERE NAME=?")->execute([$courseName]);
    $db->prepare("DELETE FROM $typeTable 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 $typeTable WHERE NAME=?", [$typeName])
        + $count("SELECT COUNT(*) FROM $staffTable WHERE NAME=?", [$marker])
        + $count("SELECT COUNT(*) FROM $userTable WHERE NICK_NAME=?", [$marker]);
    if ($userId > 0) {
        foreach ([$scoreLogTable, $scoreTable, $sportEnergyTable, $planTable] as $childTable) {
            $left += $count("SELECT COUNT(*) FROM $childTable WHERE USER_ID=?", [$userId]);
        }
    }
    if ($left !== 0) throw new RuntimeException('Synthetic course write cleanup left rows=' . $left);
    fwrite(STDERR, 'Synthetic course write cleanup: marker=' . $marker . ' rows=0' . PHP_EOL);
}
